[
  {
    "path": ".azure-pipelines/INSTALL.md",
    "content": "# Configuring Azure Pipelines with Certbot\n\nLet's begin. All pipelines are defined in `.azure-pipelines`. Currently there are two:\n* `.azure-pipelines/main.yml` is the main one, executed on PRs for main, and pushes to main,\n* `.azure-pipelines/advanced.yml` add installer testing on top of the main pipeline, and is executed for `test-*` branches, release branches, and nightly run for main.\n\nSeveral templates are defined in `.azure-pipelines/templates`. These YAML files aggregate common jobs configuration that can be reused in several pipelines.\n\nUnlike Travis, where CodeCov is working without any action required, CodeCov supports Azure Pipelines\nusing the coverage-bash utility (not python-coverage for now) only if you provide the Codecov repo token\nusing the `CODECOV_TOKEN` environment variable. So `CODECOV_TOKEN` needs to be set as a secured\nenvironment variable to allow the main pipeline to publish coverage reports to CodeCov.\n\nThis INSTALL.md file explains how to configure Azure Pipelines with Certbot in order to execute the CI/CD logic defined in `.azure-pipelines` folder with it.\nDuring this installation step, warnings describing user access and legal comitments will be displayed like this:\n```\n!!! ACCESS REQUIRED !!!\n```\n\nThis document suppose that the Azure DevOps organization is named _certbot_, and the Azure DevOps project is also _certbot_.\n\n## Useful links\n\n* https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema?view=azure-devops&tabs=schema\n* https://www.azuredevopslabs.com/labs/azuredevops/github-integration/\n* https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/python?view=azure-devops\n\n## Prerequisites\n\n### Having a GitHub account\n\nUse your GitHub user for a normal GitHub account, or a user that has administrative rights to the GitHub organization if relevant.\n\n### Having an Azure DevOps account\n- Go to https://dev.azure.com/, click \"Start free with GitHub\"\n- Login to GitHub\n\n```\n!!! ACCESS REQUIRED !!!\nPersonal user data (email + profile info, in read-only)\n```\n\n- Microsoft will create a Live account using the email referenced for the GitHub account. This account is also linked to GitHub account (meaning you can log it using GitHub authentication)\n- Proceed with account registration (birth date, country), add details about name and email contact\n\n```\n!!! ACCESS REQUIRED !!!\nMicrosoft proposes to send commercial links to this mail\nAzure DevOps terms of service need to be accepted\n```\n\n_Logged to Azure DevOps, account is ready._\n\n### Installing Azure Pipelines to GitHub\n\n- On GitHub, go to Marketplace\n- Select Azure Pipeline, and \"Set up a plan\"\n- Select Free, then \"Install it for free\"\n- Click \"Complete order and begin installation\"\n\n```\n!!! ACCESS !!!\nAzure Pipeline needs RW on code, RO on metadata, RW on checks, commit statuses, deployments, issues, pull requests.\nRW access here is required to allow update of the pipelines YAML files from Azure DevOps interface, and to\nupdate the status of builds and PRs on GitHub side when Azure Pipelines are triggered.\nNote however that no admin access is defined here: this means that Azure Pipelines cannot do anything with\nprotected branches, like main, and cannot modify the security context around this on GitHub.\nAccess can be defined for all or only selected repositories, which is nice.\n```\n\n- Redirected to Azure DevOps, select the account created in _Having an Azure DevOps account_ section.\n- Select the organization, and click \"Create a new project\" (let's name it the same than the targeted github repo)\n- The Visibility is public, to profit from 10 parallel jobs\n\n```\n!!! ACCESS !!!\nAzure Pipelines needs access to the GitHub account (in term of being able to check it is valid), and the Resources shared between the GitHub account and Azure Pipelines.\n```\n\n_Done. We can move to pipelines configuration._\n\n## Import an existing pipelines from `.azure-pipelines` folder\n\n- On Azure DevOps, go to your organization (eg. _certbot_) then your project (eg. _certbot_)\n- Click \"Pipelines\" tab\n- Click \"New pipeline\"\n- Where is your code?: select \"__Use the classic editor__\"\n\n__Warning: Do not choose the GitHub option in Where is your code? section. Indeed, this option will trigger an OAuth\ngrant permissions from Azure Pipelines to GitHub in order to setup a GitHub OAuth Application. The permissions asked\nthen are way too large (admin level on almost everything), while the classic approach does not add any more\npermissions, and works perfectly well.__\n\n- Select GitHub in \"Select your repository section\", choose certbot/certbot in Repository, main in default branch.\n- Click on YAML option for \"Select a template\"\n- Choose a name for the pipeline (eg. test-pipeline), and browse to the actual pipeline YAML definition in the\n  \"YAML file path\" input (eg. `.azure-pipelines/test-pipeline.yml`)\n- Click \"Save & queue\", choose the main branch to build the first pipeline, and click \"Save and run\" button.\n\n_Done. Pipeline is operational. Repeat to add more pipelines from existing YAML files in `.azure-pipelines`._\n\n## Add a secret variable to a pipeline (like `CODECOV_TOKEN`)\n\n__NB: Following steps suppose that you already setup the YAML pipeline file to\nconsume the secret variable that these steps will create as an environment variable.\nFor a variable named `CODECOV_TOKEN` consuming the variable `codecov_token`,\nin the YAML file this setup would take the form of the following:\n```\nsteps:\n    - script: ./do_something_that_consumes_CODECOV_TOKEN  # Eg. `codecov -F windows`\n      env:\n        CODECOV_TOKEN: $(codecov_token)\n```\n\nTo set up a variable that is shared between pipelines, follow the instructions\nat\nhttps://docs.microsoft.com/en-us/azure/devops/pipelines/library/variable-groups.\nWhen adding variables to a group, don't forget to tick \"Keep this value secret\"\nif it shouldn't be shared publcily.\n"
  },
  {
    "path": ".azure-pipelines/advanced-test.yml",
    "content": "# Advanced pipeline for running our full test suite on demand.\ntrigger:\n  # When changing these triggers, please ensure the documentation under\n  # \"Running tests in CI\" is still correct.\n  - test-*\npr: none\n\nvariables:\n  # We don't publish our Docker images in this pipeline, but when building them\n  # for testing, let's use the nightly tag.\n  dockerTag: nightly\n  snapBuildTimeout: 5400\n\nstages:\n  - template: templates/stages/test-and-package-stage.yml\n"
  },
  {
    "path": ".azure-pipelines/main.yml",
    "content": "# We run the test suite on commits to main so codecov gets coverage data\n# about the main branch and can use it to track coverage changes.\ntrigger:\n  - main\npr:\n  - main\n  - '*.x'\n\nvariables:\n  # We set this here to avoid coverage data being uploaded from things like our\n  # nightly pipeline. This is done because codecov (helpfully) keeps track of\n  # the number of coverage uploads for a commit and displays a warning when\n  # comparing two commits with an unequal number of uploads. Only uploading\n  # coverage here should keep the number of uploads it sees consistent.\n  uploadCoverage: true\n\njobs:\n  - template: templates/jobs/standard-tests-jobs.yml\n"
  },
  {
    "path": ".azure-pipelines/nightly.yml",
    "content": "# Nightly pipeline running each day for main.\ntrigger: none\npr: none\nschedules:\n  - cron: \"30 4 * * *\"\n    displayName: Nightly build\n    branches:\n      include:\n      - main\n    always: true\n\nvariables:\n  dockerTag: nightly\n  snapBuildTimeout: 19800\n\nstages:\n  - template: templates/stages/test-and-package-stage.yml\n  - template: templates/stages/changelog-stage.yml\n  - template: templates/stages/nightly-deploy-stage.yml\n"
  },
  {
    "path": ".azure-pipelines/release.yml",
    "content": "# Release pipeline to run our full test suite, build artifacts, and deploy them\n# for GitHub release tags.\ntrigger:\n  tags:\n    include:\n      - v*\npr: none\n\nvariables:\n  dockerTag: ${{variables['Build.SourceBranchName']}}\n  snapBuildTimeout: 19800\n\nstages:\n  - template: templates/stages/test-and-package-stage.yml\n  - template: templates/stages/changelog-stage.yml\n  - template: templates/stages/release-deploy-stage.yml\n"
  },
  {
    "path": ".azure-pipelines/templates/jobs/common-deploy-jobs.yml",
    "content": "# As (somewhat) described at\n# https://docs.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops#context,\n# each template only has access to the parameters passed into it. To help make\n# use of this design, we define snapReleaseChannel without a default value\n# which requires the user of this template to define it as described at\n# https://docs.microsoft.com/en-us/azure/devops/pipelines/yaml-schema/parameters-name?view=azure-pipelines#remarks.\n# This makes the user of this template be explicit while allowing them to\n# define their own parameters with defaults that make sense for that context.\nparameters:\n- name: snapReleaseChannel\n  type: string\n  values:\n  - edge\n  - beta\n\njobs:\n  # This job relies on credentials used to publish the Certbot snaps. This\n  # credential file was created by running:\n  #\n  #   snapcraft logout\n  #   snapcraft export-login --channels=beta,edge snapcraft.cfg\n  #     (provide the shared snapcraft credentials when prompted)\n  #\n  # Then the file was added as a secure file in Azure pipelines\n  # with the name snapcraft.cfg by following the instructions at\n  # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops\n  # including authorizing the file for use in the \"nightly\" and \"release\"\n  # pipelines as described at\n  # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/secure-files?view=azure-devops#q-how-do-i-authorize-a-secure-file-for-use-in-a-specific-pipeline.\n  #\n  # This file has a maximum lifetime of one year. Revoking these credentials\n  # can be done by changing the password of the account used to generate the\n  # credentials. See\n  # https://forum.snapcraft.io/t/revoking-exported-credentials/19031 for more\n  # info.\n  - job: publish_snap\n    pool:\n      vmImage: ubuntu-22.04\n    variables:\n      - group: certbot-common\n    strategy:\n      matrix:\n        amd64:\n          SNAP_ARCH: amd64\n        arm32v6:\n          SNAP_ARCH: armhf\n        arm64v8:\n          SNAP_ARCH: arm64\n    steps:\n      - bash: |\n          set -e\n          sudo apt-get update\n          sudo apt-get install -y --no-install-recommends snapd\n          sudo snap install --classic snapcraft\n        displayName: Install dependencies\n      - task: DownloadPipelineArtifact@2\n        inputs:\n          artifact: snaps_$(SNAP_ARCH)\n          path: $(Build.SourcesDirectory)/snap\n        displayName: Retrieve Certbot snaps\n      - task: DownloadSecureFile@1\n        name: snapcraftCfg\n        inputs:\n          secureFile: snapcraft.cfg\n      - bash: |\n          set -e\n          export SNAPCRAFT_STORE_CREDENTIALS=$(cat \"$(snapcraftCfg.secureFilePath)\")\n          for SNAP_FILE in snap/*.snap; do\n            tools/retry.sh eval snapcraft upload --release=${{ parameters.snapReleaseChannel }} \"${SNAP_FILE}\"\n          done\n        displayName: Publish to Snap store\n  # The credentials used in the following jobs are for the shared\n  # certbotbot account on Docker Hub. The credentials are stored\n  # in a service account which was created by following the\n  # instructions at\n  # https://docs.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml#sep-docreg.\n  # The name given to this service account must match the value\n  # given to containerRegistry below. The authentication used when\n  # creating this service account was a personal access token\n  # rather than a password to bypass 2FA. When Brad set this up,\n  # Azure Pipelines failed to verify the credentials with an error\n  # like \"access is forbidden with a JWT issued from a personal\n  # access token\", but after saving them without verification, the\n  # access token worked when the pipeline actually ran. \"Grant\n  # access to all pipelines\" should also be checked on the service\n  # account. The access token can be deleted on Docker Hub if\n  # these credentials need to be revoked.\n  - job: publish_docker_by_arch\n    pool:\n      vmImage: ubuntu-22.04\n    strategy:\n      matrix:\n        arm32v6:\n          DOCKER_ARCH: arm32v6\n        arm64v8:\n          DOCKER_ARCH: arm64v8\n        amd64:\n          DOCKER_ARCH: amd64\n    steps:\n      - task: DownloadPipelineArtifact@2\n        inputs:\n          artifact: docker_$(DOCKER_ARCH)\n          path: $(Build.SourcesDirectory)\n        displayName: Retrieve Docker images\n      - bash: set -e && docker load --input $(Build.SourcesDirectory)/images.tar\n        displayName: Load Docker images\n      - task: Docker@2\n        inputs:\n          command: login\n          containerRegistry: docker-hub\n        displayName: Login to Docker Hub\n      - bash: set -e && tools/docker/deploy_images.sh $(dockerTag) $DOCKER_ARCH\n        displayName: Deploy the Docker images by architecture\n  - job: publish_docker_multiarch\n    dependsOn: publish_docker_by_arch\n    pool:\n      vmImage: ubuntu-22.04\n    steps:\n      - task: Docker@2\n        inputs:\n          command: login\n          containerRegistry: docker-hub\n        displayName: Login to Docker Hub\n      - bash: set -e && tools/docker/deploy_manifests.sh $(dockerTag) all\n        displayName: Deploy the Docker multiarch manifests\n"
  },
  {
    "path": ".azure-pipelines/templates/jobs/extended-tests-jobs.yml",
    "content": "jobs:\n  - job: extended_test\n    variables:\n      - name: IMAGE_NAME\n        value: ubuntu-22.04\n      - name: PYTHON_VERSION\n        value: 3.14\n      - group: certbot-common\n    strategy:\n      matrix:\n        linux-py311:\n          PYTHON_VERSION: 3.11\n          TOXENV: py311\n        linux-py312:\n          PYTHON_VERSION: 3.12\n          TOXENV: py312\n        linux-py313:\n          PYTHON_VERSION: 3.13\n          TOXENV: py313\n        linux-isolated:\n          TOXENV: 'isolated-acme,isolated-certbot,isolated-apache,isolated-cloudflare,isolated-digitalocean,isolated-dnsimple,isolated-dnsmadeeasy,isolated-gehirn,isolated-google,isolated-linode,isolated-luadns,isolated-nsone,isolated-ovh,isolated-rfc2136,isolated-route53,isolated-sakuracloud,isolated-nginx'\n        linux-integration-certbot-oldest:\n          PYTHON_VERSION: 3.10\n          TOXENV: integration-certbot-oldest\n        linux-integration-nginx-oldest:\n          PYTHON_VERSION: 3.10\n          TOXENV: integration-nginx-oldest\n        linux-py310-integration:\n          PYTHON_VERSION: 3.10\n          TOXENV: integration\n        linux-py311-integration:\n          PYTHON_VERSION: 3.11\n          TOXENV: integration\n        linux-py312-integration:\n          PYTHON_VERSION: 3.12\n          TOXENV: integration\n        linux-py313-integration:\n          PYTHON_VERSION: 3.13\n          TOXENV: integration\n        # python 3.14 integration tests are not run here because they're run as\n        # part of the standard test suite\n        nginx-compat:\n          TOXENV: nginx_compat\n        linux-integration-rfc2136:\n          IMAGE_NAME: ubuntu-22.04\n          PYTHON_VERSION: 3.12\n          TOXENV: integration-dns-rfc2136\n        le-modification:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: modification\n        farmtest-apache2:\n          PYTHON_VERSION: 3.12\n          TOXENV: test-farm-apache2\n    pool:\n      vmImage: $(IMAGE_NAME)\n    steps:\n      - template: ../steps/tox-steps.yml\n"
  },
  {
    "path": ".azure-pipelines/templates/jobs/packaging-jobs.yml",
    "content": "jobs:\n  - job: docker_build\n    pool:\n      vmImage: ubuntu-24.04\n    strategy:\n      matrix:\n        arm32v6:\n          DOCKER_ARCH: arm32v6\n        arm64v8:\n          DOCKER_ARCH: arm64v8\n        amd64:\n          DOCKER_ARCH: amd64\n    # The default timeout of 60 minutes is a little low for compiling\n    # cryptography on ARM architectures.\n    timeoutInMinutes: 180\n    steps:\n      - bash: set -e && tools/docker/build.sh $(dockerTag) $DOCKER_ARCH\n        displayName: Build the Docker images\n      # We don't filter for the Docker Hub organization to continue to allow\n      # easy testing of these scripts on forks.\n      - bash: |\n          set -e\n          DOCKER_IMAGES=$(docker images --filter reference='*/certbot' --filter reference='*/dns-*' --format '{{.Repository}}')\n          docker save --output images.tar $DOCKER_IMAGES\n        displayName: Save the Docker images\n      # If the name of the tar file or artifact changes, the deploy stage will\n      # also need to be updated.\n      - bash: set -e && mv images.tar $(Build.ArtifactStagingDirectory)\n        displayName: Prepare Docker artifact\n      - task: PublishPipelineArtifact@1\n        inputs:\n          path: $(Build.ArtifactStagingDirectory)\n          artifact: docker_$(DOCKER_ARCH)\n        displayName: Store Docker artifact\n  - job: docker_test\n    dependsOn: docker_build\n    pool:\n      vmImage: ubuntu-22.04\n    strategy:\n      matrix:\n        arm32v6:\n          DOCKER_ARCH: arm32v6\n        arm64v8:\n          DOCKER_ARCH: arm64v8\n        amd64:\n          DOCKER_ARCH: amd64\n    steps:\n      - task: DownloadPipelineArtifact@2\n        inputs:\n          artifact: docker_$(DOCKER_ARCH)\n          path: $(Build.SourcesDirectory)\n        displayName: Retrieve Docker images\n      - bash: set -e && docker load --input $(Build.SourcesDirectory)/images.tar\n        displayName: Load Docker images\n      - bash: |\n          set -e && tools/docker/test.sh $(dockerTag) $DOCKER_ARCH\n        displayName: Run integration tests for Docker images\n  - job: snaps_build\n    pool:\n      vmImage: ubuntu-22.04\n    strategy:\n      matrix:\n        amd64:\n          SNAP_ARCH: amd64\n        armhf:\n          SNAP_ARCH: armhf\n        arm64:\n          SNAP_ARCH: arm64\n    timeoutInMinutes: 0\n    steps:\n      - script: |\n          set -e\n          sudo apt-get update\n          sudo apt-get install -y --no-install-recommends snapd\n          sudo snap install --classic snapcraft\n        displayName: Install dependencies\n      - task: UsePythonVersion@0\n        inputs:\n          versionSpec: 3.12\n          addToPath: true\n      - task: DownloadSecureFile@1\n        name: credentials\n        inputs:\n          secureFile: launchpad-credentials\n      - script: |\n          set -e\n          git config --global user.email \"$(Build.RequestedForEmail)\"\n          git config --global user.name \"$(Build.RequestedFor)\"\n          mkdir -p ~/.local/share/snapcraft/provider/launchpad\n          cp $(credentials.secureFilePath) ~/.local/share/snapcraft/provider/launchpad/credentials\n          python3 tools/snap/build_remote.py ALL --archs ${SNAP_ARCH} --timeout $(snapBuildTimeout)\n        displayName: Build snaps\n      - script: |\n          set -e\n          mv *.snap $(Build.ArtifactStagingDirectory)\n          mv certbot-dns-*/*.snap $(Build.ArtifactStagingDirectory)\n        displayName: Prepare artifacts\n      - task: PublishPipelineArtifact@1\n        inputs:\n          path: $(Build.ArtifactStagingDirectory)\n          artifact: snaps_$(SNAP_ARCH)\n        displayName: Store snaps artifacts\n  - job: snap_run\n    dependsOn: snaps_build\n    pool:\n      vmImage: ubuntu-22.04\n    steps:\n      - task: UsePythonVersion@0\n        inputs:\n          versionSpec: 3.12\n          addToPath: true\n      - script: |\n          set -e\n          sudo apt-get update\n          sudo apt-get install -y --no-install-recommends nginx-light snapd\n          python3 -m venv venv\n          venv/bin/python tools/pip_install.py -U tox\n        displayName: Install dependencies\n      - task: DownloadPipelineArtifact@2\n        inputs:\n          artifact: snaps_amd64\n          path: $(Build.SourcesDirectory)/snap\n        displayName: Retrieve Certbot snaps\n      - script: |\n          set -e\n          sudo snap install --dangerous --classic snap/certbot_*.snap\n        displayName: Install Certbot snap\n      - script: |\n          set -e\n          venv/bin/python -m tox run -e integration-external,apacheconftest-external-with-pebble\n        displayName: Run tox\n  - job: snap_dns_run\n    dependsOn: snaps_build\n    pool:\n      vmImage: ubuntu-22.04\n    steps:\n      - script: |\n          set -e\n          sudo apt-get update\n          sudo apt-get install -y --no-install-recommends snapd\n        displayName: Install dependencies\n      - task: UsePythonVersion@0\n        inputs:\n          versionSpec: 3.12\n          addToPath: true\n      - task: DownloadPipelineArtifact@2\n        inputs:\n          artifact: snaps_amd64\n          path: $(Build.SourcesDirectory)/snap\n        displayName: Retrieve Certbot snaps\n      - script: |\n          set -e\n          python3 -m venv venv\n          venv/bin/python tools/pip_install.py -e certbot-ci\n        displayName: Prepare Certbot-CI\n      - script: |\n          set -e\n          sudo -E venv/bin/pytest certbot-ci/src/snap_integration_tests/dns_tests --allow-persistent-changes --snap-folder $(Build.SourcesDirectory)/snap --snap-arch amd64\n        displayName: Test DNS plugins snaps\n"
  },
  {
    "path": ".azure-pipelines/templates/jobs/standard-tests-jobs.yml",
    "content": "jobs:\n  - job: test\n    variables:\n      PYTHON_VERSION: 3.14\n    strategy:\n      matrix:\n        macos-cover:\n          IMAGE_NAME: macOS-15\n          TOXENV: cover\n          # As of pip 23.1.0, builds started failing on macOS unless this flag was set.\n          # See https://github.com/certbot/certbot/pull/9717#issuecomment-1610861794.\n          PIP_USE_PEP517: \"true\"\n        linux-oldest:\n          IMAGE_NAME: ubuntu-22.04\n          PYTHON_VERSION: 3.10\n          TOXENV: oldest\n        linux-py310:\n          # linux unit tests with the oldest python we support\n          IMAGE_NAME: ubuntu-22.04\n          PYTHON_VERSION: 3.10\n          TOXENV: py310\n        linux-cover:\n          # linux unit+cover tests with the newest python we support\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: cover\n        linux-lint:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: lint-posix\n        linux-mypy:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: mypy\n        linux-integration:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: integration\n        apache-compat:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: apache_compat\n        apacheconftest:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: apacheconftest-with-pebble\n        nginxroundtrip:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: nginxroundtrip\n        validate-changelog:\n          IMAGE_NAME: ubuntu-22.04\n          TOXENV: validate-changelog\n    pool:\n      vmImage: $(IMAGE_NAME)\n    steps:\n      - template: ../steps/tox-steps.yml\n  - job: test_sphinx_builds\n    pool:\n      vmImage: ubuntu-22.04\n    steps:\n      - template: ../steps/sphinx-steps.yml\n"
  },
  {
    "path": ".azure-pipelines/templates/stages/changelog-stage.yml",
    "content": "stages:\n  - stage: Changelog\n    jobs:\n      - job: prepare\n        pool:\n          vmImage: ubuntu-latest\n        steps:\n          # If we change the output filename from `release_notes.md`, it should also be changed in tools/create_github_release.py\n          - bash: |\n              set -e\n              CERTBOT_VERSION=\"$(cd certbot/src && python -c \"import certbot; print(certbot.__version__)\" && cd ~-)\"\n              \"${BUILD_REPOSITORY_LOCALPATH}/tools/extract_changelog.py\" \"${CERTBOT_VERSION}\" >> \"${BUILD_ARTIFACTSTAGINGDIRECTORY}/release_notes.md\"\n            displayName: Prepare changelog\n          - task: PublishPipelineArtifact@1\n            inputs:\n              path: $(Build.ArtifactStagingDirectory)\n              # If we change the artifact's name, it should also be changed in tools/create_github_release.py\n              artifact: changelog\n            displayName: Publish changelog\n"
  },
  {
    "path": ".azure-pipelines/templates/stages/nightly-deploy-stage.yml",
    "content": "stages:\n  - stage: Deploy\n    jobs:\n      - template: ../jobs/common-deploy-jobs.yml\n        parameters:\n          snapReleaseChannel: edge\n"
  },
  {
    "path": ".azure-pipelines/templates/stages/release-deploy-stage.yml",
    "content": "stages:\n  - stage: Deploy\n    jobs:\n      - template: ../jobs/common-deploy-jobs.yml\n        parameters:\n          snapReleaseChannel: beta\n      - job: create_github_release\n        pool:\n          vmImage: ubuntu-22.04\n        steps:\n        - task: DownloadPipelineArtifact@2\n          inputs:\n            artifact: changelog\n            path: '$(Pipeline.Workspace)'\n        - task: GitHubRelease@1\n          inputs:\n            # this \"github-releases\" credential is what azure pipelines calls a\n            # \"service connection\". it needs to be recreated annually. instructions\n            # to do so and further information about the token are available at\n            # https://github.com/EFForg/certbot-misc/wiki/Azure-Pipelines-setup#regenerating-github-release-credentials-for-use-on-azure\n            #\n            # as of writing this, the current token will expire on Wed, Feb 25 2026.\n            gitHubConnection: github-releases\n            title: ${{ format('Certbot {0}', replace(variables['Build.SourceBranchName'], 'v', '')) }}\n            releaseNotesFilePath: '$(Pipeline.Workspace)/release_notes.md'\n            assets: '$(Build.SourcesDirectory)/packages/{*.tar.gz,SHA256SUMS*}'\n            addChangeLog: false\n"
  },
  {
    "path": ".azure-pipelines/templates/stages/test-and-package-stage.yml",
    "content": "stages:\n  - stage: TestAndPackage\n    jobs:\n      - template: ../jobs/standard-tests-jobs.yml\n      - template: ../jobs/extended-tests-jobs.yml\n      - template: ../jobs/packaging-jobs.yml\n"
  },
  {
    "path": ".azure-pipelines/templates/steps/sphinx-steps.yml",
    "content": "steps:\n  - bash: |\n      set -e\n      sudo apt-get update\n      sudo apt-get install -y --no-install-recommends libaugeas-dev\n      FINAL_STATUS=0\n      declare -a FAILED_BUILDS\n      tools/venv.py\n      source venv/bin/activate\n      for doc_path in */docs\n      do\n        echo \"\"\n        echo \"##[group]Building $doc_path\"\n        if ! sphinx-build -W --keep-going -b html $doc_path $doc_path/_build/html; then\n          FINAL_STATUS=1\n          FAILED_BUILDS[${#FAILED_BUILDS[@]}]=\"${doc_path%/docs}\"\n        fi\n        echo \"##[endgroup]\"\n      done\n      if [[ $FINAL_STATUS -ne 0 ]]; then\n        echo \"##[error]The following builds failed: ${FAILED_BUILDS[*]}\"\n        exit 1\n      fi\n    displayName: Build Sphinx Documentation\n"
  },
  {
    "path": ".azure-pipelines/templates/steps/tox-steps.yml",
    "content": "# This does not include the dependencies needed to build cryptography. See\n# https://cryptography.io/en/latest/installation/\nsteps:\n  # We run brew update because we've seen attempts to install an older version\n  # of a package fail. See\n  # https://github.com/actions/virtual-environments/issues/3165.\n  #\n  # We untap homebrew/core and homebrew/cask and unset HOMEBREW_NO_INSTALL_FROM_API (which\n  # is set by the CI macOS env) because GitHub has been having issues, making these jobs\n  # fail on git clones: https://github.com/orgs/Homebrew/discussions/4612.\n  - bash: |\n      set -e\n      unset HOMEBREW_NO_INSTALL_FROM_API\n      brew untap homebrew/core homebrew/cask\n      brew update\n      brew install augeas\n    condition: startswith(variables['IMAGE_NAME'], 'macOS')\n    displayName: Install MacOS dependencies\n  - bash: |\n      set -e\n      sudo apt-get update\n      sudo apt-get install -y --no-install-recommends \\\n        libaugeas-dev \\\n        nginx-light\n      sudo systemctl stop nginx\n      sudo sysctl net.ipv4.ip_unprivileged_port_start=0\n    condition: startswith(variables['IMAGE_NAME'], 'ubuntu')\n    displayName: Install Linux dependencies\n  - task: UsePythonVersion@0\n    inputs:\n      versionSpec: $(PYTHON_VERSION)\n      addToPath: true\n  - bash: |\n      set -e\n      python3 tools/pip_install.py tox\n    displayName: Install runtime dependencies\n  - task: DownloadSecureFile@1\n    name: testFarmPem\n    inputs:\n      secureFile: azure-test-farm.pem\n    condition: contains(variables['TOXENV'], 'test-farm')\n  - bash: |\n      set -e\n      export TARGET_BRANCH=\"`echo \"${BUILD_SOURCEBRANCH}\" | sed -E 's!refs/(heads|tags)/!!g'`\"\n      [ -z \"${SYSTEM_PULLREQUEST_TARGETBRANCH}\" ] || export TARGET_BRANCH=\"${SYSTEM_PULLREQUEST_TARGETBRANCH}\"\n      env\n      python3 -m tox run\n    env:\n      AWS_ACCESS_KEY_ID: $(AWS_ACCESS_KEY_ID)\n      AWS_SECRET_ACCESS_KEY: $(AWS_SECRET_ACCESS_KEY)\n      AWS_EC2_PEM_FILE: $(testFarmPem.secureFilePath)\n    displayName: Run tox\n    # For now, let's omit `set -e` and avoid the script exiting with a nonzero\n    # status code to prevent problems here from causing build failures.  If\n    # this turns out to work well, we can change this.\n  - bash: |\n      python3 tools/pip_install.py -I coverage\n      case \"$AGENT_OS\" in\n        Darwin)\n          CODECOV_URL=\"https://uploader.codecov.io/latest/macos/codecov\"\n          ;;\n        Linux)\n          CODECOV_URL=\"https://uploader.codecov.io/latest/linux/codecov\"\n          ;;\n        Windows_NT)\n          CODECOV_URL=\"https://uploader.codecov.io/latest/windows/codecov.exe\"\n          ;;\n        *)\n          echo \"Unexpected OS\"\n          exit 0\n      esac\n      curl --retry 3 -o codecov \"$CODECOV_URL\"\n      chmod +x codecov\n      coverage xml\n      ./codecov || echo \"Uploading coverage data failed\"\n    condition: and(eq(variables['uploadCoverage'], true), or(startsWith(variables['TOXENV'], 'cover'), startsWith(variables['TOXENV'], 'integration')))\n    displayName: Upload coverage data\n"
  },
  {
    "path": ".coveragerc",
    "content": "[run]\nomit = */setup.py\nsource =\n    acme\n    certbot\n    certbot-apache\n    certbot-dns-cloudflare\n    certbot-dns-digitalocean\n    certbot-dns-dnsimple\n    certbot-dns-dnsmadeeasy\n    certbot-dns-gehirn\n    certbot-dns-google\n    certbot-dns-linode\n    certbot-dns-luadns\n    certbot-dns-nsone\n    certbot-dns-ovh\n    certbot-dns-rfc2136\n    certbot-dns-route53\n    certbot-dns-sakuracloud\n    certbot-nginx\n\n[report]\nomit = */setup.py\nshow_missing = True\n"
  },
  {
    "path": ".dockerignore",
    "content": "# this file uses slightly different syntax than .gitignore,\n# e.g. \".tox/\" will not ignore .tox directory\n\n# well, official docker build should be done on clean git checkout\n# anyway, so .tox should be empty... But I'm sure people will try to\n# test docker on their git working directories.\n\n.git\n.tox\nvenv\ndocs\n"
  },
  {
    "path": ".editorconfig",
    "content": "# https://editorconfig.org/\n\nroot = true\n\n[*]\ninsert_final_newline = true\ntrim_trailing_whitespace = true\nend_of_line = lf\n\n[*.py]\nindent_style = space\nindent_size = 4\ncharset = utf-8\nmax_line_length = 100\n\n[*.yaml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "#Default, normalize CRLF into LF in non-binary files\n# Files identified as binary by Git are not changed\n* crlf=auto\n\n# special files\n*.sh crlf=input\n*.py crlf=input\n\n*.bat text eol=crlf\n\n*.der binary\n*.gz binary\n*.jpeg binary\n*.jpg binary\n*.png binary\n*.gz binary\n"
  },
  {
    "path": ".github/CODEOWNERS",
    "content": "* @certbot/eff-devs\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "custom: https://supporters.eff.org/donate/support-work-on-certbot\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yaml",
    "content": "name: Bug Report\ndescription: File a bug report.\ntitle: \"[Bug]: \"\ntype: Bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to fill out this bug report! If you're \n        having trouble using Certbot and aren't sure you've found a bug,\n        please first try asking for help at https://community.letsencrypt.org/.\n        There is a much larger community there of people familiar with the\n        project who will be able to more quickly answer your questions.\n  - type: input\n    attributes:\n      label: OS\n      description: |\n        Describe your Operating System. Examples: Ubuntu 18.04, CentOS 8 Stream\n      placeholder: Ubuntu 24.04\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Installation method\n      description: |\n        How did you install Certbot? Examples: snap, pip, apt, yum\n      placeholder: snap\n    validations:\n      required: true\n  - type: input\n    attributes:\n      label: Certbot Version\n      description: |\n        If you're not sure, you can find this by running `certbot --version`.\n      placeholder: 1.0.0\n    validations:\n      required: true\n  - type: textarea\n    id: what-happened\n    attributes:\n      label: What happened?\n      description: |\n        I ran this command and it produced this output. Example:\n        ```\n        $ sudo certbot certonly -d adfsfasdf.asdfasdf --staging\n        Saving debug log to /var/log/letsencrypt/letsencrypt.log\n        Plugins selected: Authenticator nginx, Installer nginx\n        Requesting a certificate for example.com\n        An unexpected error occurred:\n        Invalid identifiers requested :: Cannot issue for \"adfsfasdf.asdfasdf\": Domain name does not end with a valid public suffix (TLD)\n        Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.\n        ```\n      placeholder: Tell us what you see!\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: Expected behavior\n      description: Certbot's behavior differed from what I expected because.\n      placeholder: \"What was expected?\"\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Relevant log output\n      description: Here is a Certbot log showing the issue (if available). Logs are stored in `/var/log/letsencrypt` by default. Feel free to redact domains, e-mail and IP addresses as you see fit.\n      render: shell\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Let's Encrypt Community Support\n    url: https://community.letsencrypt.org/\n    about: If you're having trouble using Certbot and aren't sure you've found a bug or request for a new feature, please first try asking for help here. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.\n  - name: Certbot Security Policy\n    url: https://github.com/certbot/certbot/security/advisories/new\n    about: Please report security vulnerabilities here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yaml",
    "content": "name: Feature Request\ndescription: Suggest a new feature or improvement to Certbot\ntitle: \"[Feature Request]: \"\ntype: Feature\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: What problem does this feature solve or what does it enhance?\n      description: Explain what this feature addresses, or the benefit it provides.\n      placeholder: For example, \"Currently, users have to manually do X, which is time-consuming.\"\n    validations:\n      required: true\n  - type: textarea\n    id: solution\n    attributes:\n      label: Proposed Solution\n      description: Describe the solution you'd like to see implemented.\n      placeholder: For example, \"Implement a new button that automatically does X.\"\n    validations:\n      required: true\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered\n      description: Have you considered any alternative solutions?\n      placeholder: For example, \"We considered Y, but Z is a better approach because...\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/task.yaml",
    "content": "name: Task\ndescription: A codebase upkeep task such as managing deprecations and refactoring\ntitle: \"[Task]: \"\ntype: Task\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: Task description\n      description: Describe the work that needs to happen, and why.\n      placeholder: >\n        For example, \"In issue [link here], we noted that we cannot update [dependency] until\n        [something happens]. That thing has happened, so now we should update [dependency].\"\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/codecov.yml",
    "content": "# This disables all reporting from codecov. Let's just set it up to collect\n# data for now and then we can play with the settings here.\ncomment: false\ncoverage:\n  status:\n    project: off\n    patch: off\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Pull Request Checklist\n\n- [ ] The Certbot team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.\n- [ ] If you used AI to create this PR, you have done a self-review of all AI-generated code and disclosed that your contribution was AI-generated per [EFF's AI-generated contribution policy](https://www.eff.org/about/opportunities/volunteer/coding-with-eff). You assert you have thoroughly understood, reviewed, and tested your entire submission.\n- [ ] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), add a description of your change to the `newsfragments` directory. This should be a file called `<title>.<type>`, where `<title>` is either a GitHub issue number or some other unique name starting with `+`, and `<type>` is either `changed`, `fixed`, or `added`.\n  * For example, if you fixed a bug for issue number 42, create a file called `42.fixed` and put a description of your change in that file.\n- [ ] Add or update any documentation as needed to support the changes in this PR.\n- [ ] Include your name in `AUTHORS.md` if you like."
  },
  {
    "path": ".github/workflows/assigned.yaml",
    "content": "name: Issue Assigned\n\non:\n  issues:\n    types: [assigned]\npermissions: {}  # let's not use any permissions we don't need here\njobs:\n  send-mattermost-message:\n    runs-on: ubuntu-latest\n    steps:\n    # issue triggers in github actions can be dangerous like\n    # pull_request_target because they run with additional privileges in an\n    # environment containing values that can be controlled by an attacker.\n    # because of this, please take extra caution when modifying the steps taken\n    # by this workflow. for additional information, see\n    # https://github.com/certbot/certbot/pull/10490\n    #\n    # we pin this action to a version tested and audited by certbot's\n    # maintainers for extra security. the full hash is used as doing so is\n    # recommended by zizmor\n    - uses: mattermost/action-mattermost-notify@b7d118e440bf2749cd18a4a8c88e7092e696257a\n      with:\n        MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }}\n        TEXT: >\n            ${{ github.event.assignee.login }} assigned to \"${{ github.event.issue.title }}\": ${{ github.event.issue.html_url }}\n"
  },
  {
    "path": ".github/workflows/merged.yaml",
    "content": "name: Merge Event\n\non:\n  pull_request_target:\n    types:\n      - closed\npermissions: {}  # let's not use any permissions we don't need here\njobs:\n  if_merged:\n    if: github.event.pull_request.merged == true\n    runs-on: ubuntu-latest\n    steps:\n    # github actions workflows triggered by pull_request_target can be\n    # dangerous because they run with additional privileges in an environment\n    # containing values that can be controlled by an attacker. because of\n    # this, please take extra caution when modifying the steps taken by this\n    # workflow. for additional information, see\n    # https://github.com/certbot/certbot/pull/10490\n    #\n    # we pin this action to a version tested and audited by certbot's\n    # maintainers for extra security. the full hash is used as doing so is\n    # recommended by zizmor\n    - uses: mattermost/action-mattermost-notify@b7d118e440bf2749cd18a4a8c88e7092e696257a\n      with:\n        MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_MERGE_WEBHOOK }}\n        TEXT: >\n          [${{ github.repository }}] |\n          [${{ github.event.pull_request.title }}\n          #${{ github.event.number }}](https://github.com/${{ github.repository }}/pull/${{ github.event.number }})\n          was merged into ${{ github.event.pull_request.base.ref }} by ${{ github.actor }}\n"
  },
  {
    "path": ".github/workflows/notify_weekly.yaml",
    "content": "name: Weekly Github Update\n\non:\n  schedule:\n    # Every week on Thursday @ 10:00\n    - cron: \"0 10 * * 4\"\n  workflow_dispatch:\npermissions: {}  # let's not use any permissions we don't need here\njobs:\n  send-mattermost-message:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Create Mattermost Message\n      run: |\n        DATE=$(date --date=\"7 days ago\" +\"%Y-%m-%d\")\n        echo \"ASSIGNED_PRS=https://github.com/pulls?q=is%3Apr+is%3Aopen+updated%3A%3E%3D${DATE}+assignee%3A*+user%3Acertbot\" >> $GITHUB_ENV\n        echo \"UPDATED_URL=https://github.com/issues?q=is%3Aissue+is%3Aopen+sort%3Acomments-desc+updated%3A%3E%3D${DATE}+user%3Acertbot\" >> $GITHUB_ENV\n    # we pin this action to a version tested and audited by certbot's\n    # maintainers for extra security. the full hash is used as doing so is\n    # recommended by zizmor\n    - uses: mattermost/action-mattermost-notify@b7d118e440bf2749cd18a4a8c88e7092e696257a\n      with:\n        MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK_URL }}\n        MATTERMOST_CHANNEL: private-certbot\n        TEXT: |\n          ## Updates In the Past Week\n          - Most commented in the last week: [link](${{ env.UPDATED_URL }})\n          - Updated (assigned) PRs in the last week: [link](${{ env.ASSIGNED_PRS }})\n"
  },
  {
    "path": ".github/workflows/review_requested.yaml",
    "content": "name: Review Requested\n\non:\n  pull_request_target:\n    types: [review_requested]\npermissions: {}\njobs:\n  send-mattermost-message:\n    # Don't notify for the interim step of certbot/eff-devs being assigned\n    if: ${{ github.event.requested_reviewer.login != ''}}\n    runs-on: ubuntu-latest\n    steps:\n    # github actions workflows triggered by pull_request_target can be\n    # dangerous because they run with additional privileges in an environment\n    # containing values that can be controlled by an attacker. because of\n    # this, please take extra caution when modifying the steps taken by this\n    # workflow. for additional information, see\n    # https://github.com/certbot/certbot/pull/10490\n    #\n    # we pin this action to a version tested and audited by certbot's\n    # maintainers for extra security. the full hash is used as doing so is\n    # recommended by zizmor\n    - uses: mattermost/action-mattermost-notify@b7d118e440bf2749cd18a4a8c88e7092e696257a\n      with:\n        MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_ASSIGN_WEBHOOK }}\n        TEXT: >\n            Review requested from ${{ github.event.requested_reviewer.login }} for \"${{ github.event.pull_request.title }}\": ${{ github.event.pull_request.html_url }}\n"
  },
  {
    "path": ".github/workflows/stale.yml",
    "content": "name: Update Stale Issues\non:\n  schedule:\n    # Run 1:24AM every night\n    - cron: '24 1 * * *'\n  workflow_dispatch:\npermissions:\n  issues: write\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/stale@v6\n        with:\n          # Idle number of days before marking issues stale\n          days-before-issue-stale: 365\n\n          # Never mark PRs as stale\n          days-before-pr-stale: -1\n\n          # Idle number of days before closing stale issues\n          days-before-issue-close: 30\n\n          # Never close PRs\n          days-before-pr-close: -1\n\n          # Ignore issues with an assignee\n          exempt-all-issue-assignees: true\n\n          # Label to use when marking as stale\n          stale-issue-label: stale-needs-update\n\n          # Label to use when issue is automatically closed\n          close-issue-label: auto-closed\n\n          stale-issue-message: >\n            We've made a lot of changes to Certbot since this issue was opened. If you\n            still have this issue with an up-to-date version of Certbot, can you please\n            add a comment letting us know? This helps us to better see what issues are\n            still affecting our users. If there is no activity in the next 30 days, this\n            issue will be automatically closed.\n\n          close-issue-message: >\n            This issue has been closed due to lack of activity, but if you think it\n            should be reopened, please open a new issue with a link to this one and we'll\n            take a look.\n\n          # Limit the number of actions per run. As of writing this, GitHub's\n          # rate limit is 1000 requests per hour so we're still a ways off. See\n          # https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limits-for-requests-from-github-actions.\n          operations-per-run: 180\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.egg-info/\n.eggs/\nbuild/\ndist*/\n/venv*/\n/.tox/\n/releases*/\n/log*\nletsencrypt.log\ncertbot.log\npoetry.lock\n\n# coverage\n.coverage\n.coverage.*\n/htmlcov/\n\n/.vagrant\n\ntags\n\n# editor temporary files\n*~\n*.sw?\n\\#*#\n.idea\n.ropeproject\n.vscode\n*.sublime-project\n*.sublime-workspace\n\n# auth --cert-path --chain-path\n/*.pem\n\n.venv\n\n# pytest cache\n.cache\n.mypy_cache/\n.pytest_cache/\n\n# docker files\n.docker\n\n# certbot tests\n.certbot_test_workspace\n**/assets/pebble*\n**/assets/challtestsrv*\n\n# snap files\n.snapcraft\nparts\nprime\nstage\n*.snap\nsnap-constraints.txt\nqemu-*\ncertbot-dns*/certbot-dns*_amd64*.txt\ncertbot-dns*/certbot-dns*_arm*.txt\n/certbot_amd64*.txt\n/certbot_arm*.txt\ncertbot-dns*/snap\nsnapcraft.cfg\n\n# pyenv files\n.python-version\n\n# macOS files\n.DS_Store\n"
  },
  {
    "path": ".isort.cfg",
    "content": "[settings]\nskip_glob=venv*\nforce_sort_within_sections=True\nforce_single_line=True\norder_by_type=False\nline_length=400\nsrc_paths=acme/src,acme/tests,certbot*/tests,certbot/src,certbot*/src/certbot*\n"
  },
  {
    "path": ".pylintrc",
    "content": "[MAIN]\n\n# Analyse import fallback blocks. This can be used to support both Python 2 and\n# 3 compatible code, which means that the block might have code that exists\n# only in one or another interpreter, leading to false positives when analysed.\nanalyse-fallback-blocks=no\n\n# Load and enable all available extensions. Use --list-extensions to see a list\n# all available extensions.\n#enable-all-extensions=\n\n# In error mode, messages with a category besides ERROR or FATAL are\n# suppressed, and no reports are done by default. Error mode is compatible with\n# disabling specific errors.\n#errors-only=\n\n# Always return a 0 (non-error) status code, even if lint errors are found.\n# This is primarily useful in continuous integration scripts.\n#exit-zero=\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code.\nextension-pkg-allow-list=\n\n# A comma-separated list of package or module names from where C extensions may\n# be loaded. Extensions are loading into the active Python interpreter and may\n# run arbitrary code. (This is an alternative name to extension-pkg-allow-list\n# for backward compatibility.)\nextension-pkg-whitelist=pywintypes,win32api,win32file,win32security\n\n# Return non-zero exit code if any of these messages/categories are detected,\n# even if score is above --fail-under value. Syntax same as enable. Messages\n# specified are enabled, while categories only check already-enabled messages.\nfail-on=\n\n# Specify a score threshold under which the program will exit with error.\nfail-under=10\n\n# Interpret the stdin as a python script, whose filename needs to be passed as\n# the module_or_package argument.\n#from-stdin=\n\n# Files or directories to be skipped. They should be base names, not paths.\nignore=CVS\n\n# Add files or directories matching the regular expressions patterns to the\n# ignore-list. The regex matches against paths and can be in Posix or Windows\n# format. Because '\\' represents the directory delimiter on Windows systems, it\n# can't be used as an escape character.\n# CERTBOT COMMENT\n# Changing this line back to the default of `ignore-paths=` is being tracked by\n# https://github.com/certbot/certbot/issues/7908.\nignore-paths=.*/_internal/tests/\n\n# Files or directories matching the regular expression patterns are skipped.\n# The regex matches against base names, not paths. The default value ignores\n# Emacs file locks\nignore-patterns=^\\.#\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis). It\n# supports qualified module names, as well as Unix pattern matching.\nignored-modules=\n\n# Python code to execute, usually for sys.path manipulation such as\n# pygtk.require().\n# CERTBOT COMMENT\n# This is needed for pylint to import linter_plugin.py since\n# https://github.com/PyCQA/pylint/pull/3396.\ninit-hook=\"import pylint.config, os, sys; sys.path.append(os.path.dirname(next(pylint.config.find_default_config_files())))\"\n\n# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the\n# number of processors available to use, and will cap the count on Windows to\n# avoid hangs.\njobs=0\n\n# Control the amount of potential inferred values when inferring a single\n# object. This can help the performance when dealing with large functions or\n# complex, nested conditions.\nlimit-inference-results=100\n\n# List of plugins (as comma separated values of python module names) to load,\n# usually to register additional checkers.\nload-plugins=linter_plugin\n\n# Pickle collected data for later comparisons.\npersistent=yes\n\n# Minimum Python version to use for version dependent checks. Will default to\n# the version used to run pylint.\npy-version=3.10\n\n# Discover python modules and packages in the file system subtree.\nrecursive=no\n\n# When enabled, pylint would attempt to guess common misconfiguration and emit\n# user-friendly hints instead of false-positive error messages.\nsuggestion-mode=yes\n\n# Allow loading of arbitrary C extensions. Extensions are imported into the\n# active Python interpreter and may run arbitrary code.\nunsafe-load-any-extension=no\n\n# In verbose mode, extra non-checker-related info will be displayed.\n#verbose=\n\n\n[BASIC]\n\n# Naming style matching correct argument names.\nargument-naming-style=snake_case\n\n# Regular expression matching correct argument names. Overrides argument-\n# naming-style. If left empty, argument names will be checked with the set\n# naming style.\n#argument-rgx=\n\n# Naming style matching correct attribute names.\nattr-naming-style=snake_case\n\n# Regular expression matching correct attribute names. Overrides attr-naming-\n# style. If left empty, attribute names will be checked with the set naming\n# style.\n#attr-rgx=\n\n# Bad variable names which should always be refused, separated by a comma.\nbad-names=foo,\n          bar,\n          baz,\n          toto,\n          tutu,\n          tata\n\n# Bad variable names regexes, separated by a comma. If names match any regex,\n# they will always be refused\nbad-names-rgxs=\n\n# Naming style matching correct class attribute names.\nclass-attribute-naming-style=any\n\n# Regular expression matching correct class attribute names. Overrides class-\n# attribute-naming-style. If left empty, class attribute names will be checked\n# with the set naming style.\n#class-attribute-rgx=\n\n# Naming style matching correct class constant names.\nclass-const-naming-style=UPPER_CASE\n\n# Regular expression matching correct class constant names. Overrides class-\n# const-naming-style. If left empty, class constant names will be checked with\n# the set naming style.\n#class-const-rgx=\n\n# Naming style matching correct class names.\nclass-naming-style=PascalCase\n\n# Regular expression matching correct class names. Overrides class-naming-\n# style. If left empty, class names will be checked with the set naming style.\n#class-rgx=\n\n# Naming style matching correct constant names.\nconst-naming-style=UPPER_CASE\n\n# Regular expression matching correct constant names. Overrides const-naming-\n# style. If left empty, constant names will be checked with the set naming\n# style.\n#const-rgx=\n\n# Minimum line length for functions/classes that require docstrings, shorter\n# ones are exempt.\ndocstring-min-length=-1\n\n# Naming style matching correct function names.\nfunction-naming-style=snake_case\n\n# Regular expression matching correct function names. Overrides function-\n# naming-style. If left empty, function names will be checked with the set\n# naming style.\nfunction-rgx=[a-z_][a-z0-9_]{2,40}$\n\n# Good variable names which should always be accepted, separated by a comma.\ngood-names=i,\n           j,\n           k,\n           ex,\n           Run,\n           _,\n           fd,\n           logger\n\n# Good variable names regexes, separated by a comma. If names match any regex,\n# they will always be accepted\ngood-names-rgxs=\n\n# Include a hint for the correct naming format with invalid-name.\ninclude-naming-hint=no\n\n# Naming style matching correct inline iteration names.\ninlinevar-naming-style=any\n\n# Regular expression matching correct inline iteration names. Overrides\n# inlinevar-naming-style. If left empty, inline iteration names will be checked\n# with the set naming style.\n#inlinevar-rgx=\n\n# Naming style matching correct method names.\nmethod-naming-style=snake_case\n\n# Regular expression matching correct method names. Overrides method-naming-\n# style. If left empty, method names will be checked with the set naming style.\nmethod-rgx=[a-z_][a-z0-9_]{2,50}$\n\n# Naming style matching correct module names.\nmodule-naming-style=snake_case\n\n# Regular expression matching correct module names. Overrides module-naming-\n# style. If left empty, module names will be checked with the set naming style.\n#module-rgx=\n\n# Colon-delimited sets of names that determine each other's naming style when\n# the name regexes allow several styles.\nname-group=\n\n# Regular expression which should only match function or class names that do\n# not require a docstring.\nno-docstring-rgx=(__.*__)|(test_[A-Za-z0-9_]*)|(_.*)|(.*Test$)\n\n# List of decorators that produce properties, such as abc.abstractproperty. Add\n# to this list to register other decorators that produce valid properties.\n# These decorators are taken in consideration only for invalid-name.\nproperty-classes=abc.abstractproperty\n\n# Regular expression matching correct type variable names. If left empty, type\n# variable names will be checked with the set naming style.\n#typevar-rgx=\n\n# Naming style matching correct variable names.\nvariable-naming-style=snake_case\n\n# Regular expression matching correct variable names. Overrides variable-\n# naming-style. If left empty, variable names will be checked with the set\n# naming style.\nvariable-rgx=[a-z_][a-z0-9_]{1,30}$\n\n\n[CLASSES]\n\n# Warn about protected attribute access inside special methods\ncheck-protected-access-in-special-methods=no\n\n# List of method names used to declare (i.e. assign) instance attributes.\ndefining-attr-methods=__init__,\n                      __new__,\n                      setUp,\n                      __post_init__\n\n# List of valid names for the first argument in a class method.\nvalid-classmethod-first-arg=cls\n\n# List of valid names for the first argument in a metaclass class method.\nvalid-metaclass-classmethod-first-arg=cls\n\n\n[EXCEPTIONS]\n\n# Exceptions that will emit a warning when caught.\novergeneral-exceptions=builtins.BaseException,\n                       builtins.Exception\n\n\n[FORMAT]\n\n# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.\nexpected-line-ending-format=\n\n# Regexp for a line that is allowed to be longer than the limit.\nignore-long-lines=^\\s*(# )?<?https?://\\S+>?$\n\n# Number of spaces of indent required inside a hanging or continued line.\n# git history told me that \"This does something silly/broken...\"\n#indent-after-paren=4\n\n# String used as indentation unit. This is usually \"    \" (4 spaces) or \"\\t\" (1\n# tab).\nindent-string='    '\n\n# Maximum number of characters on a single line.\nmax-line-length=100\n\n# Maximum number of lines in a module.\nmax-module-lines=1250\n\n# Allow the body of a class to be on the same line as the declaration if body\n# contains single statement.\nsingle-line-class-stmt=no\n\n# Allow the body of an if to be on the same line as the test if there is no\n# else.\nsingle-line-if-stmt=no\n\n\n[IMPORTS]\n\n# List of modules that can be imported at any level, not just the top level\n# one.\nallow-any-import-level=\n\n# Allow wildcard imports from modules that define __all__.\nallow-wildcard-with-all=no\n\n# Deprecated modules which should not be used, separated by a comma.\ndeprecated-modules=\n\n# Output a graph (.gv or any supported image format) of external dependencies\n# to the given file (report RP0402 must not be disabled).\next-import-graph=\n\n# Output a graph (.gv or any supported image format) of all (i.e. internal and\n# external) dependencies to the given file (report RP0402 must not be\n# disabled).\nimport-graph=\n\n# Output a graph (.gv or any supported image format) of internal dependencies\n# to the given file (report RP0402 must not be disabled).\nint-import-graph=\n\n# Force import order to recognize a module as part of the standard\n# compatibility libraries.\nknown-standard-library=\n\n# Force import order to recognize a module as part of a third party library.\nknown-third-party=enchant\n\n# Couples of modules and preferred modules, separated by a comma.\npreferred-modules=\n\n\n[LOGGING]\n\n# The type of string formatting that logging methods do. `old` means using %\n# formatting, `new` is for `{}` formatting.\nlogging-format-style=old\n\n# Logging modules to check that the string format arguments are in logging\n# function parameter format.\nlogging-modules=logging,logger\n\n\n[MESSAGES CONTROL]\n\n# Only show warnings with the listed confidence levels. Leave empty to show\n# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,\n# UNDEFINED.\nconfidence=HIGH,\n           CONTROL_FLOW,\n           INFERENCE,\n           INFERENCE_FAILURE,\n           UNDEFINED\n\n# Disable the message, report, category or checker with the given id(s). You\n# can either give multiple identifiers separated by comma (,) or put this\n# option multiple times (only on the command line, not in the configuration\n# file where it should appear only once). You can also use \"--disable=all\" to\n# disable everything first and then re-enable specific checks. For example, if\n# you want to run only the similarities checker, you can use \"--disable=all\n# --enable=similarities\". If you want to run only the classes checker, but have\n# no Warning level messages displayed, use \"--disable=all --enable=classes\n# --disable=W\".\n# CERTBOT COMMENT\n# 1) Once certbot codebase is claimed to be compatible exclusively with Python 3,\n#    the useless-object-inheritance check can be enabled again, and code fixed accordingly.\n# 2) Check unsubscriptable-object tends to create a lot of false positives. Let's disable it.\n#    See https://github.com/PyCQA/pylint/issues/1498.\n# 3) Same as point 2 for no-value-for-parameter.\n#    See https://github.com/PyCQA/pylint/issues/2820.\n# 4) raise-missing-from makes it an error to raise an exception from except\n#    block without using explicit exception chaining. While explicit exception\n#    chaining results in a slightly more informative traceback, I don't think\n#    it's beneficial enough for us to change all of our current instances and\n#    give Certbot developers errors about this when they're working on new code\n#    in the future. You can read more about exception chaining and this pylint\n#    check at\n#    https://blog.ram.rachum.com/post/621791438475296768/improving-python-exception-chaining-with.\n# 5) wrong-import-order generates false positives and a pylint developer\n#    suggests that people using isort should disable this check at\n#    https://github.com/PyCQA/pylint/issues/3817#issuecomment-687892090.\n# 6) unspecified-encoding generates errors when encoding is not specified in\n#    in a call to the built-in open function. This relates more to a design decision\n#    (unspecified encoding makes the open function use the default encoding of the system)\n#    than a clear flaw on which a check should be enforced. Anyway the project does\n#    not need to enforce encoding on files so we disable this check.\n# 7) consider-using-f-string is \"suggesting\" to move to f-string when possible with an error. This\n#    clearly relates to code design and not to potential defects in the code, let's just ignore that.\ndisable=fixme,locally-disabled,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,unspecified-encoding,consider-using-f-string,raw-checker-failed,bad-inline-option,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,use-symbolic-message-instead\n\n# Enable the message, report, category or checker with the given id(s). You can\n# either give multiple identifier separated by comma (,) or put this option\n# multiple time (only on the command line, not in the configuration file where\n# it should appear only once). See also the \"--disable\" option for examples.\nenable=c-extension-no-member\n\n\n[METHOD_ARGS]\n\n# List of qualified names (i.e., library.method) which require a timeout\n# parameter e.g. 'requests.api.get,requests.api.post'\ntimeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request\n\n\n[MISCELLANEOUS]\n\n# List of note tags to take in consideration, separated by a comma.\nnotes=FIXME,\n      XXX,\n      TODO\n\n# Regular expression of note tags to take in consideration.\nnotes-rgx=\n\n\n[REFACTORING]\n\n# Maximum number of nested blocks for function / method body\nmax-nested-blocks=5\n\n# Complete name of functions that never returns. When checking for\n# inconsistent-return-statements if a never returning function is called then\n# it will be considered as an explicit return statement and no message will be\n# printed.\nnever-returning-functions=sys.exit,argparse.parse_error\n\n\n[REPORTS]\n\n# Python expression which should return a score less than or equal to 10. You\n# have access to the variables 'fatal', 'error', 'warning', 'refactor',\n# 'convention', and 'info' which contain the number of messages in each\n# category, as well as 'statement' which is the total number of statements\n# analyzed. This score is used by the global evaluation report (RP0004).\nevaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))\n\n# Template used to display messages. This is a python new-style format string\n# used to format the message information. See doc for all details.\nmsg-template=\n\n# Set the output format. Available formats are text, parseable, colorized, json\n# and msvs (visual studio). You can also give a reporter class, e.g.\n# mypackage.mymodule.MyReporterClass.\n#output-format=\n\n# Tells whether to display a full report or only the messages.\nreports=no\n\n# Activate the evaluation score.\nscore=yes\n\n\n[SIMILARITIES]\n\n# Comments are removed from the similarity computation\nignore-comments=yes\n\n# Docstrings are removed from the similarity computation\nignore-docstrings=yes\n\n# Imports are removed from the similarity computation\nignore-imports=yes\n\n# Signatures are removed from the similarity computation\nignore-signatures=yes\n\n# Minimum lines number of a similarity.\nmin-similarity-lines=6\n\n\n[STRING]\n\n# This flag controls whether inconsistent-quotes generates a warning when the\n# character used as a quote delimiter is used inconsistently within a module.\ncheck-quote-consistency=no\n\n# This flag controls whether the implicit-str-concat should generate a warning\n# on implicit string concatenation in sequences defined over several lines.\ncheck-str-concat-over-line-jumps=no\n\n\n[TYPECHECK]\n\n# List of decorators that produce context managers, such as\n# contextlib.contextmanager. Add to this list to register other decorators that\n# produce valid context managers.\ncontextmanager-decorators=contextlib.contextmanager\n\n# List of members which are set dynamically and missed by pylint inference\n# system, and so shouldn't trigger E1101 when accessed. Python regular\n# expressions are accepted.\ngenerated-members=\n\n# Tells whether to warn about missing members when the owner of the attribute\n# is inferred to be None.\nignore-none=yes\n\n# This flag controls whether pylint should warn about no-member and similar\n# checks whenever an opaque object is returned when inferring. The inference\n# can return multiple potential results while evaluating a Python object, but\n# some branches might not be evaluated, which results in partial inference. In\n# that case, it might be useful to still emit no-member and other checks for\n# the rest of the inferred objects.\nignore-on-opaque-inference=yes\n\n# List of symbolic message names to ignore for Mixin members.\nignored-checks-for-mixins=no-member,\n                          not-async-context-manager,\n                          not-context-manager,\n                          attribute-defined-outside-init\n\n# List of class names for which member attributes should not be checked (useful\n# for classes with dynamically set attributes). This supports the use of\n# qualified names.\nignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace,Field,Header,JWS,closing\n\n# List of module names for which member attributes should not be checked\n# (useful for modules/projects where namespaces are manipulated during runtime\n# and thus existing member attributes cannot be deduced by static analysis\nignored-modules=confargparse,argparse\n\n# Show a hint with possible names when a member name was not found. The aspect\n# of finding the hint is based on edit distance.\nmissing-member-hint=yes\n\n# The minimum edit distance a name should have in order to be considered a\n# similar match for a missing member name.\nmissing-member-hint-distance=1\n\n# The total number of similar names that should be taken in consideration when\n# showing a hint for a missing member.\nmissing-member-max-choices=1\n\n# Regex pattern to define which classes are considered mixins.\nmixin-class-rgx=.*[Mm]ixin\n\n# List of decorators that change the signature of a decorated function.\nsignature-mutators=\n\n\n[VARIABLES]\n\n# List of additional names supposed to be defined in builtins. Remember that\n# you should avoid defining new builtins when possible.\nadditional-builtins=\n\n# Tells whether unused global variables should be treated as a violation.\nallow-global-unused-variables=yes\n\n# List of names allowed to shadow builtins\nallowed-redefined-builtins=\n\n# List of strings which can identify a callback function by name. A callback\n# name must start or end with one of those strings.\ncallbacks=cb_,\n          _cb\n\n# A regular expression matching the name of dummy variables (i.e. expected to\n# not be used).\ndummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_\n\n# Argument names that match this expression will be ignored.\nignored-argument-names=_.*|^ignored_|^unused_\n\n# Tells whether we should check for unused import in __init__ files.\ninit-import=no\n\n# List of qualified module names which can have objects that can redefine\n# builtins.\nredefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io\n"
  },
  {
    "path": "AUTHORS.md",
    "content": "Authors\n=======\n\n* [Aaron Gable](https://github.com/aarongable)\n* [Aaron Zirbes](https://github.com/aaronzirbes)\n* Aaron Zuehlke\n* Ada Lovelace\n* [Adam Woodbeck](https://github.com/awoodbeck)\n* [Adrien Ferrand](https://github.com/adferrand)\n* [Aidin Gharibnavaz](https://github.com/aidin36)\n* [AJ ONeal](https://github.com/coolaj86)\n* [Alcaro](https://github.com/Alcaro)\n* [Alexander Mankuta](https://github.com/pointlessone)\n* [Alex Bowers](https://github.com/alexbowers)\n* [Alex Conlin](https://github.com/alexconlin)\n* [Alex Gaynor](https://github.com/alex)\n* [Alex Halderman](https://github.com/jhalderm)\n* [Alex Jordan](https://github.com/strugee)\n* [Alex Zorin](https://github.com/alexzorin)\n* [Alexis Hancock](https://github.com/zoracon)\n* [Amir Omidi](https://github.com/aaomidi)\n* [Amjad Mashaal](https://github.com/TheNavigat)\n* [amplifi](https://github.com/amplifi)\n* [Andrew Murray](https://github.com/radarhere)\n* [Andrzej Górski](https://github.com/andrzej3393)\n* [Anna Glasgall](https://github.com/aglasgall)\n* [Anselm Levskaya](https://github.com/levskaya)\n* [Antoine Jacoutot](https://github.com/ajacoutot)\n* [April King](https://github.com/april)\n* [asaph](https://github.com/asaph)\n* [Axel Beckert](https://github.com/xtaran)\n* [Bas](https://github.com/Mechazawa)\n* [benbankes](https://github.com/benbankes)\n* [Ben Irving](https://github.com/benileo)\n* [Benjamin Kerensa](https://github.com/bkerensa)\n* [Benjamin Neff](https://github.com/SuperTux88)\n* [Benjamin Piouffle](https://github.com/Betree)\n* [Ben Ubois](https://github.com/benubois)\n* [Ben Wolfe](https://github.com/bwolfe)\n* [Bigfish](https://github.com/bwolfe)\n* [Blake Griffith](https://github.com/cowlicks)\n* [Brad Warren](https://github.com/bmw)\n* [Brandon Kraft](https://github.com/kraftbj)\n* [Brandon Kreisel](https://github.com/BKreisel)\n* [Brian Heim](https://github.com/brianlheim)\n* [Cameron Steel](https://github.com/Tugzrida)\n* [Ceesjan Luiten](https://github.com/quinox)\n* [Chad Whitacre](https://github.com/whit537)\n* [Chhatoi Pritam Baral](https://github.com/pritambaral)\n* [Chris Johns](https://github.com/ter0)\n* [Chris Lamb](https://github.com/lamby)\n* [chrismarget](https://github.com/chrismarget)\n* [Christian Gärtner](https://github.com/ChristianGaertner)\n* [Christian Rosentreter](https://github.com/the-real-tokai)\n* [Christopher Brown](https://github.com/chbrown)\n* [Christopher Manning](https://github.com/christophermanning)\n* [Christoph Kisfeld](https://github.com/chk1)\n* [Clif Houck](https://github.com/ClifHouck)\n* [Cooper Quintin](https://github.com/cooperq)\n* [Corey Farwell](https://github.com/frewsxcv)\n* [Craig Smith](https://github.com/dashaxiong)\n* [Damian Poddebniak](https://github.com/duesee)\n* [Damien Nozay](https://github.com/dnozay)\n* [Damien Tournoud](https://github.com/damz)\n* [DanCld](https://github.com/DanCld)\n* [Daniel Albers](https://github.com/AID)\n* [Daniel Aleksandersen](https://github.com/da2x)\n* [Daniel Almasi](https://github.com/almasen)\n* [Daniel Convissor](https://github.com/convissor)\n* [Daniel \"Drex\" Drexler](https://github.com/aeturnum)\n* [Daniel Huang](https://github.com/dhuang)\n* [Daniel McMahon] (https://github.com/igloodan)\n* [Dave Guarino](https://github.com/daguar)\n* [David cz](https://github.com/dave-cz)\n* [David Dworken](https://github.com/ddworken)\n* [David Kreitschmann](https://kreitschmann.de)\n* [David Xia](https://github.com/davidxia)\n* [Devin Howard](https://github.com/devvmh)\n* [dokazaki](https://github.com/dokazaki)\n* [Dominic Cleal](https://github.com/domcleal)\n* [Dominic Lüchinger](https://github.com/dol)\n* [Douglas José](https://github.com/douglasjose)\n* [Erica Portnoy](https://github.com/ohemorange)\n* [Eric Engestrom](https://github.com/1ace)\n* [Eric Rescorla](https://github.com/ekr)\n* [Eric Wustrow](https://github.com/ewust)\n* [Erik Rose](https://github.com/erikrose)\n* [Eugene Kazakov](https://github.com/xgin)\n* [Fabian](https://github.com/faerbit)\n* [Faidon Liambotis](https://github.com/paravoid)\n* [Fan Jiang](https://github.com/tcz001)\n* [Felix Lechner](https://github.com/lechner)\n* [Felix Schwarz](https://github.com/FelixSchwarz)\n* [Felix Yan](https://github.com/felixonmars)\n* [Filip Ochnik](https://github.com/filipochnik)\n* [Florian Klink](https://github.com/flokli)\n* [Francesco Colista](https://github.com/fcolista)\n* [Francois Marier](https://github.com/fmarier)\n* [Frank](https://github.com/Frankkkkk)\n* [Frederic BLANC](https://github.com/fblanc)\n* [Garrett Robinson](https://github.com/garrettr)\n* [Gene Wood](https://github.com/gene1wood)\n* [Geoffroy Doucet](https://www.geoffroydoucet.com)\n* [Gian Carlo Pace](https://github.com/gicappa)\n* [Gilles Pietri](https://github.com/gilou)\n* [Giovanni Pellerano](https://github.com/evilaliv3)\n* [Giovanni Toraldo](https://github.com/gionn)\n* [Gordin](https://github.com/Gordin)\n* [Gregor Dschung](https://github.com/chkpnt)\n* [Gregory L. Dietsche](https://github.com/farmergreg)\n* [Greg Osuri](https://github.com/gosuri)\n* [Guillaume Boudreau](https://github.com/gboudreau)\n* [Harlan Lieberman-Berg](https://github.com/hlieberman)\n* [Henri Salo](https://github.com/fgeek)\n* [Henry Chen](https://github.com/henrychen95)\n* [Hugo van Kemenade](https://github.com/hugovk)\n* [Ingolf Becker](https://github.com/watercrossing)\n* [Ivan Nejgebauer](https://github.com/inejge)\n* [Jaap Eldering](https://github.com/eldering)\n* [Jacob Hoffman-Andrews](https://github.com/jsha)\n* [Jacob Sachs](https://github.com/jsachs)\n* [Jairo Llopis](https://github.com/Yajo)\n* [Jakub Warmuz](https://github.com/kuba)\n* [James Balazs](https://github.com/jamesbalazs)\n* [James Kasten](https://github.com/jdkasten)\n* [Jason Grinblat](https://github.com/ptychomancer)\n* [Jason Owen](https://github.com/jasonaowen)\n* [Jawshua](https://github.com/jawshua)\n* [Jay Faulkner](https://github.com/jayofdoom)\n* [J.C. Jones](https://github.com/jcjones)\n* [Jeff Hodges](https://github.com/jmhodges)\n* [Jeremy Gillula](https://github.com/jgillula)\n* [Jeroen Ketelaar](https://github.com/JKetelaar)\n* [Jeroen Pluimers](https://github.com/jpluimers)\n* [j](https://github.com/bit)\n* [Jim Tittsler](https://github.com/jimt)\n* [Joe Ranweiler](https://github.com/ranweiler)\n* [Joerg Sonnenberger](https://github.com/jsonn)\n* [John Leach](https://github.com/johnl)\n* [John Reed](https://github.com/leerspace)\n* [Jonas Berlin](https://github.com/xkr47)\n* [Jonathan Herlin](https://github.com/Jonher937)\n* [Jonathan Vanasco](https://github.com/jvanasco)\n* [Jon Walsh](https://github.com/code-tree)\n* [Joona Hoikkala](https://github.com/joohoi)\n* [Josh McCullough](https://github.com/JoshMcCullough)\n* [Josh Soref](https://github.com/jsoref)\n* [Joubin Jabbari](https://github.com/joubin)\n* [Juho Juopperi](https://github.com/jkjuopperi)\n* [Kane York](https://github.com/riking)\n* [Katsuyoshi Ozaki](https://github.com/moratori)\n* [Kenichi Maehashi](https://github.com/kmaehashi)\n* [Kenneth Skovhede](https://github.com/kenkendk)\n* [Kevin Burke](https://github.com/kevinburke)\n* [Kevin London](https://github.com/kevinlondon)\n* [Kubilay Kocak](https://github.com/koobs)\n* [LeCoyote](https://github.com/LeCoyote)\n* [Lee Watson](https://github.com/TheReverend403)\n* [Leo Famulari](https://github.com/lfam)\n* [Leon G](https://github.com/LeonGr)\n* [lf](https://github.com/lf-)\n* [Liam Marshall](https://github.com/liamim)\n* [Lior Sabag](https://github.com/liorsbg)\n* [Lipis](https://github.com/lipis)\n* [lord63](https://github.com/lord63)\n* [Lorenzo Fundaró](https://github.com/lfundaro)\n* [Luca Beltrame](https://github.com/lbeltrame)\n* [Luca Ebach](https://github.com/lucebac)\n* [Luca Olivetti](https://github.com/olivluca)\n* [Luke Rogers](https://github.com/lukeroge)\n* [Lukhnos Liu](https://github.com/lukhnos)\n* [Maarten](https://github.com/mrtndwrd)\n* [Mads Jensen](https://github.com/atombrella)\n* [Maikel Martens](https://github.com/krukas)\n* [Malte Janduda](https://github.com/MalteJ)\n* [Mantas Mikulėnas](https://github.com/grawity)\n* [Marcel Krüger](https://github.com/zauguin)\n* [Marcos Caceres](https://github.com/marcoscaceres)\n* [Marek Viger](https://github.com/freezy-sk)\n* [Mario Villaplana](https://github.com/supermari0)\n* [Marius Gedminas](https://github.com/mgedmin)\n* [Martey Dodoo](https://github.com/martey)\n* [Martijn Bastiaan](https://github.com/martijnbastiaan)\n* [Martijn Braam](https://github.com/MartijnBraam)\n* [Martin Brugger](https://github.com/mbrugger)\n* [Mathieu Leduc-Hamel](https://github.com/mlhamel)\n* [Matt Bostock](https://github.com/mattbostock)\n* [Matthew Ames](https://github.com/SuperMatt)\n* [Matthew W. Thomas](https://github.com/mwt)\n* [Michael Schumacher](https://github.com/schumaml)\n* [Michael Strache](https://github.com/Jarodiv)\n* [Michael Sverdlin](https://github.com/sveder)\n* [Michael Watters](https://github.com/blackknight36)\n* [Michal Moravec](https://github.com/https://github.com/Majkl578)\n* [Michal Papis](https://github.com/mpapis)\n* [Mickaël Schoentgen](https://github.com/BoboTiG)\n* [Minn Soe](https://github.com/MinnSoe)\n* [Min RK](https://github.com/minrk)\n* [Miquel Ruiz](https://github.com/miquelruiz)\n* [Môshe van der Sterre](https://github.com/moshevds)\n* [mrstanwell](https://github.com/mrstanwell)\n* [Nav Aulakh](https://github.com/Nav)\n* [Nelson Elhage](https://github.com/nelhage)\n* [Nick Fong](https://github.com/nickfong)\n* [Nick Le Mouton](https://github.com/NoodlesNZ)\n* [Nikos Roussos](https://github.com/comzeradd)\n* [Noah Swartz](https://github.com/swartzcr)\n* [Ola Bini](https://github.com/olabini)\n* [Ondřej Súkup](https://github.com/mimi1vx)\n* [Ondřej Surý](https://github.com/oerdnj)\n* [osirisinferi](https://github.com/osirisinferi)\n* Patrick Figel\n* [Patrick Heppler](https://github.com/PatrickHeppler)\n* [Paul Buonopane](https://github.com/Zenexer)\n* [Paul Feitzinger](https://github.com/pfeyz)\n* [Paulo Dias](https://github.com/paulojmdias)\n* [Pavan Gupta](https://github.com/pavgup)\n* [Pavel Pavlov](https://github.com/ghost355)\n* [Peter Conrad](https://github.com/pconrad-fb)\n* [Peter Eckersley](https://github.com/pde)\n* [Peter Mosmans](https://github.com/PeterMosmans)\n* [Phil Martin](https://github.com/frillip)\n* [Philippe Langlois](https://github.com/langloisjp)\n* [Philipp Spitzer](https://github.com/spitza)\n* [Piero Steinger](https://github.com/Jadaw1n)\n* [Pierre Jaury](https://github.com/kaiyou)\n* [Piotr Kasprzyk](https://github.com/kwadrat)\n* [Prayag Verma](https://github.com/pra85)\n* [Preston Locke](https://github.com/Preston12321)\n* [Q Misell][https://magicalcodewit.ch]\n* [Rasesh Patel](https://github.com/raspat1)\n* [Reinaldo de Souza Jr](https://github.com/juniorz)\n* [Remi Rampin](https://github.com/remram44)\n* [Rémy HUBSCHER](https://github.com/Natim)\n* [Rémy Léone](https://github.com/sieben)\n* [Richard Barnes](https://github.com/r-barnes)\n* [Richard Harman](https://github.com/warewolf)\n* [Richard Panek](https://github.com/kernelpanek)\n* [Robert Buchholz](https://github.com/rbu)\n* [Robert Dailey](https://github.com/pahrohfit)\n* [Robert Habermann](https://github.com/frennkie)\n* [Robert Xiao](https://github.com/nneonneo)\n* [Roland Shoemaker](https://github.com/rolandshoemaker)\n* [Roy Wellington Ⅳ](https://github.com/thanatos)\n* [rugk](https://github.com/rugk)\n* [seeg](https://github.com/s33g)\n* [Sachi King](https://github.com/nakato)\n* [Sagi Kedmi](https://github.com/sagi)\n* [Sam Lanning](https://github.com/samlanning)\n* [sapics](https://github.com/sapics)\n* [SATOH Fumiyasu](https://github.com/fumiyas)\n* [Scott Barr](https://github.com/scottjbarr)\n* [Scott Merrill](https://github.com/skpy)\n* [Sebastian Bögl](https://github.com/TheBoegl)\n* [Sebastian Wagner](https://github.com/sebix)\n* [sedrubal](https://github.com/sedrubal)\n* [Seppe Stas](https://github.com/seppestas)\n* [Sergey Nuzdhin](https://github.com/lwolf)\n* [Seth Schoen](https://github.com/schoen)\n* [Sharif Nassar](https://github.com/mrwacky42)\n* [Shaun Cummiskey](https://github.com/ampersign)\n* [Shiloh Heurich](https://github.com/sheurich)\n* [silverwind](https://github.com/silverwind)\n* [Sorvani](https://github.com/sorvani)\n* [Spencer Bliven](https://github.com/sbliven)\n* [Stacey Sheldon](https://github.com/solidgoldbomb)\n* [Stavros Korokithakis](https://github.com/skorokithakis)\n* [Ștefan Talpalaru](https://github.com/stefantalpalaru)\n* [Stefan Weil](https://github.com/stweil)\n* [Steve Desmond](https://github.com/stevedesmond-ca)\n* [sydneyli](https://github.com/sydneyli)\n* [taixx046](https://github.com/taixx046)\n* [Tan Jay Jun](https://github.com/jayjun)\n* [Tapple Gao](https://github.com/tapple)\n* [Telepenin Nikolay](https://github.com/telepenin)\n* [Thomas Cottier](https://github.com/tcottier-enalean)\n* [Thomas Mayer](https://github.com/thomaszbz)\n* [Thomas Waldmann](https://github.com/ThomasWaldmann)\n* [Thom Wiggers](https://github.com/thomwiggers)\n* [Till Maas](https://github.com/tyll)\n* [Timothy Guan-tin Chien](https://github.com/timdream)\n* [Torsten Bögershausen](https://github.com/tboegi)\n* [Travis Raines](https://github.com/rainest)\n* [Trung Ngo](https://github.com/Ngo-The-Trung)\n* [Valentin](https://github.com/e00E)\n* [venyii](https://github.com/venyii)\n* [Viktor Szakats](https://github.com/vszakats)\n* [Ville Skyttä](https://github.com/scop)\n* [Vinney Cavallo](https://github.com/vcavallo)\n* [Vladimir Rutsky](https://github.com/rutsky)\n* [Wang Yu](https://github.com/wyhitcs)\n* [Ward Vandewege](https://github.com/cure)\n* [Whyfoo](https://github.com/whyfoo)\n* [Wilfried Teiken](https://github.com/wteiken)\n* [Willem Fibbe](https://github.com/fibbers)\n* [William Budington](https://github.com/Hainish)\n* [Will Greenberg](https://github.com/wgreenberg)\n* [Will Newby](https://github.com/willnewby)\n* [Will Oller](https://github.com/willoller)\n* [Yan](https://github.com/diracdeltas)\n* [Yen Chi Hsuan](https://github.com/yan12125)\n* [Yomna](https://github.com/ynasser)\n* [Yoni Jah](https://github.com/yonjah)\n* [YourDaddyIsHere](https://github.com/YourDaddyIsHere)\n* [Yuseong Cho](https://github.com/g6123)\n* [Zach Shepherd](https://github.com/zjs)\n* [陈三](https://github.com/chenxsan)\n* [Shahar Naveh](https://github.com/ShaharNaveh)\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "This project is governed by [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode)."
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "<!---\n\nzoracon: (This is an old comment below, not sure how accurate this is anymore. \nSince Github seems to lean more towards Markdown these days, it's still probably accurate)\n \nThis file serves as an entry point for GitHub's Contributing\nGuidelines [1] only.\n\nGitHub doesn't render rST very well, especially in respect to internal\nhyperlink targets and cross-references [2]. People also tend to\nconfuse rST and Markdown syntax. Therefore, instead of keeping the\ncontents here (and including from rST documentation under doc/), link\nto the Sphinx generated docs is provided below.\n\n\n[1] https://github.com/blog/1184-contributing-guidelines\n[2] https://docutils.sourceforge.io/docs/user/rst/quickref.html#hyperlink-targets\n\n-->\n\n# Certbot Contributing Guide\n\nHi! Welcome to the Certbot project. We look forward to collaborating with you.\n\nIf you're reporting a bug in Certbot. Please open an issue: https://github.com/certbot/certbot/issues/new/choose.\n\nIf you're having trouble using Certbot and aren't sure you've found a bug, please first try asking for help at https://community.letsencrypt.org/. There is a much larger community there of people familiar with the project who will be able to more quickly answer your questions.\n\nIf you're a developer, we have some helpful information in our\n[Developer's Guide](https://certbot.eff.org/docs/contributing.html) to get you\nstarted. In particular, we recommend you read these sections:\n\n - [EFF's Public Projects Code of Conduct](https://www.eff.org/pages/eppcode)\n - [Finding issues to work on](https://certbot.eff.org/docs/contributing.html#find-issues-to-work-on)\n - [Coding style](https://certbot.eff.org/docs/contributing.html#coding-style)\n - [Submitting a pull request](https://certbot.eff.org/docs/contributing.html#submitting-a-pull-request)\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "Certbot ACME Client\nCopyright (c) Electronic Frontier Foundation and others\nLicensed Apache Version 2.0\n\nThe nginx plugin incorporates code from nginxparser\nCopyright (c) 2014 Fatih Erikli\nLicensed MIT\n\n\nText of Apache License\n======================\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n\nText of MIT License\n===================\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nExplanation on supported versions [here](https://github.com/certbot/certbot/wiki/Architectural-Decision-Records#-update-to-certbots-version-policy-and-end-of-life-support-on-previous-major-versions)\n\n| Major Version | Support Level |\n| ------- | ------------------ |\n| >= 5.0  | Full Support |\n| 4.x     | Discretionary Backports |\n| <=3.x   | None |\n\n\n## Reporting a Vulnerability\n\nSecurity vulnerabilities can be reported using GitHub's [private vulnerability reporting tool](https://github.com/certbot/certbot/security/advisories/new).\n"
  },
  {
    "path": "acme/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: acme/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: acme/readthedocs.org.requirements.txt"
  },
  {
    "path": "acme/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "acme/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\nrecursive-include examples *\nrecursive-include src/acme/_internal/tests/testdata *\ninclude src/acme/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "acme/README.rst",
    "content": "ACME protocol implementation in Python\n"
  },
  {
    "path": "acme/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "acme/docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(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/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  applehelp  to make an Apple Help Book\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/acme-python.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/acme-python.qhc\"\n\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/acme-python\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/acme-python\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "acme/docs/_static/.gitignore",
    "content": ""
  },
  {
    "path": "acme/docs/_templates/.gitignore",
    "content": ""
  },
  {
    "path": "acme/docs/api/challenges.rst",
    "content": "Challenges\n----------\n\n.. automodule:: acme.challenges\n   :members:\n"
  },
  {
    "path": "acme/docs/api/client.rst",
    "content": "Client\n------\n\n.. automodule:: acme.client\n   :members:\n"
  },
  {
    "path": "acme/docs/api/crypto_util.rst",
    "content": "Crypto_util\n-----------\n\n.. automodule:: acme.crypto_util\n   :members:\n"
  },
  {
    "path": "acme/docs/api/errors.rst",
    "content": "Errors\n------\n\n.. automodule:: acme.errors\n   :members:\n"
  },
  {
    "path": "acme/docs/api/fields.rst",
    "content": "Fields\n------\n\n.. automodule:: acme.fields\n   :members:\n"
  },
  {
    "path": "acme/docs/api/jose.rst",
    "content": "JOSE\n----\n\nThe ``acme.jose`` module was moved to its own package \"josepy_\".\nPlease refer to its documentation there.\n\n.. _josepy: https://josepy.readthedocs.io/\n"
  },
  {
    "path": "acme/docs/api/jws.rst",
    "content": "JWS\n---\n\n.. automodule:: acme.jws\n   :members:\n"
  },
  {
    "path": "acme/docs/api/messages.rst",
    "content": "Messages\n--------\n\n.. automodule:: acme.messages\n   :members:\n"
  },
  {
    "path": "acme/docs/api/standalone.rst",
    "content": "Standalone\n----------\n\n.. automodule:: acme.standalone\n   :members:\n"
  },
  {
    "path": "acme/docs/api/util.rst",
    "content": "Util\n----\n\n.. automodule:: acme.util\n   :members:\n"
  },
  {
    "path": "acme/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\n.. toctree::\n   :glob:\n\n   api/*\n"
  },
  {
    "path": "acme/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# acme-python documentation build configuration file, created by\n# sphinx-quickstart on Sun Oct 18 13:38:06 2015.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\nimport os\nimport sys\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\nsys.path.insert(0, os.path.abspath(os.path.join(here, '..')))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme',\n]\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'acme-python'\ncopyright = u'2015, Let\\'s Encrypt Project'\nauthor = u'Let\\'s Encrypt Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '0'\n# The full version, including alpha/beta/rc tags.\nrelease = '0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#today = ''\n# Else, today_fmt is used as the format for a strftime call.\n#today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = [\n    '_build',\n]\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\ndefault_role = 'py:obj'\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n#modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n#keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n#html_theme_path = []\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n#html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#html_logo = None\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#html_extra_path = []\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\n#html_last_updated_fmt = '%b %d, %Y'\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#html_additional_pages = {}\n\n# If false, no module index is generated.\n#html_domain_indices = True\n\n# If false, no index is generated.\n#html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n#html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'\n#html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# Now only 'ja' uses this config value\n#html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'acme-pythondoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #'preamble': '',\n\n    # Latex figure (float) alignment\n    #'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'acme-python.tex', u'acme-python Documentation',\n     u'Let\\'s Encrypt Project', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#latex_use_parts = False\n\n# If true, show page references after internal links.\n#latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n#latex_appendices = []\n\n# If false, no module index is generated.\n#latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'acme-python', u'acme-python Documentation',\n     [author], 1),\n    ('man/jws', 'jws', u'jws script documentation', [project], 1),\n]\n\n# If true, show URL addresses after external links.\n#man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'acme-python', u'acme-python Documentation',\n     author, 'acme-python', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n#texinfo_appendices = []\n\n# If false, no module index is generated.\n#texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#texinfo_no_detailmenu = False\n\n\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'josepy': ('https://josepy.readthedocs.io/en/latest/', None),\n}\n"
  },
  {
    "path": "acme/docs/index.rst",
    "content": ".. acme-python documentation master file, created by\n   sphinx-quickstart on Sun Oct 18 13:38:06 2015.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to acme-python's documentation!\n=======================================\n\nContents:\n\n.. toctree::\n   :maxdepth: 2\n\n   api\n\n.. automodule:: acme\n   :members:\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n\n"
  },
  {
    "path": "acme/docs/jws-help.txt",
    "content": "usage: jws [-h] [--compact] {sign,verify} ...\n\npositional arguments:\n  {sign,verify}\n\noptions:\n  -h, --help     show this help message and exit\n  --compact\n"
  },
  {
    "path": "acme/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset BUILDDIR=_build\r\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .\r\nset I18NSPHINXOPTS=%SPHINXOPTS% .\r\nif NOT \"%PAPER%\" == \"\" (\r\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\r\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\nif \"%1\" == \"help\" (\r\n\t:help\r\n\techo.Please use `make ^<target^>` where ^<target^> is one of\r\n\techo.  html       to make standalone HTML files\r\n\techo.  dirhtml    to make HTML files named index.html in directories\r\n\techo.  singlehtml to make a single large HTML file\r\n\techo.  pickle     to make pickle files\r\n\techo.  json       to make JSON files\r\n\techo.  htmlhelp   to make HTML files and a HTML help project\r\n\techo.  qthelp     to make HTML files and a qthelp project\r\n\techo.  devhelp    to make HTML files and a Devhelp project\r\n\techo.  epub       to make an epub\r\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\r\n\techo.  text       to make text files\r\n\techo.  man        to make manual pages\r\n\techo.  texinfo    to make Texinfo files\r\n\techo.  gettext    to make PO message catalogs\r\n\techo.  changes    to make an overview over all changed/added/deprecated items\r\n\techo.  xml        to make Docutils-native XML files\r\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\r\n\techo.  linkcheck  to check all external links for integrity\r\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\r\n\techo.  coverage   to run coverage check of the documentation if enabled\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"clean\" (\r\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\r\n\tdel /q /s %BUILDDIR%\\*\r\n\tgoto end\r\n)\r\n\r\n\r\nREM Check if sphinx-build is available and fallback to Python version if any\r\n%SPHINXBUILD% 2> nul\r\nif errorlevel 9009 goto sphinx_python\r\ngoto sphinx_ok\r\n\r\n:sphinx_python\r\n\r\nset SPHINXBUILD=python -m sphinx.__init__\r\n%SPHINXBUILD% 2> nul\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n:sphinx_ok\r\n\r\n\r\nif \"%1\" == \"html\" (\r\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/html.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"dirhtml\" (\r\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"singlehtml\" (\r\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pickle\" (\r\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the pickle files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"json\" (\r\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the JSON files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"htmlhelp\" (\r\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run HTML Help Workshop with the ^\r\n.hhp project file in %BUILDDIR%/htmlhelp.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"qthelp\" (\r\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\r\n.qhcp project file in %BUILDDIR%/qthelp, like this:\r\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\acme-python.qhcp\r\n\techo.To view the help file:\r\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\acme-python.ghc\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"devhelp\" (\r\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"epub\" (\r\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The epub file is in %BUILDDIR%/epub.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latex\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; the LaTeX files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdf\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdfja\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf-ja\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"text\" (\r\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The text files are in %BUILDDIR%/text.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"man\" (\r\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The manual pages are in %BUILDDIR%/man.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"texinfo\" (\r\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"gettext\" (\r\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The message catalogs are in %BUILDDIR%/locale.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"changes\" (\r\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.The overview file is in %BUILDDIR%/changes.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"linkcheck\" (\r\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Link check complete; look for any errors in the above output ^\r\nor in %BUILDDIR%/linkcheck/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"doctest\" (\r\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of doctests in the sources finished, look at the ^\r\nresults in %BUILDDIR%/doctest/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"coverage\" (\r\n\t%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of coverage in the sources finished, look at the ^\r\nresults in %BUILDDIR%/coverage/python.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"xml\" (\r\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The XML files are in %BUILDDIR%/xml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pseudoxml\" (\r\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.\r\n\tgoto end\r\n)\r\n\r\n:end\r\n"
  },
  {
    "path": "acme/docs/man/jws.rst",
    "content": ":orphan:\n\n.. literalinclude:: ../jws-help.txt\n"
  },
  {
    "path": "acme/examples/http01_example.py",
    "content": "\"\"\"Example ACME-V2 API for HTTP-01 challenge.\n\nBrief:\n\nThis a complete usage example of the python-acme API.\n\nLimitations of this example:\n    - Works for only one Domain name\n    - Performs only HTTP-01 challenge\n    - Uses ACME-v2\n\nWorkflow:\n    (Account creation)\n    - Create account key\n    - Register account and accept TOS\n    (Certificate actions)\n    - Select HTTP-01 within offered challenges by the CA server\n    - Set up http challenge resource\n    - Set up standalone web server\n    - Create domain private key and CSR\n    - Issue certificate\n    - Renew certificate\n    - Revoke certificate\n    (Account update actions)\n    - Change contact information\n    - Deactivate Account\n\"\"\"\nfrom contextlib import contextmanager\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nimport josepy as jose\n\nfrom acme import challenges\nfrom acme import client\nfrom acme import crypto_util\nfrom acme import errors\nfrom acme import messages\nfrom acme import standalone\n\n# Constants:\n\n# This is the staging point for ACME-V2 within Let's Encrypt.\nDIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'\n\nUSER_AGENT = 'python-acme-example'\n\n# Account key size\nACC_KEY_BITS = 2048\n\n# Certificate private key size\nCERT_PKEY_BITS = 2048\n\n# Domain name for the certificate.\nDOMAIN = 'client.example.com'\n\n# If you are running Boulder locally, it is possible to configure any port\n# number to execute the challenge, but real CA servers will always use port\n# 80, as described in the ACME specification.\nPORT = 80\n\n\n# Useful methods and classes:\n\n\ndef new_csr_comp(domain_name, pkey_pem=None):\n    \"\"\"Create certificate signing request.\"\"\"\n    if pkey_pem is None:\n        # Create private key.\n        pkey = rsa.generate_private_key(public_exponent=65537, key_size=CERT_PKEY_BITS)\n        pkey_pem = pkey.private_bytes(encoding=serialization.Encoding.PEM,\n                                      format=serialization.PrivateFormat.PKCS8,\n                                      encryption_algorithm=serialization.NoEncryption())\n\n    csr_pem = crypto_util.make_csr(pkey_pem, [domain_name])\n    return pkey_pem, csr_pem\n\n\ndef select_http01_chall(orderr):\n    \"\"\"Extract authorization resource from within order resource.\"\"\"\n    # Authorization Resource: authz.\n    # This object holds the offered challenges by the server and their status.\n    authz_list = orderr.authorizations\n\n    for authz in authz_list:\n        # Choosing challenge.\n        # authz.body.challenges is a set of ChallengeBody objects.\n        for i in authz.body.challenges:\n            # Find the supported challenge.\n            if isinstance(i.chall, challenges.HTTP01):\n                return i\n\n    raise Exception('HTTP-01 challenge was not offered by the CA server.')\n\n\n@contextmanager\ndef challenge_server(http_01_resources):\n    \"\"\"Manage standalone server set up and shutdown.\"\"\"\n\n    # Setting up a fake server that binds at PORT and any address.\n    address = ('', PORT)\n    try:\n        servers = standalone.HTTP01DualNetworkedServers(address,\n                                                        http_01_resources)\n        # Start client standalone web server.\n        servers.serve_forever()\n        yield servers\n    finally:\n        # Shutdown client web server and unbind from PORT\n        servers.shutdown_and_server_close()\n\n\ndef perform_http01(client_acme, challb, orderr):\n    \"\"\"Set up standalone webserver and perform HTTP-01 challenge.\"\"\"\n\n    response, validation = challb.response_and_validation(client_acme.net.key)\n\n    resource = standalone.HTTP01RequestHandler.HTTP01Resource(\n        chall=challb.chall, response=response, validation=validation)\n\n    with challenge_server({resource}):\n        # Let the CA server know that we are ready for the challenge.\n        client_acme.answer_challenge(challb, response)\n\n        # Wait for challenge status and then issue a certificate.\n        # It is possible to set a deadline time.\n        finalized_orderr = client_acme.poll_and_finalize(orderr)\n\n    return finalized_orderr.fullchain_pem\n\n\n# Main examples:\n\n\ndef example_http():\n    \"\"\"This example executes the whole process of fulfilling a HTTP-01\n    challenge for one specific domain.\n\n    The workflow consists of:\n    (Account creation)\n    - Create account key\n    - Register account and accept TOS\n    (Certificate actions)\n    - Select HTTP-01 within offered challenges by the CA server\n    - Set up http challenge resource\n    - Set up standalone web server\n    - Create domain private key and CSR\n    - Issue certificate\n    - Renew certificate\n    - Revoke certificate\n    (Account update actions)\n    - Change contact information\n    - Deactivate Account\n\n    \"\"\"\n    # Create account key\n\n    acc_key = jose.JWKRSA(\n        key=rsa.generate_private_key(public_exponent=65537,\n                                     key_size=ACC_KEY_BITS,\n                                     backend=default_backend()))\n\n    # Register account and accept TOS\n\n    net = client.ClientNetwork(acc_key, user_agent=USER_AGENT)\n    directory = client.ClientV2.get_directory(DIRECTORY_URL, net)\n    client_acme = client.ClientV2(directory, net=net)\n\n    # Terms of Service URL is in client_acme.directory.meta.terms_of_service\n    # Registration Resource: regr\n    regr = client_acme.new_account(\n        messages.NewRegistration.from_data(terms_of_service_agreed=True))\n\n    # Create domain private key and CSR\n    pkey_pem, csr_pem = new_csr_comp(DOMAIN)\n\n    # Issue certificate\n\n    orderr = client_acme.new_order(csr_pem)\n\n    # Select HTTP-01 within offered challenges by the CA server\n    challb = select_http01_chall(orderr)\n\n    # The certificate is ready to be used in the variable \"fullchain_pem\".\n    fullchain_pem = perform_http01(client_acme, challb, orderr)\n\n    # Renew certificate\n\n    _, csr_pem = new_csr_comp(DOMAIN, pkey_pem)\n\n    orderr = client_acme.new_order(csr_pem)\n\n    challb = select_http01_chall(orderr)\n\n    # Performing challenge\n    fullchain_pem = perform_http01(client_acme, challb, orderr)\n\n    # Revoke certificate\n\n    fullchain_com = x509.load_pem_x509_certificate(fullchain_pem.encode())\n\n    try:\n        client_acme.revoke(fullchain_com, 0)  # revocation reason = 0\n    except errors.ConflictError:\n        # Certificate already revoked.\n        pass\n\n    # Query registration status.\n    client_acme.net.account = regr\n    try:\n        regr = client_acme.query_registration(regr)\n    except errors.Error as err:\n        if err.typ == messages.ERROR_PREFIX + 'unauthorized':\n            # Status is deactivated.\n            pass\n        raise\n\n    # Change contact information\n\n    email = 'newfake@example.com'\n    regr = client_acme.update_registration(\n        regr.update(\n            body=regr.body.update(\n                contact=('mailto:' + email,)\n            )\n        )\n    )\n\n    # Deactivate account/registration\n\n    regr = client_acme.deactivate_registration(regr)\n\n\nif __name__ == \"__main__\":\n    example_http()\n"
  },
  {
    "path": "acme/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"acme\"\ndynamic = [\"version\"]\ndescription = \"ACME protocol implementation in Python\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n]\ndependencies = [\n    \"cryptography>=43.0.0\",\n    \"josepy>=2.0.0\",\n    # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be\n    # relaxed to >=24.0.0 if needed.\n    \"PyOpenSSL>=25.0.0\",\n    \"pyrfc3339\",\n    \"requests>=2.25.1\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n    \"pytest-xdist\",\n    \"typing-extensions\",\n]\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "acme/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e acme[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install acme[docs]\" does not work as\n# expected and \"pip install -e acme[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme[docs]\n"
  },
  {
    "path": "acme/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\nsetup(\n    version=version,\n)\n"
  },
  {
    "path": "acme/src/acme/__init__.py",
    "content": "\"\"\"ACME protocol implementation.\n\nThis module is an implementation of the `ACME protocol`_.\n\n.. _`ACME protocol`: https://datatracker.ietf.org/doc/html/rfc8555\n\n\"\"\"\nimport sys\n\n# This code exists to keep backwards compatibility with people using acme.jose\n# before it became the standalone josepy package.\n#\n# It is based on\n# https://github.com/requests/requests/blob/1278ecdf71a312dc2268f3bfc0aabfab3c006dcf/requests/packages.py\nimport josepy as jose # noqa: F401\n\nfor mod in list(sys.modules):\n    # This traversal is apparently necessary such that the identities are\n    # preserved (acme.jose.* is josepy.*)\n    if mod == 'josepy' or mod.startswith('josepy.'):\n        sys.modules['acme.' + mod.replace('josepy', 'jose', 1)] = sys.modules[mod]\n"
  },
  {
    "path": "acme/src/acme/_internal/__init__.py",
    "content": "\"\"\"acme's internal implementation\"\"\"\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/__init__.py",
    "content": "\"\"\"acme tests\"\"\"\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/challenges_test.py",
    "content": "\"\"\"Tests for acme.challenges.\"\"\"\nimport sys\nfrom typing import TYPE_CHECKING\nimport unittest\nfrom unittest import mock\nimport urllib.parse as urllib_parse\n\nimport josepy as jose\nfrom josepy.jwk import JWKEC\nimport pytest\nimport requests\n\nfrom acme._internal.tests import test_util\n\nCERT = test_util.load_cert('cert.pem')\nKEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))\n\n\nclass ChallengeTest(unittest.TestCase):\n\n    def test_from_json_unrecognized(self):\n        from acme.challenges import Challenge\n        from acme.challenges import UnrecognizedChallenge\n        chall = UnrecognizedChallenge({\"type\": \"foo\"})\n        assert chall == Challenge.from_json(chall.jobj)\n\n\nclass UnrecognizedChallengeTest(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import UnrecognizedChallenge\n        self.jobj = {\"type\": \"foo\"}\n        self.chall = UnrecognizedChallenge(self.jobj)\n\n    def test_to_partial_json(self):\n        assert self.jobj == self.chall.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import UnrecognizedChallenge\n        assert self.chall == UnrecognizedChallenge.from_json(self.jobj)\n\n\nclass KeyAuthorizationChallengeResponseTest(unittest.TestCase):\n\n    def setUp(self):\n        def _encode(name):\n            assert name == \"token\"\n            return \"foo\"\n        self.chall = mock.Mock()\n        self.chall.encode.side_effect = _encode\n\n    def test_verify_ok(self):\n        from acme.challenges import KeyAuthorizationChallengeResponse\n        response = KeyAuthorizationChallengeResponse(\n            key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')\n        assert response.verify(self.chall, KEY.public_key())\n\n    def test_verify_wrong_token(self):\n        from acme.challenges import KeyAuthorizationChallengeResponse\n        response = KeyAuthorizationChallengeResponse(\n            key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')\n        assert not response.verify(self.chall, KEY.public_key())\n\n    def test_verify_wrong_thumbprint(self):\n        from acme.challenges import KeyAuthorizationChallengeResponse\n        response = KeyAuthorizationChallengeResponse(\n            key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv')\n        assert not response.verify(self.chall, KEY.public_key())\n\n    def test_verify_wrong_form(self):\n        from acme.challenges import KeyAuthorizationChallengeResponse\n        response = KeyAuthorizationChallengeResponse(\n            key_authorization='.foo.oKGqedy-b-acd5eoybm2f-'\n            'NVFxvyOoET5CNy3xnv8WY')\n        assert not response.verify(self.chall, KEY.public_key())\n\n\nclass DNS01ResponseTest(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import DNS01Response\n        self.msg = DNS01Response(key_authorization=u'foo')\n        self.jmsg = {\n            'resource': 'challenge',\n            'type': 'dns-01',\n            'keyAuthorization': u'foo',\n        }\n\n        from acme.challenges import DNS01\n        self.chall = DNS01(token=(b'x' * 16))\n        self.response = self.chall.response(KEY)\n\n    def test_to_partial_json(self):\n        assert {} == self.msg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import DNS01Response\n        assert self.msg == DNS01Response.from_json(self.jmsg)\n\n    def test_from_json_hashable(self):\n        from acme.challenges import DNS01Response\n        hash(DNS01Response.from_json(self.jmsg))\n\n    def test_simple_verify_failure(self):\n        key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))\n        public_key = key2.public_key()\n        verified = self.response.simple_verify(self.chall, \"local\", public_key)\n        assert not verified\n\n    def test_simple_verify_success(self):\n        public_key = KEY.public_key()\n        verified = self.response.simple_verify(self.chall, \"local\", public_key)\n        assert verified\n\n\nclass DNS01Test(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import DNS01\n        self.msg = DNS01(token=jose.decode_b64jose(\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))\n        self.jmsg = {\n            'type': 'dns-01',\n            'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',\n        }\n\n    def test_validation_domain_name(self):\n        assert '_acme-challenge.www.example.com' == \\\n                         self.msg.validation_domain_name('www.example.com')\n\n    def test_validation(self):\n        assert \"rAa7iIg4K2y63fvUhCfy8dP1Xl7wEhmQq0oChTcE3Zk\" == \\\n            self.msg.validation(KEY)\n\n    def test_to_partial_json(self):\n        assert self.jmsg == self.msg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import DNS01\n        assert self.msg == DNS01.from_json(self.jmsg)\n\n    def test_from_json_hashable(self):\n        from acme.challenges import DNS01\n        hash(DNS01.from_json(self.jmsg))\n\n\nclass HTTP01ResponseTest(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import HTTP01Response\n        self.msg = HTTP01Response(key_authorization=u'foo')\n        self.jmsg = {\n            'resource': 'challenge',\n            'type': 'http-01',\n            'keyAuthorization': u'foo',\n        }\n\n        from acme.challenges import HTTP01\n        self.chall = HTTP01(token=(b'x' * 16))\n        self.response = self.chall.response(KEY)\n\n    def test_to_partial_json(self):\n        assert {} == self.msg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import HTTP01Response\n        assert self.msg == HTTP01Response.from_json(self.jmsg)\n\n    def test_from_json_hashable(self):\n        from acme.challenges import HTTP01Response\n        hash(HTTP01Response.from_json(self.jmsg))\n\n    def test_simple_verify_bad_key_authorization(self):\n        key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))\n        self.response.simple_verify(self.chall, \"local\", key2.public_key())\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_good_validation(self, mock_get):\n        validation = self.chall.validation(KEY)\n        mock_get.return_value = mock.MagicMock(text=validation)\n        assert self.response.simple_verify(\n            self.chall, \"local\", KEY.public_key())\n        mock_get.assert_called_once_with(self.chall.uri(\"local\"), verify=False,\n                                         timeout=mock.ANY)\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_bad_validation(self, mock_get):\n        mock_get.return_value = mock.MagicMock(text=\"!\")\n        assert not self.response.simple_verify(\n            self.chall, \"local\", KEY.public_key())\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_whitespace_validation(self, mock_get):\n        from acme.challenges import HTTP01Response\n        mock_get.return_value = mock.MagicMock(\n            text=(self.chall.validation(KEY) +\n                  HTTP01Response.WHITESPACE_CUTSET))\n        assert self.response.simple_verify(\n            self.chall, \"local\", KEY.public_key())\n        mock_get.assert_called_once_with(self.chall.uri(\"local\"), verify=False,\n                                         timeout=mock.ANY)\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_connection_error(self, mock_get):\n        mock_get.side_effect = requests.exceptions.RequestException\n        assert not self.response.simple_verify(\n            self.chall, \"local\", KEY.public_key())\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_port(self, mock_get):\n        self.response.simple_verify(\n            self.chall, domain=\"local\",\n            account_public_key=KEY.public_key(), port=8080)\n        assert \"local:8080\" == urllib_parse.urlparse(\n            mock_get.mock_calls[0][1][0]).netloc\n\n    @mock.patch(\"acme.challenges.requests.get\")\n    def test_simple_verify_timeout(self, mock_get):\n        self.response.simple_verify(self.chall, \"local\", KEY.public_key())\n        mock_get.assert_called_once_with(self.chall.uri(\"local\"), verify=False,\n                                         timeout=30)\n        mock_get.reset_mock()\n        self.response.simple_verify(self.chall, \"local\", KEY.public_key(), timeout=1234)\n        mock_get.assert_called_once_with(self.chall.uri(\"local\"), verify=False,\n                                         timeout=1234)\n\n\nclass HTTP01Test(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import HTTP01\n        self.msg = HTTP01(\n            token=jose.decode_b64jose(\n                'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))\n        self.jmsg = {\n            'type': 'http-01',\n            'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',\n        }\n\n    def test_path(self):\n        assert self.msg.path == '/.well-known/acme-challenge/' \\\n                         'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'\n\n    def test_uri(self):\n        assert 'http://example.com/.well-known/acme-challenge/' \\\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' == \\\n            self.msg.uri('example.com')\n        assert 'http://1.2.3.4/.well-known/acme-challenge/' \\\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' == \\\n            self.msg.uri('1.2.3.4')\n        assert 'http://[::1]/.well-known/acme-challenge/' \\\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA' == \\\n            self.msg.uri('::1')\n\n    def test_to_partial_json(self):\n        assert self.jmsg == self.msg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import HTTP01\n        assert self.msg == HTTP01.from_json(self.jmsg)\n\n    def test_from_json_hashable(self):\n        from acme.challenges import HTTP01\n        hash(HTTP01.from_json(self.jmsg))\n\n    def test_good_token(self):\n        assert self.msg.good_token\n        assert not self.msg.update(token=b'..').good_token\n\n\nclass TestDNS:\n\n    if TYPE_CHECKING:\n        from acme.challenges import DNS\n\n    @pytest.fixture\n    def jmsg(self) -> dict:\n        jmsg = {\n            'type': 'dns',\n            'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',\n        }\n        return jmsg\n\n    @pytest.fixture\n    def msg(self) -> 'DNS':\n        from acme.challenges import DNS\n        msg = DNS(token=jose.b64decode(\n            b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))\n        return msg\n\n    def test_to_partial_json(self, msg: 'DNS', jmsg: dict):\n        assert jmsg == msg.to_partial_json()\n\n    def test_from_json(self, msg: 'DNS', jmsg: dict):\n        from acme.challenges import DNS\n        assert msg == DNS.from_json(jmsg)\n\n    def test_from_json_hashable(self, jmsg: dict):\n        from acme.challenges import DNS\n        hash(DNS.from_json(jmsg))\n\n    # Using fixtures in parametrize is an open issue\n    # https://github.com/pytest-dev/pytest/issues/349\n    @pytest.mark.parametrize(\"key, alg\", [\n        (KEY, jose.RS256),\n        (JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem')), jose.ES384)])\n    def test_gen_check_validation(self, key, alg, msg: 'DNS'):\n        assert msg.check_validation(\n            msg.gen_validation(key, alg=alg), key.public_key())\n\n    def test_gen_check_validation_wrong_key(self, msg: 'DNS'):\n        key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))\n        assert not msg.check_validation(\n            msg.gen_validation(KEY), key2.public_key())\n\n    def test_check_validation_wrong_payload(self, msg: 'DNS'):\n        validations = tuple(\n            jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY)\n            for payload in (b'', b'{}')\n        )\n        for validation in validations:\n            assert not msg.check_validation(\n                validation, KEY.public_key())\n\n    def test_check_validation_wrong_fields(self, msg: 'DNS'):\n        bad_validation = jose.JWS.sign(\n            payload=msg.update(\n                token=b'x' * 20).json_dumps().encode('utf-8'),\n            alg=jose.RS256, key=KEY)\n        assert not msg.check_validation(bad_validation, KEY.public_key())\n\n    def test_gen_response(self, msg: 'DNS'):\n        with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen:\n            mock_gen.return_value = mock.sentinel.validation\n            response = msg.gen_response(KEY)\n        from acme.challenges import DNSResponse\n        assert isinstance(response, DNSResponse)\n        assert response.validation == mock.sentinel.validation\n\n    def test_validation_domain_name(self, msg: 'DNS'):\n        assert '_acme-challenge.le.wtf' == msg.validation_domain_name('le.wtf')\n\n    def test_validation_domain_name_ecdsa(self, msg: 'DNS'):\n        ec_key_secp384r1 = JWKEC(key=test_util.load_ecdsa_private_key('ec_secp384r1_key.pem'))\n        assert msg.check_validation(\n            msg.gen_validation(ec_key_secp384r1, alg=jose.ES384),\n            ec_key_secp384r1.public_key()) is True\n\n\nclass DNSResponseTest(unittest.TestCase):\n\n    def setUp(self):\n        from acme.challenges import DNS\n        self.chall = DNS(token=jose.b64decode(\n            b\"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA\"))\n        self.validation = jose.JWS.sign(\n            payload=self.chall.json_dumps(sort_keys=True).encode(),\n            key=KEY, alg=jose.RS256)\n\n        from acme.challenges import DNSResponse\n        self.msg = DNSResponse(validation=self.validation)\n        self.jmsg_to = {\n            'validation': self.validation,\n        }\n        self.jmsg_from = {\n            'resource': 'challenge',\n            'type': 'dns',\n            'validation': self.validation.to_json(),\n        }\n\n    def test_to_partial_json(self):\n        assert self.jmsg_to == self.msg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.challenges import DNSResponse\n        assert self.msg == DNSResponse.from_json(self.jmsg_from)\n\n    def test_from_json_hashable(self):\n        from acme.challenges import DNSResponse\n        hash(DNSResponse.from_json(self.jmsg_from))\n\n    def test_check_validation(self):\n        assert self.msg.check_validation(self.chall, KEY.public_key())\n\n\nclass JWSPayloadRFC8555Compliant(unittest.TestCase):\n    \"\"\"Test for RFC8555 compliance of JWS generated from resources/challenges\"\"\"\n    def test_challenge_payload(self):\n        from acme.challenges import HTTP01Response\n\n        challenge_body = HTTP01Response()\n\n        jobj = challenge_body.json_dumps(indent=2).encode()\n        # RFC8555 states that challenge responses must have an empty payload.\n        assert jobj == b'{}'\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/client_test.py",
    "content": "\"\"\"Tests for acme.client.\"\"\"\n# pylint: disable=too-many-lines\nimport copy\nimport datetime\nimport http.client as http_client\nimport json\nimport sys\nimport unittest\nfrom unittest import mock\n\nfrom cryptography import x509\n\nimport josepy as jose\nimport pytest\nimport requests\n\nfrom acme import challenges\nfrom acme import errors\nfrom acme import jws as acme_jws\nfrom acme import messages\nfrom acme._internal.tests import messages_test\nfrom acme._internal.tests import test_util\nfrom acme.client import ClientNetwork\nfrom acme.client import ClientV2\n\nCERT_SAN_PEM = test_util.load_vector('cert-san.pem')\nCSR_MIXED_PEM = test_util.load_vector('csr-mixed.pem')\nCSR_NO_SANS_PEM = test_util.load_vector('csr-nosans.pem')\nKEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))\n\nDIRECTORY_V2 = messages.Directory({\n    'newAccount': 'https://www.letsencrypt-demo.org/acme/new-account',\n    'newNonce': 'https://www.letsencrypt-demo.org/acme/new-nonce',\n    'newOrder': 'https://www.letsencrypt-demo.org/acme/new-order',\n    'revokeCert': 'https://www.letsencrypt-demo.org/acme/revoke-cert',\n    'meta': messages.Directory.Meta(),\n})\n\n\nclass ClientV2Test(unittest.TestCase):\n    \"\"\"Tests for acme.client.ClientV2.\"\"\"\n\n    def setUp(self):\n        self.response = mock.MagicMock(\n            ok=True, status_code=http_client.OK, headers={}, links={})\n        self.net = mock.MagicMock()\n        self.net.post.return_value = self.response\n        self.net.get.return_value = self.response\n\n        self.identifier = messages.Identifier(\n            typ=messages.IDENTIFIER_FQDN, value='example.com')\n\n        # Registration\n        self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')\n        reg = messages.Registration(\n            contact=self.contact, key=KEY.public_key())\n        the_arg: dict = dict(reg)\n        self.new_reg = messages.NewRegistration(**the_arg)\n        self.regr = messages.RegistrationResource(\n            body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1')\n\n        # Authorization\n        authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'\n        challb = messages.ChallengeBody(\n            uri=(authzr_uri + '/1'), status=messages.STATUS_VALID,\n            chall=challenges.DNS(token=jose.b64decode(\n                'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')))\n        self.challr = messages.ChallengeResource(\n            body=challb, authzr_uri=authzr_uri)\n        self.authz = messages.Authorization(\n            identifier=messages.Identifier(\n                typ=messages.IDENTIFIER_FQDN, value='example.com'),\n            challenges=(challb,))\n        self.authzr = messages.AuthorizationResource(\n            body=self.authz, uri=authzr_uri)\n\n        # Reason code for revocation\n        self.rsn = 1\n\n        self.directory = DIRECTORY_V2\n\n        self.client = ClientV2(self.directory, self.net)\n\n        self.new_reg = self.new_reg.update(terms_of_service_agreed=True)\n\n        self.authzr_uri2 = 'https://www.letsencrypt-demo.org/acme/authz/2'\n        self.authz2 = self.authz.update(identifier=messages.Identifier(\n            typ=messages.IDENTIFIER_FQDN, value='www.example.com'),\n            status=messages.STATUS_PENDING)\n        self.authzr2 = messages.AuthorizationResource(\n            body=self.authz2, uri=self.authzr_uri2)\n\n        self.order = messages.Order(\n            identifiers=(self.authz.identifier, self.authz2.identifier),\n            status=messages.STATUS_PENDING,\n            authorizations=(self.authzr.uri, self.authzr_uri2),\n            finalize='https://www.letsencrypt-demo.org/acme/acct/1/order/1/finalize')\n        self.orderr = messages.OrderResource(\n            body=self.order,\n            uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',\n            authorizations=[self.authzr, self.authzr2], csr_pem=CSR_MIXED_PEM)\n        self.orderr2 = messages.OrderResource(\n            body=self.order,\n            uri='https://www.letsencrypt-demo.org/acme/acct/1/order/1',\n            authorizations=[self.authzr, self.authzr2], csr_pem=CSR_NO_SANS_PEM)\n\n    def test_new_account(self):\n        self.response.status_code = http_client.CREATED\n        self.response.json.return_value = self.regr.body.to_json()\n        self.response.headers['Location'] = self.regr.uri\n\n        assert self.regr == self.client.new_account(self.new_reg)\n\n    def test_new_account_tos_link(self):\n        self.response.status_code = http_client.CREATED\n        self.response.json.return_value = self.regr.body.to_json()\n        self.response.headers['Location'] = self.regr.uri\n        self.response.links.update({\n            'terms-of-service': {'url': 'https://www.letsencrypt-demo.org/tos'},\n        })\n\n        assert self.client.new_account(self.new_reg).terms_of_service == \\\n                         'https://www.letsencrypt-demo.org/tos'\n\n\n    def test_new_account_conflict(self):\n        self.response.status_code = http_client.OK\n        self.response.headers['Location'] = self.regr.uri\n        with pytest.raises(errors.ConflictError):\n            self.client.new_account(self.new_reg)\n\n    def test_deactivate_account(self):\n        deactivated_regr = self.regr.update(\n            body=self.regr.body.update(status='deactivated'))\n        self.response.json.return_value = deactivated_regr.body.to_json()\n        self.response.status_code = http_client.OK\n        self.response.headers['Location'] = self.regr.uri\n        assert self.client.deactivate_registration(self.regr) == deactivated_regr\n\n    def test_deactivate_authorization(self):\n        deactivated_authz = self.authzr.update(\n            body=self.authzr.body.update(status=messages.STATUS_DEACTIVATED))\n        self.response.json.return_value = deactivated_authz.body.to_json()\n        authzr = self.client.deactivate_authorization(self.authzr)\n        assert deactivated_authz.body == authzr.body\n        assert self.client.net.post.call_count == 1\n        assert self.authzr.uri in self.net.post.call_args_list[0][0]\n\n    def test_new_order(self):\n        order_response = copy.deepcopy(self.response)\n        order_response.status_code = http_client.CREATED\n        order_response.json.return_value = self.order.to_json()\n        order_response.headers['Location'] = self.orderr.uri\n        self.net.post.return_value = order_response\n\n        authz_response = copy.deepcopy(self.response)\n        authz_response.json.return_value = self.authz.to_json()\n        authz_response.headers['Location'] = self.authzr.uri\n        authz_response2 = self.response\n        authz_response2.json.return_value = self.authz2.to_json()\n        authz_response2.headers['Location'] = self.authzr2.uri\n\n        with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:\n            mock_post_as_get.side_effect = (authz_response, authz_response2)\n            assert self.client.new_order(CSR_MIXED_PEM) == self.orderr\n\n        with mock.patch('acme.client.ClientV2._post_as_get') as mock_post_as_get:\n            mock_post_as_get.side_effect = (authz_response, authz_response2)\n            assert self.client.new_order(CSR_NO_SANS_PEM) == self.orderr2\n\n    def test_answer_challege(self):\n        self.response.links['up'] = {'url': self.challr.authzr_uri}\n        self.response.json.return_value = self.challr.body.to_json()\n        chall_response = challenges.DNSResponse(validation=None)\n        self.client.answer_challenge(self.challr.body, chall_response)\n\n        with pytest.raises(errors.UnexpectedUpdate):\n            self.client.answer_challenge(self.challr.body.update(uri='foo'), chall_response)\n\n    def test_answer_challenge_missing_next(self):\n        with pytest.raises(errors.ClientError):\n            self.client.answer_challenge(self.challr.body, challenges.DNSResponse(validation=None))\n\n    @mock.patch('acme.client.datetime')\n    def test_poll_and_finalize(self, mock_datetime):\n        mock_datetime.datetime.now.return_value = datetime.datetime(2018, 2, 15)\n        mock_datetime.timedelta = datetime.timedelta\n        expected_deadline = mock_datetime.datetime.now() + datetime.timedelta(seconds=90)\n\n        self.client.poll_authorizations = mock.Mock(return_value=self.orderr)\n        self.client.finalize_order = mock.Mock(return_value=self.orderr)\n\n        assert self.client.poll_and_finalize(self.orderr) == self.orderr\n        self.client.poll_authorizations.assert_called_once_with(self.orderr, expected_deadline)\n        self.client.finalize_order.assert_called_once_with(self.orderr, expected_deadline)\n\n    @mock.patch('acme.client.datetime')\n    def test_poll_authorizations_timeout(self, mock_datetime):\n        now_side_effect = [datetime.datetime(2018, 2, 15),\n                           datetime.datetime(2018, 2, 16),\n                           datetime.datetime(2018, 2, 17)]\n        mock_datetime.datetime.now.side_effect = now_side_effect\n        self.response.json.side_effect = [\n            self.authz.to_json(), self.authz2.to_json(), self.authz2.to_json()]\n\n        with pytest.raises(errors.TimeoutError):\n            self.client.poll_authorizations(self.orderr, now_side_effect[1])\n\n    def test_poll_authorizations_failure(self):\n        deadline = datetime.datetime(9999, 9, 9)\n        challb = self.challr.body.update(status=messages.STATUS_INVALID,\n                                         error=messages.Error.with_code('unauthorized'))\n        authz = self.authz.update(status=messages.STATUS_INVALID, challenges=(challb,))\n        self.response.json.return_value = authz.to_json()\n\n        with pytest.raises(errors.ValidationError):\n            self.client.poll_authorizations(self.orderr, deadline)\n\n    def test_poll_authorizations_success(self):\n        deadline = datetime.datetime(9999, 9, 9)\n        updated_authz2 = self.authz2.update(status=messages.STATUS_VALID)\n        updated_authzr2 = messages.AuthorizationResource(\n            body=updated_authz2, uri=self.authzr_uri2)\n        updated_orderr = self.orderr.update(authorizations=[self.authzr, updated_authzr2])\n\n        self.response.json.side_effect = (\n            self.authz.to_json(), self.authz2.to_json(), updated_authz2.to_json())\n        assert self.client.poll_authorizations(self.orderr, deadline) == updated_orderr\n\n    def test_poll_unexpected_update(self):\n        updated_authz = self.authz.update(identifier=self.identifier.update(value='foo'))\n        self.response.json.return_value = updated_authz.to_json()\n        with pytest.raises(errors.UnexpectedUpdate):\n            self.client.poll(self.authzr)\n\n    def test_finalize_order_success(self):\n        updated_order = self.order.update(\n            certificate='https://www.letsencrypt-demo.org/acme/cert/',\n            status=messages.STATUS_VALID)\n        updated_orderr = self.orderr.update(body=updated_order, fullchain_pem=CERT_SAN_PEM)\n\n        self.response.json.return_value = updated_order.to_json()\n        self.response.text = CERT_SAN_PEM\n\n        deadline = datetime.datetime(9999, 9, 9)\n        assert self.client.finalize_order(self.orderr, deadline) == updated_orderr\n\n    def test_finalize_order_error(self):\n        updated_order = self.order.update(\n            error=messages.Error.with_code('unauthorized'),\n            status=messages.STATUS_INVALID)\n        self.response.json.return_value = updated_order.to_json()\n\n        deadline = datetime.datetime(9999, 9, 9)\n        with pytest.raises(errors.IssuanceError):\n            self.client.finalize_order(self.orderr, deadline)\n\n    @mock.patch('acme.client.ClientV2.begin_finalization')\n    def test_finalize_order_ready(self, mock_begin):\n        # https://github.com/certbot/certbot/issues/9766\n        updated_order_ready = self.order.update(status=messages.STATUS_READY)\n\n        updated_order_valid = self.order.update(\n            certificate='https://www.letsencrypt-demo.org/acme/cert/',\n            status=messages.STATUS_VALID)\n        updated_orderr = self.orderr.update(body=updated_order_valid, fullchain_pem=CERT_SAN_PEM)\n\n        self.response.text = CERT_SAN_PEM\n\n        self.response.json.side_effect = [updated_order_ready.to_json(),\n                                           updated_order_valid.to_json()]\n\n        deadline = datetime.datetime(9999, 9, 9)\n        assert self.client.finalize_order(self.orderr, deadline) == updated_orderr\n        assert self.response.json.call_count == 2\n        assert mock_begin.call_count == 2\n\n    def test_finalize_order_invalid_status(self):\n        # https://github.com/certbot/certbot/issues/9296\n        order = self.order.update(error=None, status=messages.STATUS_INVALID)\n        self.response.json.return_value = order.to_json()\n        with pytest.raises(errors.Error, match=\"The certificate order failed\"):\n            self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))\n\n    @mock.patch('acme.client.time.sleep')\n    @mock.patch('acme.client.datetime')\n    def test_finalize_order_orderNotReady(self, dt_mock, mock_sleep):\n        # https://github.com/certbot/certbot/issues/9766\n        updated_order_processing = self.order.update(status=messages.STATUS_PROCESSING)\n        updated_order_ready = self.order.update(status=messages.STATUS_READY)\n\n        updated_order_valid = self.order.update(\n            certificate='https://www.letsencrypt-demo.org/acme/cert/',\n            status=messages.STATUS_VALID)\n        self.orderr.update(body=updated_order_valid, fullchain_pem=CERT_SAN_PEM)\n\n        self.response.text = CERT_SAN_PEM\n\n        self.response.json.side_effect = [updated_order_processing.to_json(),\n                                          updated_order_ready.to_json(),\n                                          updated_order_valid.to_json()]\n\n        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)\n        dt_mock.timedelta = datetime.timedelta\n        self.response.headers['Retry-After'] = '50'\n\n        post = mock.MagicMock()\n        post.side_effect = [messages.Error.with_code('orderNotReady'), # first begin_finalization\n                            # sleep 1\n                            self.response, # first poll_finalization poll --> returns processing\n                            # retry-after sleep here\n                            self.response, # second poll_finalization poll --> returns ready\n                            mock.MagicMock(), # second begin_finalization\n                            # sleep 1\n                            self.response, # third poll_finalization poll --> returns valid\n                            self.response # fetch cert\n                            ]\n        self.net.post = post\n\n        self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))\n        assert self.net.post.call_count == 6\n        assert mock_sleep.call_args_list == [((1,),), ((50,),), ((1,),)]\n\n    def test_finalize_order_otherErrorCode(self):\n        post = mock.MagicMock()\n        post.side_effect = [messages.Error.with_code('serverInternal')]\n        self.net.post = post\n\n        with pytest.raises(messages.Error):\n            self.client.finalize_order(self.orderr, datetime.datetime(9999, 9, 9))\n\n    def test_finalize_order_timeout(self):\n        deadline = datetime.datetime.now() - datetime.timedelta(seconds=60)\n        with pytest.raises(errors.TimeoutError):\n            self.client.finalize_order(self.orderr, deadline)\n\n    def test_finalize_order_alt_chains(self):\n        updated_order = self.order.update(\n            certificate='https://www.letsencrypt-demo.org/acme/cert/',\n            status=messages.STATUS_VALID\n        )\n        updated_orderr = self.orderr.update(body=updated_order,\n                                            fullchain_pem=CERT_SAN_PEM,\n                                            alternative_fullchains_pem=[CERT_SAN_PEM,\n                                                                        CERT_SAN_PEM])\n        self.response.json.return_value = updated_order.to_json()\n        self.response.text = CERT_SAN_PEM\n        self.response.headers['Link'] ='<https://example.com/acme/cert/1>;rel=\"alternate\", ' + \\\n            '<https://example.com/dir>;rel=\"index\", ' + \\\n            '<https://example.com/acme/cert/2>;title=\"foo\";rel=\"alternate\"'\n\n        deadline = datetime.datetime(9999, 9, 9)\n        resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)\n        self.net.post.assert_any_call('https://example.com/acme/cert/1',\n                                      mock.ANY, new_nonce_url=mock.ANY)\n        self.net.post.assert_any_call('https://example.com/acme/cert/2',\n                                      mock.ANY, new_nonce_url=mock.ANY)\n        assert resp == updated_orderr\n\n        del self.response.headers['Link']\n        resp = self.client.finalize_order(self.orderr, deadline, fetch_alternative_chains=True)\n        assert resp == updated_orderr.update(alternative_fullchains_pem=[])\n\n    def test_revoke(self):\n        self.client.revoke(messages_test.CERT, self.rsn)\n        self.net.post.assert_called_once_with(\n            self.directory[\"revokeCert\"], mock.ANY, new_nonce_url=DIRECTORY_V2['newNonce'])\n\n    def test_revoke_bad_status_raises_error(self):\n        self.response.status_code = http_client.METHOD_NOT_ALLOWED\n        with pytest.raises(errors.ClientError):\n            self.client.revoke(messages_test.CERT,\n            self.rsn)\n\n    def test_update_registration(self):\n        # \"Instance of 'Field' has no to_json/update member\" bug:\n        self.response.headers['Location'] = self.regr.uri\n        self.response.json.return_value = self.regr.body.to_json()\n        assert self.regr == self.client.update_registration(self.regr)\n        assert self.client.net.account is not None\n        assert self.client.net.post.call_count == 2\n        assert DIRECTORY_V2.newAccount in self.net.post.call_args_list[0][0]\n\n        self.response.json.return_value = self.regr.body.update(\n            contact=()).to_json()\n\n    def test_external_account_required_true(self):\n        self.client.directory = messages.Directory({\n            'meta': messages.Directory.Meta(external_account_required=True)\n        })\n\n        assert self.client.external_account_required()\n\n    def test_external_account_required_false(self):\n        self.client.directory = messages.Directory({\n            'meta': messages.Directory.Meta(external_account_required=False)\n        })\n\n        assert not self.client.external_account_required()\n\n    def test_external_account_required_default(self):\n        assert not self.client.external_account_required()\n\n    def test_query_registration_client(self):\n        self.response.json.return_value = self.regr.body.to_json()\n        self.response.headers['Location'] = 'https://www.letsencrypt-demo.org/acme/reg/1'\n        assert self.regr == self.client.query_registration(self.regr)\n\n    def test_post_as_get(self):\n        with mock.patch('acme.client.ClientV2._authzr_from_response') as mock_client:\n            mock_client.return_value = self.authzr2\n\n            self.client.poll(self.authzr2)\n\n            self.client.net.post.assert_called_once_with(\n                self.authzr2.uri, None,\n                new_nonce_url='https://www.letsencrypt-demo.org/acme/new-nonce')\n            self.client.net.get.assert_not_called()\n\n    def test_retry_after_date(self):\n        self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'\n        assert datetime.datetime(1999, 12, 31, 23, 59, 59) == \\\n            self.client.retry_after(response=self.response, default=10)\n\n    @mock.patch('acme.client.datetime')\n    def test_retry_after_invalid(self, dt_mock):\n        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)\n        dt_mock.timedelta = datetime.timedelta\n\n        self.response.headers['Retry-After'] = 'foooo'\n        assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \\\n            self.client.retry_after(response=self.response, default=10)\n\n    @mock.patch('acme.client.datetime')\n    def test_retry_after_overflow(self, dt_mock):\n        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)\n        dt_mock.timedelta = datetime.timedelta\n        dt_mock.datetime.side_effect = datetime.datetime\n\n        self.response.headers['Retry-After'] = \"Tue, 116 Feb 2016 11:50:00 MST\"\n        assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \\\n            self.client.retry_after(response=self.response, default=10)\n\n    @mock.patch('acme.client.datetime')\n    def test_retry_after_seconds(self, dt_mock):\n        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)\n        dt_mock.timedelta = datetime.timedelta\n\n        self.response.headers['Retry-After'] = '50'\n        assert datetime.datetime(2015, 3, 27, 0, 0, 50) == \\\n            self.client.retry_after(response=self.response, default=10)\n\n    @mock.patch('acme.client.datetime')\n    def test_retry_after_missing(self, dt_mock):\n        dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)\n        dt_mock.timedelta = datetime.timedelta\n\n        assert datetime.datetime(2015, 3, 27, 0, 0, 10) == \\\n            self.client.retry_after(response=self.response, default=10)\n\n    def test_get_directory(self):\n        self.response.json.return_value = DIRECTORY_V2.to_json()\n        assert DIRECTORY_V2.to_partial_json() == \\\n            ClientV2.get_directory('https://example.com/dir', self.net).to_partial_json()\n\n    @mock.patch('acme.client.datetime')\n    def test_renewal_time_expired_cert(self, dt_mock):\n        utc_now = datetime.datetime(2026, 1, 1, tzinfo=datetime.timezone.utc)\n        dt_mock.datetime.now.return_value = utc_now\n\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),\n        )\n        cert = x509.load_pem_x509_certificate(cert_pem)\n\n        t, _ = self.client.renewal_time(cert_pem)\n        assert t == cert.not_valid_after_utc\n\n    @mock.patch('acme.client.datetime')\n    def test_renewal_time_no_renewal_info(self, dt_mock):\n        utc_now = datetime.datetime(2025, 3, 15, tzinfo=datetime.timezone.utc)\n        dt_mock.datetime.now.return_value = utc_now\n        # A directory with no 'renewalInfo' should result in None.\n        self.client.directory = messages.Directory({})\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),\n        )\n        t, _ = self.client.renewal_time(cert_pem)\n        assert t is None\n\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),\n        )\n        t, _ = self.client.renewal_time(cert_pem)\n        assert t is None\n\n    @mock.patch('acme.client.datetime')\n    def test_renewal_time_with_renewal_info(self, dt_mock):\n        from cryptography import x509\n        from acme.client import _renewal_info_path_component\n        utc_now = datetime.datetime(2025, 3, 15, tzinfo=datetime.timezone.utc)\n        dt_mock.datetime.now.return_value = utc_now\n        dt_mock.timedelta = datetime.timedelta\n\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),\n        )\n\n        self.client.directory = messages.Directory({\n            'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',\n        })\n\n        self.response.json.return_value = {\n            \"suggestedWindow\": {\n                \"start\": \"2025-03-14T01:01:01Z\",\n                \"end\": \"2025-03-14T01:01:01Z\",\n            },\n            \"message\": \"Keep those certs fresh\"\n        }\n        t, _ = self.client.renewal_time(cert_pem)\n        cert_parsed = x509.load_pem_x509_certificate(cert_pem)\n        ari_path_component = _renewal_info_path_component(cert_parsed)\n        self.net.get.assert_called_once_with(\"https://www.letsencrypt-demo.org/acme/renewal-info/\" +\n                                             ari_path_component,\n                                             content_type='application/json')\n        assert t == datetime.datetime(2025, 3, 14, 1, 1, 1, tzinfo=datetime.timezone.utc)\n\n        self.net.reset_mock()\n\n        self.response.json.return_value = {\n            \"suggestedWindow\": {\n                \"start\": \"2025-03-16T01:01:01Z\",\n                \"end\": \"2025-03-17T01:01:01Z\",\n            },\n            \"message\": \"Keep those certs fresh\"\n        }\n        t, _ = self.client.renewal_time(cert_pem)\n        self.net.get.assert_called_once_with(\"https://www.letsencrypt-demo.org/acme/renewal-info/\" +\n                                             ari_path_component,\n                                             content_type='application/json')\n        assert t >= datetime.datetime(2025, 3, 16, 1, 1, 1, tzinfo=datetime.timezone.utc)\n        assert t <= datetime.datetime(2025, 3, 17, 1, 1, 1, tzinfo=datetime.timezone.utc)\n\n    @mock.patch('acme.client.datetime')\n    def test_renewal_time_renewal_info_errors(self, dt_mock):\n        def now(tzinfo=None):\n            return datetime.datetime(2025, 3, 15, tzinfo=tzinfo)\n        dt_mock.datetime.now.side_effect = now\n        dt_mock.timedelta = datetime.timedelta\n        dt_mock.timezone = datetime.timezone\n\n        self.client.directory = messages.Directory({\n            'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',\n        })\n        # Failure to fetch the 'renewalInfo' URL should raise an ARIError with the exception raised\n        # by self.net.get as the __cause__ and a Retry-After 6 hours in the future\n        expected_cause = requests.exceptions.RequestException\n        expected_retry_after = now() + datetime.timedelta(seconds=6 * 60 * 60)\n        self.net.get.side_effect = expected_cause\n\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),\n        )\n        with pytest.raises(errors.ARIError) as exception_info:\n            self.client.renewal_time(cert_pem)\n        assert isinstance(exception_info.value.__cause__, expected_cause)\n        assert exception_info.value.retry_after == expected_retry_after\n\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 30, 00, 00, 00),\n        )\n        with pytest.raises(errors.ARIError) as exception_info:\n            self.client.renewal_time(cert_pem)\n        assert isinstance(exception_info.value.__cause__, expected_cause)\n        assert exception_info.value.retry_after == expected_retry_after\n\n    @mock.patch('acme.client.datetime')\n    def test_renewal_time_returns_retry_after(self, dt_mock):\n        def now(tzinfo=None):\n            return datetime.datetime(2025, 3, 15, tzinfo=tzinfo)\n        dt_mock.datetime.now.side_effect = now\n        dt_mock.timedelta = datetime.timedelta\n        dt_mock.timezone = datetime.timezone\n\n        self.client.directory = messages.Directory({\n            'renewalInfo': 'https://www.letsencrypt-demo.org/acme/renewal-info',\n        })\n        cert_pem = make_cert_for_renewal(\n            not_before=datetime.datetime(2025, 3, 12, 00, 00, 00),\n            not_after=datetime.datetime(2025, 3, 20, 00, 00, 00),\n        )\n        self.response.json.return_value = {\n            \"suggestedWindow\": {\n                \"start\": \"2025-03-14T01:01:01Z\",\n                \"end\": \"2025-03-14T01:01:01Z\",\n            },\n            \"message\": \"Keep those certs fresh\"\n        }\n\n        # With no explicit Retry-After in header, default to six hours\n        _, retry_after = self.client.renewal_time(cert_pem)\n        assert retry_after == datetime.datetime(2025, 3, 15, 6, 0, 0)\n\n        # With an explicit Retry-After in header, use that\n        self.response.headers['Retry-After'] = '100'\n        _, retry_after = self.client.renewal_time(cert_pem)\n        assert retry_after == datetime.datetime(2025, 3, 15, 00, 1, 40)\n\ndef test_renewal_info_path_component():\n    from cryptography import x509\n    from acme.client import _renewal_info_path_component\n\n    cert = x509.load_pem_x509_certificate(test_util.load_vector('rsa2048_cert.pem'))\n\n    assert _renewal_info_path_component(cert) == \"fL5sRirC8VS5AtOQh9DfoAzYNCI.ALVG_VbBb5U7\"\n\n    # From https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html appendix A.\n    ARI_TEST_CERT = b\"\"\"\n-----BEGIN CERTIFICATE-----\nMIIBQzCB66ADAgECAgUAh2VDITAKBggqhkjOPQQDAjAVMRMwEQYDVQQDEwpFeGFt\ncGxlIENBMCIYDzAwMDEwMTAxMDAwMDAwWhgPMDAwMTAxMDEwMDAwMDBaMBYxFDAS\nBgNVBAMTC2V4YW1wbGUuY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEeBZu\n7cbpAYNXZLbbh8rNIzuOoqOOtmxA1v7cRm//AwyMwWxyHz4zfwmBhcSrf47NUAFf\nqzLQ2PPQxdTXREYEnKMjMCEwHwYDVR0jBBgwFoAUaYhba4dGQEHhs3uEe6CuLN4B\nyNQwCgYIKoZIzj0EAwIDRwAwRAIge09+S5TZAlw5tgtiVvuERV6cT4mfutXIlwTb\n+FYN/8oCIClDsqBklhB9KAelFiYt9+6FDj3z4KGVelYM5MdsO3pK\n-----END CERTIFICATE-----\n\"\"\"\n\n    cert = x509.load_pem_x509_certificate(ARI_TEST_CERT)\n    assert _renewal_info_path_component(cert) == \"aYhba4dGQEHhs3uEe6CuLN4ByNQ.AIdlQyE\"\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n\ndef make_cert_for_renewal(not_before, not_after) -> bytes:\n    \"\"\"\n    Return a PEM-encoded, self-signed certificate with the given dates.\n    \"\"\"\n    from cryptography import x509\n    from cryptography.hazmat.primitives.asymmetric import ec\n    from cryptography.hazmat.primitives import serialization, hashes\n    # AKID and serial are the inputs to constructing the renewalInfo URL\n    akid = x509.AuthorityKeyIdentifier(b\"1234\", None, None)\n    serial = 56789\n    key = ec.generate_private_key(ec.SECP256R1())\n    cert = x509.CertificateBuilder(\n        issuer_name=x509.Name([x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, \"Some Issuer\")]),\n        subject_name=x509.Name([]),\n        public_key=key.public_key(),\n        serial_number=serial,\n        not_valid_before=not_before,\n        not_valid_after=not_after,\n    ).add_extension(\n        x509.SubjectAlternativeName([x509.DNSName('example.com')]),\n        critical=False,\n    ).add_extension(\n        akid,\n        critical=False,\n    ).sign(\n        private_key=key,\n        algorithm=hashes.SHA256(),\n    )\n    return cert.public_bytes(serialization.Encoding.PEM)\n\nclass MockJSONDeSerializable(jose.JSONDeSerializable):\n    # pylint: disable=missing-docstring\n    def __init__(self, value):\n        self.value = value\n\n    def to_partial_json(self):\n        return {'foo': self.value}\n\n    @classmethod\n    def from_json(cls, jobj):\n        pass  # pragma: no cover\n\n\nclass ClientNetworkTest(unittest.TestCase):\n    \"\"\"Tests for acme.client.ClientNetwork.\"\"\"\n\n    def setUp(self):\n        self.verify_ssl = mock.MagicMock()\n        self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped)\n\n        self.net = ClientNetwork(\n            key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl,\n            user_agent='acme-python-test')\n\n        self.response = mock.MagicMock(ok=True, status_code=http_client.OK)\n        self.response.headers = {}\n        self.response.links = {}\n\n    def test_init(self):\n        assert self.net.verify_ssl is self.verify_ssl\n\n    def test_wrap_in_jws(self):\n        # pylint: disable=protected-access\n        jws_dump = self.net._wrap_in_jws(\n            MockJSONDeSerializable('foo'), nonce=b'Tg', url=\"url\")\n        jws = acme_jws.JWS.json_loads(jws_dump)\n        assert json.loads(jws.payload.decode()) == {'foo': 'foo'}\n        assert jws.signature.combined.nonce == b'Tg'\n\n    def test_wrap_in_jws_v2(self):\n        self.net.account = {'uri': 'acct-uri'}\n        # pylint: disable=protected-access\n        jws_dump = self.net._wrap_in_jws(\n            MockJSONDeSerializable('foo'), nonce=b'Tg', url=\"url\")\n        jws = acme_jws.JWS.json_loads(jws_dump)\n        assert json.loads(jws.payload.decode()) == {'foo': 'foo'}\n        assert jws.signature.combined.nonce == b'Tg'\n        assert jws.signature.combined.kid == u'acct-uri'\n        assert jws.signature.combined.url == u'url'\n\n    def test_check_response_not_ok_jobj_no_error(self):\n        self.response.ok = False\n        self.response.json.return_value = {}\n        with mock.patch('acme.client.messages.Error.from_json') as from_json:\n            from_json.side_effect = jose.DeserializationError\n            # pylint: disable=protected-access\n            with pytest.raises(errors.ClientError):\n                self.net._check_response(self.response)\n\n    def test_check_response_not_ok_jobj_error(self):\n        self.response.ok = False\n        self.response.json.return_value = messages.Error.with_code(\n            'serverInternal', detail='foo', title='some title').to_json()\n        # pylint: disable=protected-access\n        with pytest.raises(messages.Error):\n            self.net._check_response(self.response)\n\n    def test_check_response_not_ok_no_jobj(self):\n        self.response.ok = False\n        self.response.json.side_effect = ValueError\n        # pylint: disable=protected-access\n        with pytest.raises(errors.ClientError):\n            self.net._check_response(self.response)\n\n    def test_check_response_ok_no_jobj_ct_required(self):\n        self.response.json.side_effect = ValueError\n        for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:\n            self.response.headers['Content-Type'] = response_ct\n            # pylint: disable=protected-access\n            with pytest.raises(errors.ClientError):\n                self.net._check_response(self.response,\n                content_type=self.net.JSON_CONTENT_TYPE)\n\n    def test_check_response_ok_no_jobj_no_ct(self):\n        self.response.json.side_effect = ValueError\n        for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:\n            self.response.headers['Content-Type'] = response_ct\n            # pylint: disable=protected-access\n            assert self.response == self.net._check_response(self.response)\n\n    @mock.patch('acme.client.logger')\n    def test_check_response_ok_ct_with_charset(self, mock_logger):\n        self.response.json.return_value = {}\n        self.response.headers['Content-Type'] = 'application/json; charset=utf-8'\n        # pylint: disable=protected-access\n        assert self.response == self.net._check_response(\n            self.response, content_type='application/json')\n        try:\n            mock_logger.debug.assert_called_with(\n                'Ignoring wrong Content-Type (%r) for JSON decodable response',\n                'application/json; charset=utf-8'\n            )\n        except AssertionError:\n            return\n        raise AssertionError('Expected Content-Type warning ' #pragma: no cover\n            'to not have been logged')\n\n    @mock.patch('acme.client.logger')\n    def test_check_response_ok_bad_ct(self, mock_logger):\n        self.response.json.return_value = {}\n        self.response.headers['Content-Type'] = 'text/plain'\n        # pylint: disable=protected-access\n        assert self.response == self.net._check_response(\n            self.response, content_type='application/json')\n        mock_logger.debug.assert_called_with(\n            'Ignoring wrong Content-Type (%r) for JSON decodable response',\n            'text/plain'\n        )\n\n    def test_check_response_conflict(self):\n        self.response.ok = False\n        self.response.status_code = 409\n        # pylint: disable=protected-access\n        with pytest.raises(errors.ConflictError):\n            self.net._check_response(self.response)\n\n    def test_check_response_jobj(self):\n        self.response.json.return_value = {}\n        for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']:\n            self.response.headers['Content-Type'] = response_ct\n            # pylint: disable=protected-access\n            assert self.response == self.net._check_response(self.response)\n\n    def test_send_request(self):\n        self.net.session = mock.MagicMock()\n        self.net.session.request.return_value = self.response\n        # pylint: disable=protected-access\n        assert self.response == self.net._send_request(\n            'HEAD', 'http://example.com/', 'foo', bar='baz')\n        self.net.session.request.assert_called_once_with(\n            'HEAD', 'http://example.com/', 'foo',\n            headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, bar='baz')\n\n    @mock.patch('acme.client.logger')\n    def test_send_request_get_der(self, mock_logger):\n        self.net.session = mock.MagicMock()\n        self.net.session.request.return_value = mock.MagicMock(\n            ok=True, status_code=http_client.OK,\n            content=b\"hi\")\n        # pylint: disable=protected-access\n        self.net._send_request('HEAD', 'http://example.com/', 'foo',\n          timeout=mock.ANY, bar='baz', headers={'Accept': 'application/pkix-cert'})\n        mock_logger.debug.assert_called_with(\n            'Received response:\\nHTTP %d\\n%s\\n\\n%s', 200,\n            '', b'aGk=')\n\n    def test_send_request_post(self):\n        self.net.session = mock.MagicMock()\n        self.net.session.request.return_value = self.response\n        # pylint: disable=protected-access\n        assert self.response == self.net._send_request(\n            'POST', 'http://example.com/', 'foo', data='qux', bar='baz')\n        self.net.session.request.assert_called_once_with(\n            'POST', 'http://example.com/', 'foo',\n            headers=mock.ANY, verify=mock.ANY, timeout=mock.ANY, data='qux', bar='baz')\n\n    def test_send_request_verify_ssl(self):\n        # pylint: disable=protected-access\n        for verify in True, False:\n            self.net.session = mock.MagicMock()\n            self.net.session.request.return_value = self.response\n            self.net.verify_ssl = verify\n            # pylint: disable=protected-access\n            assert self.response == \\\n                self.net._send_request('GET', 'http://example.com/')\n            self.net.session.request.assert_called_once_with(\n                'GET', 'http://example.com/', verify=verify,\n                timeout=mock.ANY, headers=mock.ANY)\n\n    def test_send_request_user_agent(self):\n        self.net.session = mock.MagicMock()\n        # pylint: disable=protected-access\n        self.net._send_request('GET', 'http://example.com/',\n                               headers={'bar': 'baz'})\n        self.net.session.request.assert_called_once_with(\n            'GET', 'http://example.com/', verify=mock.ANY,\n            timeout=mock.ANY,\n            headers={'User-Agent': 'acme-python-test', 'bar': 'baz'})\n\n        self.net._send_request('GET', 'http://example.com/',\n                               headers={'User-Agent': 'foo2'})\n        self.net.session.request.assert_called_with(\n            'GET', 'http://example.com/',\n            verify=mock.ANY, timeout=mock.ANY, headers={'User-Agent': 'foo2'})\n\n    def test_send_request_timeout(self):\n        self.net.session = mock.MagicMock()\n        # pylint: disable=protected-access\n        self.net._send_request('GET', 'http://example.com/',\n                               headers={'bar': 'baz'})\n        self.net.session.request.assert_called_once_with(\n            mock.ANY, mock.ANY, verify=mock.ANY, headers=mock.ANY,\n            timeout=45)\n\n    def test_del(self, close_exception=None):\n        sess = mock.MagicMock()\n\n        if close_exception is not None:\n            sess.close.side_effect = close_exception\n\n        self.net.session = sess\n        del self.net\n        sess.close.assert_called_once_with()\n\n    def test_del_error(self):\n        self.test_del(ReferenceError)\n\n    @mock.patch('acme.client.requests')\n    def test_requests_error_passthrough(self, mock_requests):\n        mock_requests.exceptions = requests.exceptions\n        mock_requests.request.side_effect = requests.exceptions.RequestException\n        # pylint: disable=protected-access\n        with pytest.raises(requests.exceptions.RequestException):\n            self.net._send_request('GET', 'uri')\n\n\nclass ClientNetworkWithMockedResponseTest(unittest.TestCase):\n    \"\"\"Tests for acme.client.ClientNetwork which mock out response.\"\"\"\n\n    def setUp(self):\n        self.net = ClientNetwork(key='fake', alg=None)\n\n        self.response = mock.MagicMock(ok=True, status_code=http_client.OK)\n        self.response.headers = {}\n        self.response.links = {}\n        self.response.checked = False\n        self.acmev1_nonce_response = mock.MagicMock(\n            ok=False, status_code=http_client.METHOD_NOT_ALLOWED)\n        self.acmev1_nonce_response.headers = {}\n        self.obj = mock.MagicMock()\n        self.wrapped_obj = mock.MagicMock()\n        self.content_type = mock.sentinel.content_type\n\n        self.all_nonces = [\n            jose.b64encode(b'Nonce'),\n            jose.b64encode(b'Nonce2'), jose.b64encode(b'Nonce3')]\n        self.available_nonces = self.all_nonces[:]\n\n        def send_request(*args, **kwargs):\n            # pylint: disable=unused-argument,missing-docstring\n            assert \"new_nonce_url\" not in kwargs\n            method = args[0]\n            uri = args[1]\n            if method == 'HEAD' and uri != \"new_nonce_uri\":\n                response = self.acmev1_nonce_response\n            else:\n                response = self.response\n\n            if self.available_nonces:\n                response.headers = {\n                    self.net.REPLAY_NONCE_HEADER:\n                    self.available_nonces.pop().decode()}\n            else:\n                response.headers = {}\n            return response\n\n        # pylint: disable=protected-access\n        self.net._send_request = self.send_request = mock.MagicMock(\n            side_effect=send_request)\n        self.net._check_response = self.check_response\n        self.net._wrap_in_jws = mock.MagicMock(return_value=self.wrapped_obj)\n\n    def check_response(self, response, content_type):\n        # pylint: disable=missing-docstring\n        assert self.response == response\n        assert self.content_type == content_type\n        assert self.response.ok\n        self.response.checked = True\n        return self.response\n\n    def test_head(self):\n        assert self.acmev1_nonce_response == self.net.head(\n            'http://example.com/', 'foo', bar='baz')\n        self.send_request.assert_called_once_with(\n            'HEAD', 'http://example.com/', 'foo', bar='baz')\n\n    def test_head_v2(self):\n        assert self.response == self.net.head(\n            'new_nonce_uri', 'foo', bar='baz')\n        self.send_request.assert_called_once_with(\n            'HEAD', 'new_nonce_uri', 'foo', bar='baz')\n\n    def test_get(self):\n        assert self.response == self.net.get(\n            'http://example.com/', content_type=self.content_type, bar='baz')\n        assert self.response.checked\n        self.send_request.assert_called_once_with(\n            'GET', 'http://example.com/', bar='baz')\n\n    def test_post_no_content_type(self):\n        self.content_type = self.net.JOSE_CONTENT_TYPE\n        assert self.response == self.net.post('uri', self.obj)\n        assert self.response.checked\n\n    def test_post(self):\n        assert self.response == self.net.post(\n            'uri', self.obj, content_type=self.content_type)\n        assert self.response.checked\n        self.net._wrap_in_jws.assert_called_once_with(\n            self.obj, jose.b64decode(self.all_nonces.pop()), \"uri\")\n\n        self.available_nonces = []\n        with pytest.raises(errors.MissingNonce):\n            self.net.post('uri', self.obj, content_type=self.content_type)\n        self.net._wrap_in_jws.assert_called_with(\n            self.obj, jose.b64decode(self.all_nonces.pop()), \"uri\")\n\n    def test_post_wrong_initial_nonce(self):  # HEAD\n        self.available_nonces = [b'f', jose.b64encode(b'good')]\n        with pytest.raises(errors.BadNonce):\n            self.net.post('uri',\n                          self.obj, content_type=self.content_type)\n\n    def test_post_wrong_post_response_nonce(self):\n        self.available_nonces = [jose.b64encode(b'good'), b'f']\n        with pytest.raises(errors.BadNonce):\n            self.net.post('uri',\n                          self.obj, content_type=self.content_type)\n\n    def test_post_failed_retry(self):\n        check_response = mock.MagicMock()\n        check_response.side_effect = messages.Error.with_code('badNonce')\n\n        self.net._check_response = check_response\n        with pytest.raises(messages.Error):\n            self.net.post('uri',\n                          self.obj, content_type=self.content_type)\n\n    def test_post_not_retried(self):\n        check_response = mock.MagicMock()\n        check_response.side_effect = [messages.Error.with_code('malformed'),\n                                      self.response]\n\n        self.net._check_response = check_response\n        with pytest.raises(messages.Error):\n            self.net.post('uri',\n                          self.obj, content_type=self.content_type)\n\n    def test_post_successful_retry(self):\n        post_once = mock.MagicMock()\n        post_once.side_effect = [messages.Error.with_code('badNonce'),\n                                      self.response]\n\n        assert self.response == self.net.post(\n            'uri', self.obj, content_type=self.content_type)\n\n    def test_head_get_post_error_passthrough(self):\n        self.send_request.side_effect = requests.exceptions.RequestException\n        for method in self.net.head, self.net.get:\n            with pytest.raises(requests.exceptions.RequestException):\n                method('GET', 'uri')\n        with pytest.raises(requests.exceptions.RequestException):\n            self.net.post('uri', obj=self.obj)\n\n    def test_post_bad_nonce_head(self):\n        # pylint: disable=protected-access\n        # regression test for https://github.com/certbot/certbot/issues/6092\n        bad_response = mock.MagicMock(ok=False, status_code=http_client.SERVICE_UNAVAILABLE)\n        self.net._send_request = mock.MagicMock()\n        self.net._send_request.return_value = bad_response\n        self.content_type = None\n        check_response = mock.MagicMock()\n        self.net._check_response = check_response\n        with pytest.raises(errors.ClientError):\n            self.net.post('uri',\n                          self.obj, content_type=self.content_type,\n                          new_nonce_url='new_nonce_uri')\n        assert check_response.call_count == 1\n\n    def test_new_nonce_uri_removed(self):\n        self.content_type = None\n        self.net.post('uri', self.obj, content_type=None, new_nonce_url='new_nonce_uri')\n\n    def test_no_key_error(self):\n        \"A ClientNetwork with no key should error on POST but succeed on GET\"\n        self.net = ClientNetwork()\n        self.net._send_request = mock.MagicMock()\n        self.net._send_request.return_value = self.response\n        with pytest.raises(errors.Error):\n            self.net.post('uri', \"body\")\n        assert self.response == self.net.get(\n            'uri', content_type=self.content_type, bar='baz')\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/conftest.py",
    "content": "from unittest import mock\n\nimport pytest\n\n# This avoids a bug on mac where getfqdn errors after a long timeout.\n# See https://bugs.python.org/issue35164\n# and discussion at https://github.com/certbot/certbot/pull/10408\n@pytest.fixture(autouse=True)\ndef mock_getfqdn():\n    with mock.patch(\"socket.getfqdn\", return_value=\"server_name\") as mocked:\n        yield mocked\n\n@pytest.fixture(autouse=True)\ndef mock_sleep():\n    with mock.patch(\"time.sleep\") as mocked:\n        yield mocked\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/crypto_util_test.py",
    "content": "\"\"\"Tests for acme.crypto_util.\"\"\"\nimport ipaddress\nimport itertools\nimport sys\nimport unittest\nfrom unittest import mock\nimport warnings\n\nimport pytest\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import rsa, x25519\n\nfrom acme._internal.tests import test_util\n\n\nclass FormatTest(unittest.TestCase):\n    def test_to_cryptography_encoding(self):\n        with pytest.warns(DeprecationWarning, match='Format is deprecated'):\n            from acme.crypto_util import Format\n            assert Format.DER.to_cryptography_encoding() == serialization.Encoding.DER\n            assert Format.PEM.to_cryptography_encoding() == serialization.Encoding.PEM\n\n\nclass MiscTests(unittest.TestCase):\n\n    def test_dump_cryptography_chain(self):\n        from acme.crypto_util import dump_cryptography_chain\n\n        cert1 = test_util.load_cert('rsa2048_cert.pem')\n        cert2 = test_util.load_cert('rsa4096_cert.pem')\n\n        chain = [cert1, cert2]\n        dumped = dump_cryptography_chain(chain)\n\n        # default is PEM encoding Encoding.PEM\n        assert isinstance(dumped, bytes)\n\n\nclass CryptographyCertOrReqSANTest(unittest.TestCase):\n    \"\"\"Test for acme.crypto_util._cryptography_cert_or_req_san.\"\"\"\n\n    @classmethod\n    def _call(cls, loader, name):\n        # pylint: disable=protected-access\n        from acme.crypto_util import _cryptography_cert_or_req_san\n        return _cryptography_cert_or_req_san(loader(name))\n\n    @classmethod\n    def _get_idn_names(cls):\n        \"\"\"Returns expected names from '{cert,csr}-idnsans.pem'.\"\"\"\n        chars = [chr(i) for i in itertools.chain(range(0x3c3, 0x400),\n                                                 range(0x641, 0x6fc),\n                                                 range(0x1820, 0x1877))]\n        return [''.join(chars[i: i + 45]) + '.invalid'\n                for i in range(0, len(chars), 45)]\n\n    def _call_cert(self, name):\n        return self._call(test_util.load_cert, name)\n\n    def _call_csr(self, name):\n        return self._call(test_util.load_csr, name)\n\n    def test_cert_no_sans(self):\n        assert self._call_cert('cert.pem') == []\n\n    def test_cert_two_sans(self):\n        assert self._call_cert('cert-san.pem') == \\\n                         ['example.com', 'www.example.com']\n\n    def test_cert_hundred_sans(self):\n        assert self._call_cert('cert-100sans.pem') == \\\n                         ['example{0}.com'.format(i) for i in range(1, 101)]\n\n    def test_cert_idn_sans(self):\n        assert self._call_cert('cert-idnsans.pem') == \\\n                         self._get_idn_names()\n\n    def test_csr_no_sans(self):\n        assert self._call_csr('csr-nosans.pem') == []\n\n    def test_csr_one_san(self):\n        assert self._call_csr('csr.pem') == ['example.com']\n\n    def test_csr_two_sans(self):\n        assert self._call_csr('csr-san.pem') == \\\n                         ['example.com', 'www.example.com']\n\n    def test_csr_six_sans(self):\n        assert self._call_csr('csr-6sans.pem') == \\\n                         ['example.com', 'example.org', 'example.net',\n                          'example.info', 'subdomain.example.com',\n                          'other.subdomain.example.com']\n\n    def test_csr_hundred_sans(self):\n        assert self._call_csr('csr-100sans.pem') == \\\n                         ['example{0}.com'.format(i) for i in range(1, 101)]\n\n    def test_csr_idn_sans(self):\n        assert self._call_csr('csr-idnsans.pem') == \\\n                         self._get_idn_names()\n\n    def test_critical_san(self):\n        assert self._call_cert('critical-san.pem') == \\\n                         ['chicago-cubs.venafi.example', 'cubs.venafi.example']\n\n\nclass GenMakeSelfSignedCertTest(unittest.TestCase):\n    \"\"\"Test for make_self_signed_cert.\"\"\"\n\n    def setUp(self):\n        self.cert_count = 5\n        self.serial_num: list[int] = []\n        self.privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from acme.crypto_util import make_self_signed_cert\n        with pytest.warns(DeprecationWarning, match='make_self_signed_cert is deprecated'):\n            return make_self_signed_cert(*args, **kwargs)\n\n    def test_sn_collisions(self):\n        for _ in range(self.cert_count):\n            cert = self._call(self.privkey, ['dummy'], force_san=True,\n                              ips=[ipaddress.ip_address(\"10.10.10.10\")])\n            self.serial_num.append(cert.serial_number)\n        assert len(set(self.serial_num)) >= self.cert_count\n\n    def test_no_ips(self):\n        self._call(self.privkey, ['dummy'])\n\n    @mock.patch(\"acme.crypto_util._now\")\n    def test_expiry_times(self, mock_now):\n        from datetime import datetime, timedelta, timezone\n        not_before = 1736200830\n        validity = 100\n\n        not_before_dt = datetime.fromtimestamp(not_before)\n        validity_td = timedelta(validity)\n        not_after_dt = not_before_dt + validity_td\n        cert = self._call(\n            self.privkey,\n            ['dummy'],\n            not_before=not_before_dt,\n            validity=validity_td,\n        )\n        # TODO: This should be `not_valid_before_utc` once we raise the minimum\n        # cryptography version.\n        # https://github.com/certbot/certbot/issues/10105\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\n                'ignore',\n                message='Properties that return.*datetime object'\n            )\n            self.assertEqual(cert.not_valid_before, not_before_dt)\n            self.assertEqual(cert.not_valid_after, not_after_dt)\n\n        now = not_before + 1\n        now_dt = datetime.fromtimestamp(now)\n        mock_now.return_value = now_dt.replace(tzinfo=timezone.utc)\n        valid_after_now_dt = now_dt + validity_td\n        cert = self._call(\n            self.privkey,\n            ['dummy'],\n            validity=validity_td,\n        )\n        with warnings.catch_warnings():\n            warnings.filterwarnings(\n                'ignore',\n                message='Properties that return.*datetime object'\n            )\n            self.assertEqual(cert.not_valid_before, now_dt)\n            self.assertEqual(cert.not_valid_after, valid_after_now_dt)\n\n    def test_no_name(self):\n        with pytest.raises(AssertionError):\n            self._call(self.privkey, ips=[ipaddress.ip_address(\"1.1.1.1\")])\n            self._call(self.privkey)\n\n    def test_extensions(self):\n        extension_type = x509.TLSFeature([x509.TLSFeatureType.status_request])\n        extension = x509.Extension(\n            x509.TLSFeature.oid,\n            False,\n            extension_type\n        )\n        cert = self._call(\n            self.privkey,\n            ips=[ipaddress.ip_address(\"1.1.1.1\")],\n            extensions=[extension]\n        )\n        self.assertIn(extension, cert.extensions)\n\n\nclass MakeCSRTest(unittest.TestCase):\n    \"\"\"Test for standalone functions.\"\"\"\n\n    @classmethod\n    def _call_with_key(cls, *args, **kwargs):\n        privkey = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n        privkey_pem = privkey.private_bytes(\n            serialization.Encoding.PEM,\n            serialization.PrivateFormat.PKCS8,\n            serialization.NoEncryption(),\n        )\n        from acme.crypto_util import make_csr\n\n        return make_csr(privkey_pem, *args, **kwargs)\n\n    def test_make_csr(self):\n        csr_pem = self._call_with_key([\"a.example\", \"b.example\"])\n        assert b\"--BEGIN CERTIFICATE REQUEST--\" in csr_pem\n        assert b\"--END CERTIFICATE REQUEST--\" in csr_pem\n        csr = x509.load_pem_x509_csr(csr_pem)\n\n        assert len(csr.extensions) == 1\n        assert list(\n            csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value\n        ) == [\n            x509.DNSName(\"a.example\"),\n            x509.DNSName(\"b.example\"),\n        ]\n\n    def test_make_csr_ip(self):\n        csr_pem = self._call_with_key(\n            [\"a.example\"],\n            False,\n            [ipaddress.ip_address(\"127.0.0.1\"), ipaddress.ip_address(\"::1\")],\n        )\n        assert b\"--BEGIN CERTIFICATE REQUEST--\" in csr_pem\n        assert b\"--END CERTIFICATE REQUEST--\" in csr_pem\n\n        csr = x509.load_pem_x509_csr(csr_pem)\n\n        assert len(csr.extensions) == 1\n        assert list(\n            csr.extensions.get_extension_for_class(x509.SubjectAlternativeName).value\n        ) == [\n            x509.DNSName(\"a.example\"),\n            x509.IPAddress(ipaddress.ip_address(\"127.0.0.1\")),\n            x509.IPAddress(ipaddress.ip_address(\"::1\")),\n        ]\n\n    def test_make_csr_must_staple(self):\n        csr_pem = self._call_with_key([\"a.example\"], must_staple=True)\n        csr = x509.load_pem_x509_csr(csr_pem)\n\n        assert len(csr.extensions) == 2\n        assert list(csr.extensions.get_extension_for_class(x509.TLSFeature).value) == [\n            x509.TLSFeatureType.status_request\n        ]\n\n    def test_make_csr_without_hostname(self):\n        with pytest.raises(ValueError):\n            self._call_with_key()\n\n    def test_make_csr_invalid_key_type(self):\n        privkey = x25519.X25519PrivateKey.generate()\n        privkey_pem = privkey.private_bytes(\n            serialization.Encoding.PEM,\n            serialization.PrivateFormat.PKCS8,\n            serialization.NoEncryption(),\n        )\n        from acme.crypto_util import make_csr\n\n        with pytest.raises(ValueError):\n            make_csr(privkey_pem, [\"a.example\"])\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/errors_test.py",
    "content": "\"\"\"Tests for acme.errors.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\n\nclass BadNonceTest(unittest.TestCase):\n    \"\"\"Tests for acme.errors.BadNonce.\"\"\"\n\n    def setUp(self):\n        from acme.errors import BadNonce\n        self.error = BadNonce(nonce=\"xxx\", error=\"error\")\n\n    def test_str(self):\n        assert \"Invalid nonce ('xxx'): error\" == str(self.error)\n\n\nclass MissingNonceTest(unittest.TestCase):\n    \"\"\"Tests for acme.errors.MissingNonce.\"\"\"\n\n    def setUp(self):\n        from acme.errors import MissingNonce\n        self.response = mock.MagicMock(headers={})\n        self.response.request.method = 'FOO'\n        self.error = MissingNonce(self.response)\n\n    def test_str(self):\n        assert \"FOO\" in str(self.error)\n        assert \"{}\" in str(self.error)\n\n\nclass PollErrorTest(unittest.TestCase):\n    \"\"\"Tests for acme.errors.PollError.\"\"\"\n\n    def setUp(self):\n        from acme.errors import PollError\n        self.timeout = PollError(\n            exhausted={mock.sentinel.AR},\n            updated={})\n        self.invalid = PollError(exhausted=set(), updated={\n            mock.sentinel.AR: mock.sentinel.AR2})\n\n    def test_timeout(self):\n        assert self.timeout.timeout\n        assert not self.invalid.timeout\n\n    def test_repr(self):\n        assert 'PollError(exhausted=%s, updated={sentinel.AR: ' \\\n                         'sentinel.AR2})' % repr(set()) == repr(self.invalid)\n\n\nclass ValidationErrorTest(unittest.TestCase):\n    \"\"\"Tests for acme.errors.ValidationError\"\"\"\n\n    def setUp(self):\n        from acme.errors import ValidationError\n        from acme.challenges import DNS01\n        from acme.messages import Error\n        from acme.messages import Authorization\n        from acme.messages import AuthorizationResource\n        from acme.messages import IDENTIFIER_FQDN\n        from acme.messages import ChallengeBody\n        from acme.messages import Identifier\n        self.challenge_error = Error(typ='custom', detail='bar')\n        failed_authzr = AuthorizationResource(\n            body=Authorization(\n                identifier=Identifier(typ=IDENTIFIER_FQDN, value=\"example.com\"),\n                challenges=[ChallengeBody(\n                    chall=DNS01(),\n                    error=self.challenge_error,\n                )]\n            )\n        )\n        self.error = ValidationError([failed_authzr])\n\n    def test_repr(self):\n        err_message = str(self.error)\n        assert 'Authorization for example.com failed' in err_message\n        assert 'Challenge dns-01 failed' in err_message\n        assert str(self.challenge_error) in err_message\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/fields_test.py",
    "content": "\"\"\"Tests for acme.fields.\"\"\"\nimport datetime\nimport sys\nimport unittest\n\nimport josepy as jose\nimport pytest\n\n\nclass FixedTest(unittest.TestCase):\n    \"\"\"Tests for acme.fields.Fixed.\"\"\"\n\n    def setUp(self):\n        from acme.fields import fixed\n        self.field = fixed('name', 'x')\n\n    def test_decode(self):\n        assert 'x' == self.field.decode('x')\n\n    def test_decode_bad(self):\n        with pytest.raises(jose.DeserializationError):\n            self.field.decode('y')\n\n    def test_encode(self):\n        assert 'x' == self.field.encode('x')\n\n    def test_encode_override(self):\n        assert 'y' == self.field.encode('y')\n\n\nclass RFC3339FieldTest(unittest.TestCase):\n    \"\"\"Tests for acme.fields.RFC3339Field.\"\"\"\n\n    def setUp(self):\n        self.decoded = datetime.datetime(2015, 3, 27, tzinfo=datetime.timezone.utc)\n        self.encoded = '2015-03-27T00:00:00Z'\n\n    def test_default_encoder(self):\n        from acme.fields import RFC3339Field\n        assert self.encoded == RFC3339Field.default_encoder(self.decoded)\n\n    def test_default_encoder_naive_fails(self):\n        from acme.fields import RFC3339Field\n        with pytest.raises(ValueError):\n            RFC3339Field.default_encoder(datetime.datetime.now())\n\n    def test_default_decoder(self):\n        from acme.fields import RFC3339Field\n        assert self.decoded == RFC3339Field.default_decoder(self.encoded)\n\n    def test_default_decoder_raises_deserialization_error(self):\n        from acme.fields import RFC3339Field\n        with pytest.raises(jose.DeserializationError):\n            RFC3339Field.default_decoder('')\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/jose_test.py",
    "content": "\"\"\"Tests for acme.jose shim.\"\"\"\nimport importlib\nimport sys\n\nimport pytest\n\n\ndef _test_it(submodule, attribute):\n    if submodule:\n        acme_jose_path = 'acme.jose.' + submodule\n        josepy_path = 'josepy.' + submodule\n    else:\n        acme_jose_path = 'acme.jose'\n        josepy_path = 'josepy'\n    acme_jose_mod = importlib.import_module(acme_jose_path)\n    josepy_mod = importlib.import_module(josepy_path)\n\n    assert acme_jose_mod is josepy_mod\n    assert getattr(acme_jose_mod, attribute) is getattr(josepy_mod, attribute)\n\n    # We use the imports below with eval, but pylint doesn't\n    # understand that.\n    import josepy  # pylint: disable=unused-import # noqa: F401\n\n    import acme  # pylint: disable=unused-import # noqa: F401\n    acme_jose_mod = eval(acme_jose_path)  # pylint: disable=eval-used\n    josepy_mod = eval(josepy_path)  # pylint: disable=eval-used\n    assert acme_jose_mod is josepy_mod\n    assert getattr(acme_jose_mod, attribute) is getattr(josepy_mod, attribute)\n\ndef test_top_level():\n    _test_it('', 'RS512')\n\ndef test_submodules():\n    # This test ensures that the modules in josepy that were\n    # available at the time it was moved into its own package are\n    # available under acme.jose. Backwards compatibility with new\n    # modules or testing code is not maintained.\n    mods_and_attrs = [('b64', 'b64decode',),\n                      ('errors', 'Error',),\n                      ('interfaces', 'JSONDeSerializable',),\n                      ('json_util', 'Field',),\n                      ('jwa', 'HS256',),\n                      ('jwk', 'JWK',),\n                      ('jws', 'JWS',),\n                      ('util', 'ImmutableMap',),]\n\n    for mod, attr in mods_and_attrs:\n        _test_it(mod, attr)\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/jws_test.py",
    "content": "\"\"\"Tests for acme.jws.\"\"\"\nimport sys\nimport unittest\n\nimport josepy as jose\nimport pytest\n\nfrom acme._internal.tests import test_util\n\nKEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))\n\n\nclass HeaderTest(unittest.TestCase):\n    \"\"\"Tests for acme.jws.Header.\"\"\"\n\n    good_nonce = jose.encode_b64jose(b'foo')\n    wrong_nonce = u'F'\n    # Following just makes sure wrong_nonce is wrong\n    try:\n        jose.b64decode(wrong_nonce)\n    except (ValueError, TypeError):\n        assert True\n    else:\n        pytest.fail(\"Exception from jose.b64decode wasn't raised\")  # pragma: no cover\n\n    def test_nonce_decoder(self):\n        from acme.jws import Header\n        nonce_field = Header._fields['nonce']\n\n        with pytest.raises(jose.DeserializationError):\n            nonce_field.decode(self.wrong_nonce)\n        assert b'foo' == nonce_field.decode(self.good_nonce)\n\n\nclass JWSTest(unittest.TestCase):\n    \"\"\"Tests for acme.jws.JWS.\"\"\"\n\n    def setUp(self):\n        self.privkey = KEY\n        self.pubkey = self.privkey.public_key()\n        self.nonce = jose.b64encode(b'Nonce')\n        self.url = 'hi'\n        self.kid = 'baaaaa'\n\n    def test_kid_serialize(self):\n        from acme.jws import JWS\n        jws = JWS.sign(payload=b'foo', key=self.privkey,\n                       alg=jose.RS256, nonce=self.nonce,\n                       url=self.url, kid=self.kid)\n        assert jws.signature.combined.nonce == self.nonce\n        assert jws.signature.combined.url == self.url\n        assert jws.signature.combined.kid == self.kid\n        assert jws.signature.combined.jwk is None\n        # TODO: check that nonce is in protected header\n\n        assert jws == JWS.from_json(jws.to_json())\n\n    def test_jwk_serialize(self):\n        from acme.jws import JWS\n        jws = JWS.sign(payload=b'foo', key=self.privkey,\n                       alg=jose.RS256, nonce=self.nonce,\n                       url=self.url)\n        assert jws.signature.combined.kid is None\n        assert jws.signature.combined.jwk == self.pubkey\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/messages_test.py",
    "content": "\"\"\"Tests for acme.messages.\"\"\"\nimport sys\nimport json\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges\nfrom acme._internal.tests import test_util\n\nCERT = test_util.load_cert('cert.der')\nCSR = test_util.load_csr('csr.der')\nKEY = test_util.load_rsa_private_key('rsa512_key.pem')\n\n\nclass ErrorTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.Error.\"\"\"\n\n    def setUp(self):\n        from acme.messages import Error\n        from acme.messages import ERROR_PREFIX\n        from acme.messages import Identifier\n        from acme.messages import IDENTIFIER_FQDN\n        self.error = Error.with_code('malformed', detail='foo', title='title')\n        self.jobj = {\n            'detail': 'foo',\n            'title': 'some title',\n            'type': ERROR_PREFIX + 'malformed',\n        }\n        self.error_custom = Error(typ='custom', detail='bar')\n        self.identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')\n        self.subproblem = Error.with_code('caa', detail='bar', title='title', identifier=self.identifier)\n        self.error_with_subproblems = Error.with_code('malformed', detail='foo', title='title', subproblems=[self.subproblem])\n        self.empty_error = Error()\n\n    def test_default_typ(self):\n        from acme.messages import Error\n        assert Error().typ == 'about:blank'\n\n    def test_from_json_empty(self):\n        from acme.messages import Error\n        assert Error() == Error.from_json('{}')\n\n    def test_from_json_hashable(self):\n        from acme.messages import Error\n        hash(Error.from_json(self.error.to_json()))\n\n    def test_from_json_with_subproblems(self):\n        from acme.messages import Error\n\n        parsed_error = Error.from_json(self.error_with_subproblems.to_json())\n\n        assert 1 == len(parsed_error.subproblems)\n        assert self.subproblem == parsed_error.subproblems[0]\n\n    def test_description(self):\n        assert 'The request message was malformed' == self.error.description\n        assert self.error_custom.description is None\n\n    def test_code(self):\n        from acme.messages import Error\n        assert 'malformed' == self.error.code\n        assert self.error_custom.code is None\n        assert Error().code is None\n\n    def test_is_acme_error(self):\n        from acme.messages import Error\n        from acme.messages import is_acme_error\n        assert is_acme_error(self.error)\n        assert not is_acme_error(self.error_custom)\n        assert not is_acme_error(Error())\n        assert not is_acme_error(self.empty_error)\n        assert not is_acme_error(\"must pet all the {dogs|rabbits}\")\n\n    def test_unicode_error(self):\n        from acme.messages import Error\n        from acme.messages import is_acme_error\n        arabic_error = Error.with_code(\n            'malformed', detail=u'\\u0639\\u062f\\u0627\\u0644\\u0629', title='title')\n        assert is_acme_error(arabic_error)\n\n    def test_with_code(self):\n        from acme.messages import Error\n        from acme.messages import is_acme_error\n        assert is_acme_error(Error.with_code('badCSR'))\n        with pytest.raises(ValueError):\n            Error.with_code('not an ACME error code')\n\n    def test_str(self):\n        assert str(self.error) == \\\n            u\"{0.typ} :: {0.description} :: {0.detail} :: {0.title}\" \\\n            .format(self.error)\n        assert str(self.error_with_subproblems) == \\\n            (u\"{0.typ} :: {0.description} :: {0.detail} :: {0.title}\\n\"+\n            u\"Problem for {1.identifier.value}: {1.typ} :: {1.description} :: {1.detail} :: {1.title}\").format(\n        self.error_with_subproblems, self.subproblem)\n\n    # this test is based on a minimal reproduction of a contextmanager/immutable\n    # exception related error: https://github.com/python/cpython/issues/99856\n    def test_setting_traceback(self):\n        assert self.error_custom.__traceback__ is None\n\n        try:\n            1/0\n        except ZeroDivisionError as e:\n            self.error_custom.__traceback__ = e.__traceback__\n\n        assert self.error_custom.__traceback__ is not None\n\n\nclass ConstantTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages._Constant.\"\"\"\n\n    def setUp(self):\n        from acme.messages import _Constant\n\n        class MockConstant(_Constant):  # pylint: disable=missing-docstring\n            POSSIBLE_NAMES: dict = {}\n\n        self.MockConstant = MockConstant  # pylint: disable=invalid-name\n        self.const_a = MockConstant('a')\n        self.const_b = MockConstant('b')\n\n    def test_to_partial_json(self):\n        assert 'a' == self.const_a.to_partial_json()\n        assert 'b' == self.const_b.to_partial_json()\n\n    def test_from_json(self):\n        assert self.const_a == self.MockConstant.from_json('a')\n        with pytest.raises(jose.DeserializationError):\n            self.MockConstant.from_json('c')\n\n    def test_from_json_hashable(self):\n        hash(self.MockConstant.from_json('a'))\n\n    def test_repr(self):\n        assert 'MockConstant(a)' == repr(self.const_a)\n        assert 'MockConstant(b)' == repr(self.const_b)\n\n    def test_equality(self):\n        const_a_prime = self.MockConstant('a')\n        assert self.const_a != self.const_b\n        assert self.const_a == const_a_prime\n\n        assert self.const_a != self.const_b\n        assert self.const_a == const_a_prime\n\n\nclass DirectoryTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.Directory.\"\"\"\n\n    def setUp(self):\n        from acme.messages import Directory\n        self.dir = Directory({\n            'newReg': 'reg',\n            'newCert': 'cert',\n            'meta': Directory.Meta(\n                terms_of_service='https://example.com/acme/terms',\n                website='https://www.example.com/',\n                caa_identities=['example.com'],\n                profiles={\n                    \"example\": \"some profile\",\n                    \"other example\": \"a different profile\"\n                }\n            ),\n        })\n\n    def test_init_wrong_key_value_success(self):  # pylint: disable=no-self-use\n        from acme.messages import Directory\n        Directory({'foo': 'bar'})\n\n    def test_getitem(self):\n        assert 'reg' == self.dir['newReg']\n\n    def test_getitem_fails_with_key_error(self):\n        with pytest.raises(KeyError):\n            self.dir.__getitem__('foo')\n\n    def test_getattr(self):\n        assert 'reg' == self.dir.newReg\n\n    def test_getattr_fails_with_attribute_error(self):\n        with pytest.raises(AttributeError):\n            self.dir.__getattr__('foo')\n\n    def test_to_json(self):\n        assert self.dir.to_json() == {\n            'newReg': 'reg',\n            'newCert': 'cert',\n            'meta': {\n                'termsOfService': 'https://example.com/acme/terms',\n                'website': 'https://www.example.com/',\n                'caaIdentities': ['example.com'],\n                'profiles': {\n                    'example': 'some profile',\n                    'other example': 'a different profile'\n                }\n            },\n        }\n\n    def test_from_json_deserialization_unknown_key_success(self):  # pylint: disable=no-self-use\n        from acme.messages import Directory\n        Directory.from_json({'foo': 'bar'})\n\n    def test_iter_meta(self):\n        result = False\n        for k in self.dir.meta:\n            if k == 'terms_of_service':\n                result = self.dir.meta[k] == 'https://example.com/acme/terms'\n        assert result\n\n\nclass ExternalAccountBindingTest(unittest.TestCase):\n    def setUp(self):\n        from acme.messages import Directory\n        self.key = jose.jwk.JWKRSA(key=KEY.public_key())\n        self.kid = \"kid-for-testing\"\n        self.hmac_key = \"hmac-key-for-testing\"\n        self.hmac_alg = \"HS256\"\n        self.dir = Directory({\n            'newAccount': 'http://url/acme/new-account',\n        })\n\n    def test_from_data(self):\n        from acme.messages import ExternalAccountBinding\n        eab = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir, self.hmac_alg)\n\n        assert len(eab) == 3\n        assert sorted(eab.keys()) == sorted(['protected', 'payload', 'signature'])\n\n    def test_from_data_invalid_hmac_alg(self):\n        from acme.messages import ExternalAccountBinding\n        invalid_alg = \"HS9999\"\n        with pytest.raises(ValueError) as info:\n            ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir, invalid_alg)\n\n        assert \"Invalid value for hmac_alg\" in str(info.value)\n\n    def test_from_data_default_hmac_alg(self):\n        from acme.messages import ExternalAccountBinding\n        eab_default = ExternalAccountBinding.from_data(self.key, self.kid, self.hmac_key, self.dir)\n\n        assert len(eab_default) == 3\n        assert sorted(eab_default.keys()) == sorted(['protected', 'payload', 'signature'])\n\n        eab_explicit = ExternalAccountBinding.from_data(\n            self.key, self.kid, self.hmac_key, self.dir, \"HS256\"\n        )\n\n        assert eab_default == eab_explicit\n\n        protected_default = json.loads(\n            jose.b64.b64decode(eab_default['protected']).decode()\n        )\n        assert protected_default['alg'] == 'HS256'\n\nclass RegistrationTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.Registration.\"\"\"\n\n    def setUp(self):\n        key = jose.jwk.JWKRSA(key=KEY.public_key())\n        contact = (\n            'mailto:admin@foo.com',\n            'tel:1234',\n        )\n        agreement = 'https://letsencrypt.org/terms'\n\n        from acme.messages import Registration\n        self.reg = Registration(key=key, contact=contact, agreement=agreement)\n        self.reg_none = Registration()\n\n        self.jobj_to = {\n            'contact': contact,\n            'agreement': agreement,\n            'key': key,\n        }\n        self.jobj_from = self.jobj_to.copy()\n        self.jobj_from['key'] = key.to_json()\n\n    def test_from_data(self):\n        from acme.messages import Registration\n        reg = Registration.from_data(phone='1234', email='admin@foo.com')\n        assert reg.contact == (\n            'tel:1234',\n            'mailto:admin@foo.com',\n        )\n\n    def test_new_registration_from_data_with_eab(self):\n        from acme.messages import Directory\n        from acme.messages import ExternalAccountBinding\n        from acme.messages import NewRegistration\n        key = jose.jwk.JWKRSA(key=KEY.public_key())\n        kid = \"kid-for-testing\"\n        hmac_key = \"hmac-key-for-testing\"\n        hmac_alg = \"HS256\"\n        directory = Directory({\n            'newAccount': 'http://url/acme/new-account',\n        })\n        eab = ExternalAccountBinding.from_data(key, kid, hmac_key, directory, hmac_alg)\n        reg = NewRegistration.from_data(email='admin@foo.com', external_account_binding=eab)\n        assert reg.contact == (\n            'mailto:admin@foo.com',\n        )\n        assert sorted(reg.external_account_binding.keys()) == \\\n                         sorted(['protected', 'payload', 'signature'])\n\n    def test_phones(self):\n        assert ('1234',) == self.reg.phones\n\n    def test_emails(self):\n        assert ('admin@foo.com',) == self.reg.emails\n\n    def test_to_partial_json(self):\n        assert self.jobj_to == self.reg.to_partial_json()\n\n    def test_from_json(self):\n        from acme.messages import Registration\n        assert self.reg == Registration.from_json(self.jobj_from)\n\n    def test_from_json_hashable(self):\n        from acme.messages import Registration\n        hash(Registration.from_json(self.jobj_from))\n\n    def test_default_not_transmitted(self):\n        from acme.messages import NewRegistration\n        empty_new_reg = NewRegistration()\n        new_reg_with_contact = NewRegistration(contact=())\n\n        assert empty_new_reg.contact == ()\n        assert new_reg_with_contact.contact == ()\n\n        assert 'contact' not in empty_new_reg.to_partial_json()\n        assert 'contact' not in empty_new_reg.fields_to_partial_json()\n        assert 'contact' in new_reg_with_contact.to_partial_json()\n        assert 'contact' in new_reg_with_contact.fields_to_partial_json()\n\n\nclass UpdateRegistrationTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.UpdateRegistration.\"\"\"\n\n    def test_empty(self):\n        from acme.messages import UpdateRegistration\n        jstring = '{\"resource\": \"reg\"}'\n        assert '{}' == UpdateRegistration().json_dumps()\n        assert UpdateRegistration() == UpdateRegistration.json_loads(jstring)\n\n\nclass RegistrationResourceTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.RegistrationResource.\"\"\"\n\n    def setUp(self):\n        from acme.messages import RegistrationResource\n        self.regr = RegistrationResource(\n            body=mock.sentinel.body, uri=mock.sentinel.uri,\n            terms_of_service=mock.sentinel.terms_of_service)\n\n    def test_to_partial_json(self):\n        assert self.regr.to_json() == {\n            'body': mock.sentinel.body,\n            'uri': mock.sentinel.uri,\n            'terms_of_service': mock.sentinel.terms_of_service,\n        }\n\n\nclass ChallengeResourceTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.ChallengeResource.\"\"\"\n\n    def test_uri(self):\n        from acme.messages import ChallengeResource\n        assert 'http://challb' == ChallengeResource(body=mock.MagicMock(\n            uri='http://challb'), authzr_uri='http://authz').uri\n\n\nclass ChallengeBodyTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.ChallengeBody.\"\"\"\n\n    def setUp(self):\n        self.chall = challenges.DNS(token=jose.b64decode(\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))\n\n        from acme.messages import ChallengeBody\n        from acme.messages import Error\n        from acme.messages import STATUS_INVALID\n        self.status = STATUS_INVALID\n        error = Error.with_code('serverInternal', detail='Unable to communicate with DNS server')\n        self.challb = ChallengeBody(\n            uri='http://challb', chall=self.chall, status=self.status,\n            error=error)\n\n        self.jobj_to = {\n            'url': 'http://challb',\n            'status': self.status,\n            'type': 'dns',\n            'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',\n            'error': error,\n        }\n        self.jobj_from = self.jobj_to.copy()\n        self.jobj_from['status'] = 'invalid'\n        self.jobj_from['error'] = {\n            'type': 'urn:ietf:params:acme:error:serverInternal',\n            'detail': 'Unable to communicate with DNS server',\n        }\n\n    def test_encode(self):\n        assert self.challb.encode('uri') == self.challb.uri\n\n    def test_to_partial_json(self):\n        assert self.jobj_to == self.challb.to_partial_json()\n\n    def test_from_json(self):\n        from acme.messages import ChallengeBody\n        assert self.challb == ChallengeBody.from_json(self.jobj_from)\n\n    def test_from_json_hashable(self):\n        from acme.messages import ChallengeBody\n        hash(ChallengeBody.from_json(self.jobj_from))\n\n    def test_proxy(self):\n        assert jose.b64decode(\n            'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') == self.challb.token\n\n\nclass AuthorizationTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.Authorization.\"\"\"\n\n    def setUp(self):\n        from acme.messages import ChallengeBody\n        from acme.messages import STATUS_VALID\n\n        self.challbs = (\n            ChallengeBody(\n                uri='http://challb1', status=STATUS_VALID,\n                chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')),\n            ChallengeBody(uri='http://challb2', status=STATUS_VALID,\n                          chall=challenges.DNS(\n                              token=b'DGyRejmCefe7v4NfDGDKfA')),\n        )\n\n        from acme.messages import Authorization\n        from acme.messages import Identifier\n        from acme.messages import IDENTIFIER_FQDN\n        identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')\n        self.authz = Authorization(\n            identifier=identifier, challenges=self.challbs)\n\n        self.jobj_from = {\n            'identifier': identifier.to_json(),\n            'challenges': [challb.to_json() for challb in self.challbs],\n        }\n\n    def test_from_json(self):\n        from acme.messages import Authorization\n        Authorization.from_json(self.jobj_from)\n\n    def test_from_json_hashable(self):\n        from acme.messages import Authorization\n        hash(Authorization.from_json(self.jobj_from))\n\n\nclass AuthorizationResourceTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.AuthorizationResource.\"\"\"\n\n    def test_json_de_serializable(self):\n        from acme.messages import AuthorizationResource\n        authzr = AuthorizationResource(\n            uri=mock.sentinel.uri,\n            body=mock.sentinel.body)\n        assert isinstance(authzr, jose.JSONDeSerializable)\n\n\nclass CertificateRequestTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.CertificateRequest.\"\"\"\n\n    def setUp(self):\n        from acme.messages import CertificateRequest\n        self.req = CertificateRequest(csr=CSR)\n\n    def test_json_de_serializable(self):\n        assert isinstance(self.req, jose.JSONDeSerializable)\n        from acme.messages import CertificateRequest\n        assert self.req == CertificateRequest.from_json(self.req.to_json())\n\n\nclass CertificateResourceTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.CertificateResourceTest.\"\"\"\n\n    def setUp(self):\n        from acme.messages import CertificateResource\n        self.certr = CertificateResource(\n            body=CERT, uri=mock.sentinel.uri, authzrs=(),\n            cert_chain_uri=mock.sentinel.cert_chain_uri)\n\n    def test_json_de_serializable(self):\n        assert isinstance(self.certr, jose.JSONDeSerializable)\n        from acme.messages import CertificateResource\n        assert self.certr == CertificateResource.from_json(self.certr.to_json())\n\n\nclass RevocationTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.RevocationTest.\"\"\"\n\n    def setUp(self):\n        from acme.messages import Revocation\n        self.rev = Revocation(certificate=CERT)\n\n    def test_from_json_hashable(self):\n        from acme.messages import Revocation\n        hash(Revocation.from_json(self.rev.to_json()))\n\n\nclass OrderResourceTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.OrderResource.\"\"\"\n\n    def setUp(self):\n        from acme.messages import OrderResource\n        self.regr = OrderResource(\n            body=mock.sentinel.body, uri=mock.sentinel.uri)\n\n    def test_to_partial_json(self):\n        assert self.regr.to_json() == {\n            'body': mock.sentinel.body,\n            'uri': mock.sentinel.uri,\n            'authorizations': None,\n        }\n\n    def test_json_de_serializable(self):\n        from acme.messages import ChallengeBody\n        from acme.messages import STATUS_PENDING\n        challbs = (\n            ChallengeBody(\n                uri='http://challb1', status=STATUS_PENDING,\n                chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')),\n            ChallengeBody(uri='http://challb2', status=STATUS_PENDING,\n                          chall=challenges.DNS(\n                              token=b'DGyRejmCefe7v4NfDGDKfA')),\n        )\n\n        from acme.messages import Authorization\n        from acme.messages import AuthorizationResource\n        from acme.messages import Identifier\n        from acme.messages import IDENTIFIER_FQDN\n        identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')\n        authz = AuthorizationResource(uri=\"http://authz1\",\n                                      body=Authorization(\n                                          identifier=identifier,\n                                          challenges=challbs))\n        from acme.messages import Order\n        body = Order(identifiers=(identifier,), status=STATUS_PENDING,\n                     authorizations=tuple(challb.uri for challb in challbs))\n        from acme.messages import OrderResource\n        orderr = OrderResource(uri=\"http://order1\", body=body,\n                               csr_pem=b'test blob',\n                               authorizations=(authz,))\n        self.assertEqual(orderr,\n                         OrderResource.from_json(orderr.to_json()))\n\nclass NewOrderTest(unittest.TestCase):\n    \"\"\"Tests for acme.messages.NewOrder.\"\"\"\n\n    def setUp(self):\n        from acme.messages import NewOrder\n        self.order = NewOrder(\n            identifiers=mock.sentinel.identifiers)\n\n    def test_to_partial_json(self):\n        assert self.order.to_json() == {\n            'identifiers': mock.sentinel.identifiers,\n        }\n\n    def test_default_profile_empty(self):\n        assert self.order.profile is None\n\n    def test_non_empty_profile(self):\n        from acme.messages import NewOrder\n        order = NewOrder(identifiers=mock.sentinel.identifiers, profile='example')\n        assert order.to_json() == {\n            'identifiers': mock.sentinel.identifiers,\n            'profile': 'example',\n        }\n\n\nclass JWSPayloadRFC8555Compliant(unittest.TestCase):\n    \"\"\"Test for RFC8555 compliance of JWS generated from resources/challenges\"\"\"\n    def test_message_payload(self):\n        from acme.messages import NewAuthorization\n\n        new_order = NewAuthorization()\n\n        jobj = new_order.json_dumps(indent=2).encode()\n        # RFC8555 states that JWS bodies must not have a resource field.\n        assert jobj == b'{}'\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/standalone_test.py",
    "content": "\"\"\"Tests for acme.standalone.\"\"\"\nimport http.client as http_client\nimport socket\nimport socketserver\nimport sys\nimport threading\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\nimport requests\n\nfrom acme import challenges\nfrom acme._internal.tests import test_util\n\n\nclass HTTP01ServerTest(unittest.TestCase):\n    \"\"\"Tests for acme.standalone.HTTP01Server.\"\"\"\n\n\n    def setUp(self):\n        self.account_key = jose.JWK.load(\n            test_util.load_vector('rsa1024_key.pem'))\n        self.resources: set = set()\n\n        from acme.standalone import HTTP01Server\n        self.server = HTTP01Server(('', 0), resources=self.resources)\n\n        self.port = self.server.socket.getsockname()[1]\n        self.thread = threading.Thread(target=self.server.serve_forever)\n        self.thread.start()\n\n    def tearDown(self):\n        self.server.shutdown()\n        self.thread.join()\n        self.server.server_close()\n\n    def test_index(self):\n        response = requests.get(\n            'http://localhost:{0}'.format(self.port), verify=False)\n        assert response.text == 'ACME client standalone challenge solver'\n        assert response.ok\n\n    def test_404(self):\n        response = requests.get(\n            'http://localhost:{0}/foo'.format(self.port), verify=False)\n        assert response.status_code == http_client.NOT_FOUND\n\n    def _test_http01(self, add):\n        chall = challenges.HTTP01(token=(b'x' * 16))\n        response, validation = chall.response_and_validation(self.account_key)\n\n        from acme.standalone import HTTP01RequestHandler\n        resource = HTTP01RequestHandler.HTTP01Resource(\n            chall=chall, response=response, validation=validation)\n        if add:\n            self.resources.add(resource)\n        return resource.response.simple_verify(\n            resource.chall, 'localhost', self.account_key.public_key(),\n            port=self.port)\n\n    def test_http01_found(self):\n        assert self._test_http01(add=True)\n\n    def test_http01_not_found(self):\n        assert not self._test_http01(add=False)\n\n    def test_timely_shutdown(self):\n        from acme.standalone import HTTP01Server\n        with HTTP01Server(('', 0), resources=set(), timeout=0.05) as server:\n            server_thread = threading.Thread(target=server.serve_forever)\n            server_thread.start()\n\n            with socket.socket() as client:\n                client.connect(('localhost', server.socket.getsockname()[1]))\n\n                stop_thread = threading.Thread(target=server.shutdown)\n                stop_thread.start()\n                server_thread.join(5.)\n\n                is_hung = server_thread.is_alive()\n                try:\n                    client.shutdown(socket.SHUT_RDWR)\n                except: # pragma: no cover, pylint: disable=bare-except\n                    # may raise error because socket could already be closed\n                    pass\n\n                assert not is_hung, 'Server shutdown should not be hung'\n\n\nclass BaseDualNetworkedServersTest(unittest.TestCase):\n    \"\"\"Test for acme.standalone.BaseDualNetworkedServers.\"\"\"\n\n    class SingleProtocolServer(socketserver.TCPServer):\n        \"\"\"Server that only serves on a single protocol. FreeBSD has this behavior for AF_INET6.\"\"\"\n        def __init__(self, *args, **kwargs):\n            ipv6 = kwargs.pop(\"ipv6\", False)\n            if ipv6:\n                self.address_family = socket.AF_INET6\n                kwargs[\"bind_and_activate\"] = False\n            else:\n                self.address_family = socket.AF_INET\n            super().__init__(*args, **kwargs)\n            if ipv6:\n                # NB: On Windows, socket.IPPROTO_IPV6 constant may be missing.\n                # We use the corresponding value (41) instead.\n                level = getattr(socket, \"IPPROTO_IPV6\", 41)\n                self.socket.setsockopt(level, socket.IPV6_V6ONLY, 1)\n                try:\n                    self.server_bind()\n                    self.server_activate()\n                except:\n                    self.server_close()\n                    raise\n\n    @mock.patch(\"socket.socket.bind\")\n    def test_fail_to_bind(self, mock_bind):\n        from errno import EADDRINUSE\n\n        from acme.standalone import BaseDualNetworkedServers\n\n        mock_bind.side_effect = OSError(EADDRINUSE, \"Fake addr in use error\")\n\n        with pytest.raises(socket.error) as exc_info:\n            BaseDualNetworkedServers(\n                BaseDualNetworkedServersTest.SingleProtocolServer,\n                ('', 0), socketserver.BaseRequestHandler)\n\n        assert exc_info.value.errno == EADDRINUSE\n\n    def test_ports_equal(self):\n        from acme.standalone import BaseDualNetworkedServers\n        servers = BaseDualNetworkedServers(\n            BaseDualNetworkedServersTest.SingleProtocolServer,\n            ('', 0),\n            socketserver.BaseRequestHandler)\n        socknames = servers.getsocknames()\n        prev_port = None\n        # assert ports are equal\n        for sockname in socknames:\n            port = sockname[1]\n            if prev_port:\n                assert prev_port == port\n            prev_port = port\n        for server in servers.servers:\n            server.server_close()\n\n\nclass HTTP01DualNetworkedServersTest(unittest.TestCase):\n    \"\"\"Tests for acme.standalone.HTTP01DualNetworkedServers.\"\"\"\n\n    def setUp(self):\n        self.account_key = jose.JWK.load(\n            test_util.load_vector('rsa1024_key.pem'))\n        self.resources: set = set()\n\n        from acme.standalone import HTTP01DualNetworkedServers\n        self.servers = HTTP01DualNetworkedServers(('', 0), resources=self.resources)\n\n        self.port = self.servers.getsocknames()[0][1]\n        self.servers.serve_forever()\n\n    def tearDown(self):\n        self.servers.shutdown_and_server_close()\n\n    def test_index(self):\n        response = requests.get(\n            'http://localhost:{0}'.format(self.port), verify=False)\n        assert response.text == 'ACME client standalone challenge solver'\n        assert response.ok\n\n    def test_404(self):\n        response = requests.get(\n            'http://localhost:{0}/foo'.format(self.port), verify=False)\n        assert response.status_code == http_client.NOT_FOUND\n\n    def _test_http01(self, add):\n        chall = challenges.HTTP01(token=(b'x' * 16))\n        response, validation = chall.response_and_validation(self.account_key)\n\n        from acme.standalone import HTTP01RequestHandler\n        resource = HTTP01RequestHandler.HTTP01Resource(\n            chall=chall, response=response, validation=validation)\n        if add:\n            self.resources.add(resource)\n        return resource.response.simple_verify(\n            resource.chall, 'localhost', self.account_key.public_key(),\n            port=self.port)\n\n    def test_http01_found(self):\n        assert self._test_http01(add=True)\n\n    def test_http01_not_found(self):\n        assert not self._test_http01(add=False)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/test_util.py",
    "content": "\"\"\"Test utilities.\n\n.. warning:: This module is not part of the public API.\n\n\"\"\"\nimport importlib.resources\nimport os\nfrom typing import Callable\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\nimport josepy as jose\nfrom josepy.util import ComparableECKey\n\n\ndef load_vector(*names):\n    \"\"\"Load contents of a test vector.\"\"\"\n    # luckily, resource_string opens file in binary mode\n    vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)\n    return vector_ref.read_bytes()\n\n\ndef _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable:\n    _, ext = os.path.splitext(filename)\n    if ext.lower() == \".pem\":\n        return loader_pem\n    elif ext.lower() == \".der\":\n        return loader_der\n    else:  # pragma: no cover\n        raise ValueError(\"Loader could not be recognized based on extension\")\n\n\ndef load_cert(*names: str) -> x509.Certificate:\n    \"\"\"Load certificate.\"\"\"\n    loader = _guess_loader(\n        names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate\n    )\n    return loader(load_vector(*names))\n\n\ndef load_csr(*names: str) -> x509.CertificateSigningRequest:\n    \"\"\"Load certificate request.\"\"\"\n    loader = _guess_loader(names[-1], x509.load_pem_x509_csr, x509.load_der_x509_csr)\n    return loader(load_vector(*names))\n\n\ndef load_rsa_private_key(*names):\n    \"\"\"Load RSA private key.\"\"\"\n    loader = _guess_loader(names[-1], serialization.load_pem_private_key,\n                           serialization.load_der_private_key)\n    return jose.ComparableRSAKey(loader(\n        load_vector(*names), password=None, backend=default_backend()))\n\n\ndef load_ecdsa_private_key(*names):\n    \"\"\"Load ECDSA private key.\"\"\"\n    loader = _guess_loader(names[-1], serialization.load_pem_private_key,\n                           serialization.load_der_private_key)\n    return ComparableECKey(loader(\n        load_vector(*names), password=None, backend=default_backend()))\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/README",
    "content": "In order for acme.test_util._guess_loader to work properly, make sure\nto use appropriate extension for vector filenames: .pem for PEM and\n.der for DER.\n\nThe following command has been used to generate test keys:\n\n  for k in 256 512 1024 2048 4096; do openssl genrsa -out rsa${k}_key.pem $k; done\n\nand for the CSR:\n\n  openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der\n\nand for the certificates:\n\n  openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der\n  openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -x509 > rsa2048_cert.pem\n  openssl req -key rsa1024_key.pem -new -subj '/CN=example.com' -x509 > rsa1024_cert.pem\n\nand for the elliptic key curves:\n\n  openssl genpkey -algorithm EC -out ec_secp384r1.pem -pkeyopt ec_paramgen_curve:P-384 -pkeyopt ec_param_enc:named_curve\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert-100sans.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV\nBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv\nbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X\nDTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU\nBgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp\nZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B\nAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580\nrv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T\nBAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t\nggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt\ncGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j\nb22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN\nZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh\nbXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs\nZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx\nLmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv\nbYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN\nZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh\nbXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs\nZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3\nLmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv\nbYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN\nZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh\nbXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs\nZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz\nLmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv\nbYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN\nZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh\nbXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs\nZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5\nLmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv\nbYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN\nZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh\nbXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs\nZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1\nLmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv\nbYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN\nZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh\nbXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs\nZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN\nAQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ\nXYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert-idnsans.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV\nBAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv\nbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X\nDTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU\nBgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp\nZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B\nAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580\nrv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T\nBAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I\nz4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g\nz6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z\nz7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM\n2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf\n2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3\n2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi\n2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi\n2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs\naWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN\n247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p\nbnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4\n27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt\n4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh\noLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh\noYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh\noZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm\n4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2\nLmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT\nTxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert-ipsans.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDizCCAnOgAwIBAgIIPNBLQXwhoUkwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxNzNiMjYwHhcNMjAwNTI5MTkxODA5\nWhcNMjUwNTI5MTkxODA5WjAWMRQwEgYDVQQDEwsxOTIuMC4yLjE0NTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyChb+NDA26GF1AfC0nzEdfOTchKw0h\nq41xEjonvg5UXgZf/aH/ntvugIkYP0MaFifNAjebOVVsemEVEtyWcUKTfBHKZGbZ\nukTDwFIjfTccCfo6U/B2H7ZLzJIywl8DcUw9DypadeQBm8PS0VVR2ncy73dvaqym\ncrhAwlASyXU0mhLqRDMMxfg5Bn/FWpcsIcDpLmPn8Q/FvdRc2t5ryBNw/aWOlwqT\nOy16nbfLj2T0zG1A3aPuD+eT/JFUe/o3K7R+FAx7wt+RziQO46wLVVF1SueZUrIU\nzqN04Gl8Kt1WM2SniZ0gq/rORUNcPtT0NAEsEslTQfA+Trq6j2peqyMCAwEAAaOB\nyjCBxzAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF\nBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFHj1mwZzP//nMIH2i58NRUl/arHn\nMB8GA1UdIwQYMBaAFF5DVAKabvIUvKFHGouscA2Qdpe6MDEGCCsGAQUFBwEBBCUw\nIzAhBggrBgEFBQcwAYYVaHR0cDovLzEyNy4wLjAuMTo0MDAyMBUGA1UdEQQOMAyH\nBMAAApGHBMsAcQEwDQYJKoZIhvcNAQELBQADggEBAHjSgDg76/UCIMSYddyhj18r\nLdNKjA7p8ovnErSkebFT4lIZ9f3Sma9moNr0w64M33NamuFyHe/KTdk90mvoW8Uu\n26aDekiRIeeMakzbAtDKn67tt2tbedKIYRATcSYVwsV46uZKbM621dZKIjjxOWpo\nIY6rZYrku8LYhoXJXOqRduV3cTRVuTm5bBa9FfVNtt6N1T5JOtKKDEhuSaF4RSug\nPDy3hQIiHrVvhPfVrXU3j6owz/8UCS5549inES9ONTFrvM9o0H1R/MsmGNXR5hF5\niJqHKC7n8LZujhVnoFIpHu2Dsiefbfr+yRYJS4I+ezy6Nq/Ok8rc8zp0eoX+uyY=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert-ipv6sans.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDmzCCAoOgAwIBAgIIFdxeZP+v2rgwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSA0M2M5NTcwHhcNMjAwNTMwMDQwNzMw\nWhcNMjUwNTMwMDQwNzMwWjAOMQwwCgYDVQQDEwM6OjEwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQC7VidVduJvqKtrSH0fw6PjE0cqL4Kfzo7klexWUkHG\nKVAa0fRVZFZ462jxKOt417V2U4WJQ6WHHO9PJ+3gW62d/MhCw8FRtUQS4nYFjqB6\n32+RFU21VRN7cWoQEqSwnEPbh/v/zv/KS5JhQ+swWUo79AOLm1kjnZWCKtcqh1Lc\nUg5Tkpot6luoxTKp52MkchvXDpj0q2B/XpLJ8/pw5cqjv7mH12EDOK2HXllA+WwX\nZpstcEhaA4FqtaHOW/OHnwTX5MUbINXE5YYHVEDR6moVM31/W/3pe9NDUMTDE7Si\nlVQnZbXM9NYbzZqlh+WhemDWwnIfGI6rtsfNEiirVEOlAgMBAAGjgeIwgd8wDgYD\nVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV\nHRMBAf8EAjAAMB0GA1UdDgQWBBS8DL+MZfDIy6AKky69Tgry2Vxq5DAfBgNVHSME\nGDAWgBRAsFqVenRRKgB1YPzWKzb9bzZ/ozAxBggrBgEFBQcBAQQlMCMwIQYIKwYB\nBQUHMAGGFWh0dHA6Ly8xMjcuMC4wLjE6NDAwMjAtBgNVHREEJjAkhxAAAAAAAAAA\nAAAAAAAAAAABhxCjvjLzIG7HXQlWDO6YWF7FMA0GCSqGSIb3DQEBCwUAA4IBAQBY\nM9UTZ3uaKMQ+He9kWR3p9jh6hTSD0FNi79ZdfkG0lgSzhhduhN7OhzQH2ihUUfa6\nrtKTw74fGbszhizCd9UB8YPKlm3si1Xbg6ZUQlA1RtoQo7RUGEa6ZbR68PKGm9Go\nhTTFIl/JS8jzxBR8jywZdyqtprUx+nnNUDiNk0hJtFLhw7OJH0AHlAUNqHsfD08m\nHXRdaV6q14HXU5g31slBat9H4D6tCU/2uqBURwW0wVdnqh4QeRfAeqiatJS9EmSF\nctbc7n894Idy2Xce7NFoIy5cht3m6Rd42o/LmBsJopBmQcDPZT70/XzRtc2qE0cS\nCzBIGQHUJ6BfmBjrCQnp\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert-san.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx\nETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM\nIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG\nA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix\nKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR\n7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c\n+pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt\ncGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF\nnTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7\nRDjyGMKy5ZgM2w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx\nETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM\nIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG\nA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix\nKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR\n7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c\n+pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll\nvr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn\nB/o=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/critical-san.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIErTCCA5WgAwIBAgIKETb7VQAAAAAdGTANBgkqhkiG9w0BAQsFADCBkTELMAkG\nA1UEBhMCVVMxDTALBgNVBAgTBFV0YWgxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5\nMRUwEwYDVQQKEwxWZW5hZmksIEluYy4xHzAdBgNVBAsTFkRlbW9uc3RyYXRpb24g\nU2VydmljZXMxIjAgBgNVBAMTGVZlbmFmaSBFeGFtcGxlIElzc3VpbmcgQ0EwHhcN\nMTcwNzEwMjMxNjA1WhcNMTcwODA5MjMxNjA1WjAAMIIBIjANBgkqhkiG9w0BAQEF\nAAOCAQ8AMIIBCgKCAQEA7CU5qRIzCs9hCRiSUvLZ8r81l4zIYbx1V1vZz6x1cS4M\n0keNfFJ1wB+zuvx80KaMYkWPYlg4Rsm9Ok3ZapakXDlaWtrfg78lxtHuPw1o7AYV\nEXDwwPkNugLMJfYw5hWYSr8PCLcOJoY00YQ0fJ44L+kVsUyGjN4UTRRZmOh/yNVU\n0W12dTCz4X7BAW01OuY6SxxwewnW3sBEep+APfr2jd/oIx7fgZmVB8aRCDPj4AFl\nXINWIwxmptOwnKPbwLN/vhCvJRUkO6rA8lpYwQkedFf6fHhqi2Sq/NCEOg4RvMCF\nfKbMpncOXxz+f4/i43SVLrPz/UyhjNbKGJZ+zFrQowIDAQABo4IBlTCCAZEwPgYD\nVR0RAQH/BDQwMoIbY2hpY2Fnby1jdWJzLnZlbmFmaS5leGFtcGxlghNjdWJzLnZl\nbmFmaS5leGFtcGxlMB0GA1UdDgQWBBTgKZXVSFNyPHHtO/phtIALPcCF5DAfBgNV\nHSMEGDAWgBT/JJ6Wei/pzf+9DRHuv6Wgdk2HsjBSBgNVHR8ESzBJMEegRaBDhkFo\ndHRwOi8vcGtpLnZlbmFmaS5leGFtcGxlL2NybC9WZW5hZmklMjBFeGFtcGxlJTIw\nSXNzdWluZyUyMENBLmNybDA6BggrBgEFBQcBAQQuMCwwKgYIKwYBBQUHMAGGHmh0\ndHA6Ly9wa2kudmVuYWZpLmV4YW1wbGUvb2NzcDAOBgNVHQ8BAf8EBAMCBaAwPQYJ\nKwYBBAGCNxUHBDAwLgYmKwYBBAGCNxUIhIDLGYTvsSSEnZ8ehvD5UofP4hMEgobv\nDIGy4mcCAWQCAQIwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGwYJKwYBBAGCNxUKBA4w\nDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA3YW4t1AzxEn384OqdU6L\nny8XkMhWpRM0W0Z9ZC3gRZKbVUu49nG/KB5hbVn/de33zdX9HOZJKc0vXzkGZQUs\nOUCCsKX4VKzV5naGXOuGRbvV4CJh5P0kPlDzyb5t312S49nJdcdBf0Y/uL5Qzhst\nbXy8qNfFNG3SIKKRAUpqE9OVIl+F+JBwexa+v/4dFtUOqMipfXxB3TaxnDqvU1dS\nyO34ZTvIMGXJIZ5nn/d/LNc3N3vBg2SHkMpladqw0Hr7mL0bFOe0b+lJgkDP06Be\nn08fikhz1j2AW4/ZHa9w4DUz7J21+RtHMhh+Vd1On0EAeZ563svDe7Z+yrg6zOVv\nKA==\n-----END CERTIFICATE-----"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-100sans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz\nY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG\nA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt\nH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6\nlUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud\nEwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv\nbYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh\nbXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu\nY29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C\nDWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4\nYW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w\nbGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy\nMS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j\nb22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C\nDWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4\nYW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w\nbGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz\nNy5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j\nb22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C\nDWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4\nYW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w\nbGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1\nMy5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j\nb22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C\nDWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4\nYW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w\nbGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2\nOS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j\nb22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C\nDWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4\nYW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w\nbGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4\nNS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j\nb22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C\nDWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4\nYW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w\nbGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3\nDQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo\nduUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-6sans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw\nEAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy\nc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0\n9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG\n9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL\nZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t\nghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd\nk4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv\nIvzVBz/nD11drfz/RNuX\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-idnsans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz\nY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG\nA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt\nH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6\nlUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud\nEwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP\niM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P\noM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP\ns8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ\njNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z\nn9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ\nt9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC\nYtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa\notqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh\nbGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb\njduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu\naW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb\nuNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg\nreGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5\n4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ\n4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ\n4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh\npuGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh\nti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr\ndfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-ipsans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICbTCCAVUCAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKT/\nCE7Y5EYBvI4p7Frt763upIKHDHO/R5/TWMjG8Jm9qTMui8sbMgyh2Yh+lR/j/5Xd\ntQrhgC6wx10MrW2+3JtYS88HP1p6si8zU1dbK34n3NyyklR2RivW0R7dXgnYNy7t\n5YcDYLCrbRMIPINV/uHrmzIHWYUDNcZVdAfIM2AHfKYuV6Mepcn///5GR+l4GcAh\nNkf9CW8OdAIuKdbyLCxVr0mUW/vJz1b12uxPsgUdax9sjXgZdT4pfMXADsFd1NeF\natpsXU073inqtHru+2F9ijHTQ75TC+u/rr6eYl3BnBntac0gp/ADtDBii7/Q1JOO\nBhq7xJNqqxIEdiyM7zcCAwEAAaAoMCYGCSqGSIb3DQEJDjEZMBcwFQYDVR0RBA4w\nDIcEwAACkYcEywBxATANBgkqhkiG9w0BAQsFAAOCAQEADG5g3zdbSCaXpZhWHkzE\nMek3f442TUE1pB+ITRpthmM4N3zZWETYmbLCIAO624uMrRnbCCMvAoLs/L/9ETg/\nXMMFtonQC8u9i9tV8B1ceBh8lpIfa+8b9TMWH3bqnrbWQ+YIl+Yd0gXiCZWJ9vK4\neM1Gddu/2bR6s/k4h/XAWRgEexqk57EHr1z0N+T9OoX939n3mVcNI+u9kfd5VJ0z\nVyA3R8WR6T6KlEl5P5pcWe5Kuyhi7xMmLVImXqBtvKq4O1AMfM+gQr/yn9aE8IRq\nkhP7JrMBLUIub1c/qu2TfvnynNPSM/ZcOX+6PHdHmRkR3nI0Ndpv7Ntv31FTplAm\nDw==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-ipv6sans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIChTCCAW0CAQIwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOIc\nUAppcqJfTkSqqOFqGt1v7lIJZPOcF4bcKI3d5cHAGbOuVxbC7uMaDuObwYLzoiED\nqnvs1NaEq2phO6KsgGESB7IE2LUjJivO7OnSZjNRpL5si/9egvBiNCn/50lULaWG\ngLEuyMfk3awZy2mVAymy7Grhbx069A4TH8TqsHuq2RpKyuDL27e/jUt6yYecb3pu\nhWMiWy3segif4tI46pkOW0/I6DpxyYD2OqOvzxm/voS9RMqE2+7YJA327H7bEi3N\nlJZEZ1zy7clZ9ga5fBQaetzbg2RyxTrZ7F919NQXSFoXgxb10Eg64wIpz0L3ooCm\nGEHehsZZexa3J5ccIvMCAwEAAaBAMD4GCSqGSIb3DQEJDjExMC8wLQYDVR0RBCYw\nJIcQAAAAAAAAAAAAAAAAAAAAAYcQo74y8yBux10JVgzumFhexTANBgkqhkiG9w0B\nAQsFAAOCAQEALvwVn0A/JPTCiNzcozHFnp5M23C9PXCplWc5u4k34d4XXzpSeFDz\nfL4gy7NpYIueme2K2ppw2j3PNQUdR6vQ5a75sriegWYrosL+7Q6Joh51ZyEUZQoD\nmNl4M4S4oX85EaChR6NFGBywTfjFarYi32XBTbFE7rK8N8KM+DQkNdwL1MXqaHWz\nF1obQKpNXlLedbCBOteV5Eg4zG3565zu/Gw/NhwzzV3mQmgxUcd1sMJxAfHQz4Vl\nImLL+xMcR03nDsH2bgtDbK2tJm7WszSxA9tC+Xp2lRewxrnQloRWPYDz177WGQ5Q\nSoGDzTTtA6uWZxG8h7CkNLOGvA8LtU2rNA==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-mixed.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICdjCCAV4CAQAwADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANoV\nT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svNPSa+oPTK\n7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm7Gj6m2Ez\npSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFnxvvOjBYo\np7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTDg7P4UAuF\nkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1RAeEeRTk\nh0WjUfltoem/5f8bIdsCAwEAAaAxMC8GCSqGSIb3DQEJDjEiMCAwHgYDVR0RBBcw\nFYINYS5leGVtcGxlLmNvbYcEwAACbzANBgkqhkiG9w0BAQsFAAOCAQEAQ7n/hYen\n5INHlcslHPYCQ/BAbX6Ou+Y8hUu8puWNVpE2OM95L2C87jbWwTmCRnkFBwtyoNqo\nj3DXVW2RYv8y/exq7V6Y5LtpHTgwfugINJ3XlcVzA4Vnf1xqOxv3kwejkq74RuXn\nxd5N28srgiFqb0e4tOAWVI8Tw27bgBqjoXl0QDFPZpctqUia5bcDJ9WzNSM7VaO1\nCBNGHBRz+zL8sqoqJA4HV58tjcgzl+1RtGM+iUHxXpnH+aCNKWIUINrAzIm4Sm00\n93RJjhb1kdNR0BC7ikWVbAWaVviHdvATK/RfpmhWDqfEaNgBpvT91GnkhpzctSFD\nro0yCUUXXrIr0w==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-nosans.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh\nMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt\ncGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn\nBUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz\nAgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo\nwgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr-san.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw\nEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy\nc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f\np580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN\nAQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy\ntmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/csr.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw\nEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy\nc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f\np580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN\nAQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB\nAHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G\nn9XBE1N9W6HCIEut2d8wACg=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/dsa512_key.pem",
    "content": "-----BEGIN DSA PARAMETERS-----\nMIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC\nOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA\nqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl\n41pgNJpgu99YOYqPpS0g7A==\n-----END DSA PARAMETERS-----\n-----BEGIN DSA PRIVATE KEY-----\nMIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf\nn6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP\nAkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm\nrfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth\nzZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE\nmNv063So6E+eYaIN\n-----END DSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/ec_secp384r1_key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDArTn0pbFk3xHfKeXte\nxJgS4JVdJQ8mqvezhaNpULZPnwb+mcKLlrj6f5SRM52yREGhZANiAAQcrMoPMVqV\nrHnDGGz5HUKLNmXfChlNgsrwsruawXF+M283CA6eckAjTXNyiC/ounWmvtoKsZG0\n2UQOfQUNSCANId/r986yRGc03W6RJSkcRp86qBYjNsLgbZpber/3+M4=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa1024_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB/TCCAWagAwIBAgIJAOyRIBs3QT8QMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV\nBAMMC2V4YW1wbGUuY29tMB4XDTE4MDQyMzEwMzE0NFoXDTE4MDUyMzEwMzE0NFow\nFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ\nAoGBAJqJ87R8aVwByONxgQA9hwgvQd/QqI1r1UInXhEF2VnEtZGtUWLi100IpIqr\nMq4qusDwNZ3g8cUPtSkvJGs89djoajMDIJP7lQUEKUYnYrI0q755Tr/DgLWSk7iW\nl5ezym0VzWUD0/xXUz8yRbNMTjTac80rS5SZk2ja2wWkYlRJAgMBAAGjUzBRMB0G\nA1UdDgQWBBSsaX0IVZ4XXwdeffVAbG7gnxSYjTAfBgNVHSMEGDAWgBSsaX0IVZ4X\nXwdeffVAbG7gnxSYjTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4GB\nADe7SVmvGH2nkwVfONk8TauRUDkePN1CJZKFb2zW1uO9ANJ2v5Arm/OQp0BG/xnI\nDjw/aLTNVESF89oe15dkrUErtcaF413MC1Ld5lTCaJLHLGqDKY69e02YwRuxW7jY\nqarpt7k7aR5FbcfO5r4V/FK/Gvp4Dmoky8uap7SJIW6x\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa1024_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi\n4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/\nw4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB\nAoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB\nIa+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc\nTmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB\nCZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X\nUTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak\nCo9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt\n73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa\nHgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU\n6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ\nc2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa2048_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDjjCCAnagAwIBAgIJALVG/VbBb5U7MA0GCSqGSIb3DQEBCwUAMFsxCzAJBgNV\nBAYTAkFVMQswCQYDVQQIDAJXQTEeMBwGA1UEBwwVVGhlIG1pZGRsZSBvZiBub3do\nZXJlMR8wHQYDVQQKDBZDZXJ0Ym90IFRlc3QgQ2VydHMgSW5jMCAXDTE2MTEyODIx\nMzUzN1oYDzIyOTAwOTEzMjEzNTM3WjBbMQswCQYDVQQGEwJBVTELMAkGA1UECAwC\nV0ExHjAcBgNVBAcMFVRoZSBtaWRkbGUgb2Ygbm93aGVyZTEfMB0GA1UECgwWQ2Vy\ndGJvdCBUZXN0IENlcnRzIEluYzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBANoVT1pdvRUUBOqvm7M2ebLEHV7higUH7qAGUZEkfP6W4YriYVY+IHrH1svN\nPSa+oPTK7weDNmT11ehWnGyECIM9z2r2Hi9yVV0ycxh4hWQ4Nt8BAKZwCwaXpyWm\n7Gj6m2EzpSN5Dd67g5YAQBrUUh1+RRbFi9c0Ls/6ZOExMvfg8kqt4c2sXCgH1IFn\nxvvOjBYop7xh0x3L1Akyax0tw8qgQp/z5mkupmVDNJYPFmbzFPMNyDR61ed6QUTD\ng7P4UAuFkejLLzFvz5YaO7vC+huaTuPhInAhpzqpr4yU97KIjos2/83Itu/Cv8U1\nRAeEeRTkh0WjUfltoem/5f8bIdsCAwEAAaNTMFEwHQYDVR0OBBYEFHy+bEYqwvFU\nuQLTkIfQ36AM2DQiMB8GA1UdIwQYMBaAFHy+bEYqwvFUuQLTkIfQ36AM2DQiMA8G\nA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAH3ANVzB59FcunZV/F8T\nRiCD6/gV7Jc3CswU8N8tVjzMCg2jOdTFF9iYZzNNKQvG13o/n5LkQr/lkKRQkWTx\nnkE5WZbR7vNqlzXgPa9NBiK5rPjgSt8azPW+Skct3Bj4B3PhTMSpoQ7PsUJ8UeV8\nkTNR5xrRLt6/mLfRJTXWXBM43GEZi8lL5q0nqz0tPGISADshHMo6ZlUu5Hvfp5v+\naonpO4sVS9hGOVxjGNMXYApEUy4jid9jjAfEk6jeELJMbXGLy/botFgIJK/QPe6P\nAfbdFgtg/qzG7Uy0A1iXXfWdgwmVrhCoGYYWCn4yWCAm894QKtdim87CHSDP0WUf\nEsg=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa2048_key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDaFU9aXb0VFATq\nr5uzNnmyxB1e4YoFB+6gBlGRJHz+luGK4mFWPiB6x9bLzT0mvqD0yu8HgzZk9dXo\nVpxshAiDPc9q9h4vclVdMnMYeIVkODbfAQCmcAsGl6clpuxo+pthM6UjeQ3eu4OW\nAEAa1FIdfkUWxYvXNC7P+mThMTL34PJKreHNrFwoB9SBZ8b7zowWKKe8YdMdy9QJ\nMmsdLcPKoEKf8+ZpLqZlQzSWDxZm8xTzDcg0etXnekFEw4Oz+FALhZHoyy8xb8+W\nGju7wvobmk7j4SJwIac6qa+MlPeyiI6LNv/NyLbvwr/FNUQHhHkU5IdFo1H5baHp\nv+X/GyHbAgMBAAECggEAURFe4C68XRuGAF+rN2Fmt+djK6QXlGswb1gp9hRkSpd3\n3BLvMAoENOAYnsX6l26Bkr3lQRurmrgv/iBEIaqrJ25QrmgzLFwKE4zvcAdNPsYO\nz7MltLktwBOb1MlKVHPkUqvKFXeoikWWUqphKhgHNmN7900UALmrNTDVU0jgs3fB\no35o8d5SjoC52K4wCTjhPyjt4cdbfbziRs2qFhfGdawidRO1xLlDM4tTTW+5yWGK\nlt0SwyvDVC6XWeNoT3nXyKjXWP7hcYqm0iS7ffL9YzEC2RXNGQUqeR50i9Y0rDdH\nVqcr+Rqio2ww68zbDWBpC/jU133BSoHuSE1wstxIkQKBgQDxlEr42WJfgdajbZ1a\nhUIeLEgvhezLmD1hcYwZuQCLgizmY2ovvmeAH74koCDEsUUQunPYHsRla7wT3q1/\nIkR1KgJPwESpkQaKuAqxeEAkv7Gn8Lzcn22jCoRCfGA68wKJz2ECFZDc0RDvRrT/\n9GhiiGUoO47jv9ezrSDO1eu5/QKBgQDnGfYVMNLiA0fy4AxSyY2vdo7vruOFGpRP\nn94gwxZ+0dQDWHzn3J4rHivxtcyd/MOZv4I8PtYK7tmmjYv1ngQ6sGl4p8bpUtwj\n9++/B1CyB1W5/VPqMkd+Sj0dbejycME55+F6/r4basPXxBFFCfknjAlVvyvbBhUy\nftNpHxZGtwKBgChJM4t2LPqCW3nbgL8ks9b2SX9rVQbKt4m1dsifWmDpb3VoJMAb\nf4UVRg8ziONkMIFOppzm3JeRNMcXflVSMJpdTA9in9CrN60QbfAUfpXiRc0cz1H3\nYEAtM8smlKGf/s9efu3rDMJWNv3AC9UXPAUae8wOypBeYKk8+NilQe89AoGAXEA3\nxFO+CqyGnwQixzVf0qf//NuSRQLMK1DEyc02gJ9gA4niKmgd11Zu8kjBClvo9MnG\nwifPJ4Qa6+pa8UwHoinjoF9Q/rit2cnSMS5JXxegd+MRCU7SzS3zYXkLYSPzbhsL\nHh7sYmNnFA1XW3jUtZ2n6EusxPyTn5mS6MaZDNcCgYBelFKFjNIQ50NbOnm8DewK\njUd5OFKowKodlQVcHiF9CVbjvpN8ZPRcBSmqDU4kpT/rmcybVoL6Zfa/zWkw8+Oh\nQxKb3BYf5vRUMd/RA+/t5KG4ZOIIYB3qoltAYlhVaINukL6cGVG1qvV/ntcsfsn6\nkmf1UgGFcKrJuXgwEtTVxw==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa256_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh\nAJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N\nE8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3\nrTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa4096_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFDTCCAvWgAwIBAgIUImqDrP53V69vFROsjP/gL0YtoA4wDQYJKoZIhvcNAQEL\nBQAwFjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMjAwNTI3MjMyNDE0WhcNMjAw\nNjI2MjMyNDE0WjAWMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCAiIwDQYJKoZIhvcN\nAQEBBQADggIPADCCAgoCggIBANY9LKLk9Dxn0MUMQFHwBoTN4ehDSWBws2KcytpF\nmc8m9Mfk1wmb4fQSKYtK3wIFMfIyo9HQu0nKqMkkUw52o3ZXyOv+oWwF5qNy2BKu\nlh5OMSkaZ0o13zoPpW42e+IUnyxvg70+0urD+sUue4cyTHh/nBIUjrM/05ZJ/ac8\nHR0RK3H41YoqBjq69JjMZczZZhbNFit3s6p0R1TbVAgc3ckqbtX5BDyQMQQCP4Ed\nm4DgbAFVqdcPUCC5W3F3fmuQiPKHiADzONZnXpy6lUvLDWqcd6loKp+nKHM6OkXX\n8hmD7pE1PYMQo4hqOfhBR2IgMjAShwd5qUFjl1m2oo0Qm3PFXOk6i2ZQdS6AA/yd\nB5/mX0RnM2oIdFZPb6UZFSmtEgs9sTzn+hMUyNSZQRE54px1ur1xws2R+vbsCyM5\n+KoFVxDjVjU9TlZx3GvDvnqz/tbHjji6l8VHZYOBMBUXbKHu2U6pJFZ5Zp7k68/z\na3Fb9Pjtn3iRkXEyC0N5kLgqO4QTlExnxebV8aMvQpWd/qefnMn9qPYIZPEXSQAR\nmEBIahkcACb60s+acG0WFFluwBPtBqEr8Q67XlSF0Ibf4iBiRzpPobhlWta1nrFg\n4IWHMSoZ0PE75bhIGBEkhrpcXQCAxXmAfxfjKDH7jdJ1fRdnZ/9+OzwYGVX5GH/l\n0QDtAgMBAAGjUzBRMB0GA1UdDgQWBBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAfBgNV\nHSMEGDAWgBQh3xiz/o1nEU2ySylZ9gxCXvIPGzAPBgNVHRMBAf8EBTADAQH/MA0G\nCSqGSIb3DQEBCwUAA4ICAQAELoXz31oR9pdAwidlv9ZBOKiC7KBWy8VMqXNVkfTn\nbVRxAUex7zleLFIOkWnqadsMesU9sIwrbLzBcZ8Q/vBY+z2xOPdXcgcAoAmdKWoq\nYBQNiqng9r54sqlzB/77QZCf5fdktESe7NTxhCifgx5SAWq7IUQs/lm3tnMUSAfE\n5ctuN6M+w8K54y3WDprcfMHpnc3ZHeSPhVQApHM0h/bDvXq0bRS7kmq27Hb153Qm\nnH3TwYB5pPSWW38NbUc+s/a7mItO7S8ly8yGbA0j9c/IbN5lM+OCdk06asz3+c8E\nuo8nuCBoYO5+6AqC2N7WJ3Tdr/pFA8jTbd6VNVlgCWTIR8ZosL5Fgkfv+4fUBrHt\nzdVUqMUzvga5rvZnwnJ5Qfu/drHeAAo9MTNFQNe2QgDlYfWBh5GweolgmFSwrpkY\nv/5wLtIyv/ASHKswybbqMIlpttcLTXjx5yuh8swttT6Wh+FQqqQ32KSRB3StiwyK\noH0ZhrwYHiFYNlPxecGX6XUta6rFtTlEdkBGSnXzgiTzL2l+Nc0as0V5B9RninZG\nqJ+VOChSQ0OFvg1riSXv7tMvbLdGQnxwTRL3t6BMS8I4LA2m3ZfWUcuXT783ODTH\n16f1Q1AgXd2csstTWO9cv+N/0fpX31nqrm6+CrGduSr2u4HjYYnlLIUhmdTvK3fX\nFg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa4096_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKgIBAAKCAgEA1j0souT0PGfQxQxAUfAGhM3h6ENJYHCzYpzK2kWZzyb0x+TX\nCZvh9BIpi0rfAgUx8jKj0dC7ScqoySRTDnajdlfI6/6hbAXmo3LYEq6WHk4xKRpn\nSjXfOg+lbjZ74hSfLG+DvT7S6sP6xS57hzJMeH+cEhSOsz/Tlkn9pzwdHRErcfjV\niioGOrr0mMxlzNlmFs0WK3ezqnRHVNtUCBzdySpu1fkEPJAxBAI/gR2bgOBsAVWp\n1w9QILlbcXd+a5CI8oeIAPM41mdenLqVS8sNapx3qWgqn6coczo6RdfyGYPukTU9\ngxCjiGo5+EFHYiAyMBKHB3mpQWOXWbaijRCbc8Vc6TqLZlB1LoAD/J0Hn+ZfRGcz\nagh0Vk9vpRkVKa0SCz2xPOf6ExTI1JlBETninHW6vXHCzZH69uwLIzn4qgVXEONW\nNT1OVnHca8O+erP+1seOOLqXxUdlg4EwFRdsoe7ZTqkkVnlmnuTrz/NrcVv0+O2f\neJGRcTILQ3mQuCo7hBOUTGfF5tXxoy9ClZ3+p5+cyf2o9ghk8RdJABGYQEhqGRwA\nJvrSz5pwbRYUWW7AE+0GoSvxDrteVIXQht/iIGJHOk+huGVa1rWesWDghYcxKhnQ\n8TvluEgYESSGulxdAIDFeYB/F+MoMfuN0nV9F2dn/347PBgZVfkYf+XRAO0CAwEA\nAQKCAgEA0hZdTkQtCYtYm9LexDsXeWYX8VcCfrMmBj7xYcg9A3oVMmzDPuYBVwH0\ngWbjd6y2hOaJ5TfGYZ99kvmvBRDsTSHaoyopC7BhssjtAKz6Ay/0X3VH8usPQ3WS\naZi+NT65tK6KRqtz08ppgLGLa1G00bl5x/Um1rpxeACI4FU/y4BJ1VMJvJpnT3KE\nZ86Qyagqx5NH+UpCApZSWPFX3zjHePzGgcfXErjniCHYOnpZQrFQ2KIzkfSvQ9fg\nx01ByKOM2CB2C1B33TCzBAioXRH6zyAu7A59NeCK9ywTduhDvie1a+oEryFC7IQW\n4s7I/H3MGX4hsf/pLXlHMy+5CZJOjRaC2h+pypfbbcuiXu6Sn64kHNpiI7SxI5DI\nMIRjyG7MdUcrzq0Rt8ogwwpbCoRqrl/w3bhxtqmeZaEZtyxbjlm7reK2YkIFDgyz\nJMqiJK5ZAi+9L/8c0xhjjAQQ0sIzrjmjA8U+6YnWL9jU5qXTVnBB8XQucyeeZGgk\nyRHyMur71qOXN8z3UEva7MHkDTUBlj8DgTz6sEjqCipaWl0CXfDNa4IhHIXD5qiF\nwplhq7OeS0v6EGG/UFa3Q/lFntxtrayxJX7uvvSccGzjPKXTjpWUELLi/FdnIsum\neXT3RgIEYozj4BibDXaBLfHTCVzxOr7AAEvKM9XWSUgLA0paSWECggEBAO9ZBeE1\nGWzd1ejTTkcxBC9AK2rNsYG8PdNqiof/iTbuJWNeRqpG+KB/0CNIpjZ2X5xZd0tM\nFDpHTFehlP26Roxuq50iRAFc+SN5KoiO0A3JuJAidreIgRTia1saUUrypHqWrYEA\nVZVj2AI8Pyg3s1OkR2frFskY7hXBVb/pJNDP/m9xTXXIYiIXYkHYe+4RIJCnAxRv\nq5YHKaX+0Ull9YCZJCxmwvcHat8sgu8qkiwUMEM6QSNEkrEbdnWYBABvC1AR6sws\n7MP1h9+j22n4Zc/3D6kpFZEL9Erx8nNyhbOZ6q2Tdnf6YKVVjZdyVa8VyNnR0ROl\n3BjkFaHb/bg4e4kCggEBAOUk8ZJS3qBeGCOjug384zbHGcnhUBYtYJiOz+RXBtP+\nPRksbFtTkgk1sHuSGO8YRddU4Qv7Av1xL8o+DEsLBSD0YQ7pmLrR/LK+iDQ5N63O\nFve9uJH0ybxAOkiua7G24+lTsVUP//KWToL4Wh5zbHBBjL5D2Z9zoeVbcE87xhva\nlImMVr4Ex252DqNP9wkZxBjudFyJ/C/TnXrjPcgwhxWTC7sLQMhE5p+490G7c4hX\nPywkIKrANbu37KDiAvVS+dC66ZgpL/NUDkeloAmGNO08LGzbV6YKchlvDyWU/AvW\n0hYjbL0FUq7K/wp1G9fumolB+fbI25K9c13X93STzUUCggEBAJDsNFUyk5yJjbYW\nC/WrRj9d+WwH9Az77+uNPSgvn+O0usq6EMuVgYGdImfa21lqv2Wp/kOHY1AOT7lX\nyyD+oyzw7dSNJOQ2aVwDR6+72Vof5DLRy1RBwPbmSd61xrc8yD658YCEtU1pUSe5\nVvyBDYH9nIbdn8RP5gkiMUusXXBaIFNWJXLFzDWcNxBrhk6V7EPp/EFphFmpKJyr\n+AkbRVWCZJbF+hMdWKadCwLJogwyhS6PnVU/dhrq6AU38GRa2Fy5HJRYN1xH1Oej\nDX3Su8L6c28Xw0k6FcczTHx+wVoIPkKvYTIwVkiFzt/+iMckx6KsGo5tBSHFKRwC\nWlQrTxECggEBALjUruLnY1oZ7AC7bTUhOimSOfQEgTQSUCtebsRxijlvhtsKYTDd\nXRt+qidStjgN7S/+8DRYuZWzOeg5WnMhpXZqiOudcyume922IGl3ibjxVsdoyjs5\nJ4xohlrgDlBgBMDNWGoTqNGFejjcmNydH+gAh8VlN2INxJYbxqCyx17qVgwJHmLR\nuggYxD/pHYvCs9GkbknCp5/wYsOgDtKuihfV741lS1D/esN1UEQ+LrfYIEW7snno\n5q7Pcdhn1hkKYCWEzy2Ec4Aj2gzixQ9JqOF/OxpnZvCw1k47rg0TeqcWFYnz8x8Y\n7xO8/DH0OoxXk2GJzVXJuItJs4gLzzfCjL0CggEAJFHfC9jisdy7CoWiOpNCSF1B\nS0/CWDz77cZdlWkpTdaXGGp1MA/UKUFPIH8sOHfvpKS660+X4G/1ZBHmFb4P5kFF\nQy8UyUMKtSOEdZS6KFlRlfSCAMd5aSTmCvq4OSjYEpMRwUhU/iEJNkn9Z1Soehe0\nU3dxJ8KiT1071geO6rRquSHoSJs6Y0WQKriYYQJOhh4Axs3PQihER2eyh+WGk8YJ\n02m0mMsjntqnXtdc6IcdKaHp9ko+OpM9QZLsvt19fxBcrXj/i21uUXrzuNtKfO6M\nJqGhsOrO2dh8lMhvodENvgKA0DmYDC9N7ogo7bxTNSedcjBF46FhJoqii8m70Q==\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/testdata/rsa512_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79\nvukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn\nelAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc\nmQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp\nZu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj\n8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq\n6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "acme/src/acme/_internal/tests/util_test.py",
    "content": "\"\"\"Tests for acme.util.\"\"\"\nimport sys\n\nimport pytest\n\n\ndef test_it():\n    from acme.util import map_keys\n    assert {'a': 'b', 'c': 'd'} == \\\n                     map_keys({'a': 'b', 'c': 'd'}, lambda key: key)\n    assert {2: 2, 4: 4} == map_keys({1: 2, 3: 4}, lambda x: x + 1)\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "acme/src/acme/challenges.py",
    "content": "\"\"\"ACME Identifier Validation Challenges.\"\"\"\nimport abc\nimport functools\nimport hashlib\nimport ipaddress\nimport logging\nfrom typing import Any\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import TypeVar\nfrom typing import Union\n\nfrom cryptography.hazmat.primitives import hashes\nimport josepy as jose\nimport requests\n\nlogger = logging.getLogger(__name__)\n\nGenericChallenge = TypeVar('GenericChallenge', bound='Challenge')\n\n\nclass Challenge(jose.TypedJSONObjectWithFields):\n    # _fields_to_partial_json\n    \"\"\"ACME challenge.\"\"\"\n    TYPES: dict[str, type['Challenge']] = {}\n\n    @classmethod\n    def from_json(cls: type[GenericChallenge],\n                  jobj: Mapping[str, Any]) -> Union[GenericChallenge, 'UnrecognizedChallenge']:\n        try:\n            return cast(GenericChallenge, super().from_json(jobj))\n        except jose.UnrecognizedTypeError as error:\n            logger.debug(error)\n            return UnrecognizedChallenge.from_json(jobj)\n\n\nclass ChallengeResponse(jose.TypedJSONObjectWithFields):\n    # _fields_to_partial_json\n    \"\"\"ACME challenge response.\"\"\"\n    TYPES: dict[str, type['ChallengeResponse']] = {}\n\n    def to_partial_json(self) -> dict[str, Any]:\n        # Removes the `type` field which is inserted by TypedJSONObjectWithFields.to_partial_json.\n        # This field breaks RFC8555 compliance.\n        jobj = super().to_partial_json()\n        jobj.pop(self.type_field_name, None)\n        return jobj\n\n\nclass UnrecognizedChallenge(Challenge):\n    \"\"\"Unrecognized challenge.\n\n    ACME specification defines a generic framework for challenges and\n    defines some standard challenges that are implemented in this\n    module. However, other implementations (including peers) might\n    define additional challenge types, which should be ignored if\n    unrecognized.\n\n    :ivar jobj: Original JSON decoded object.\n\n    \"\"\"\n    jobj: dict[str, Any]\n\n    def __init__(self, jobj: Mapping[str, Any]) -> None:\n        super().__init__()\n        object.__setattr__(self, \"jobj\", jobj)\n\n    def to_partial_json(self) -> dict[str, Any]:\n        return self.jobj  # pylint: disable=no-member\n\n    @classmethod\n    def from_json(cls, jobj: Mapping[str, Any]) -> 'UnrecognizedChallenge':\n        return cls(jobj)\n\n\nclass _TokenChallenge(Challenge):\n    \"\"\"Challenge with token.\n\n    :ivar bytes token:\n\n    \"\"\"\n    TOKEN_SIZE = 128 // 8  # Based on the entropy value from the spec\n    \"\"\"Minimum size of the :attr:`token` in bytes.\"\"\"\n\n    # TODO: acme-spec doesn't specify token as base64-encoded value\n    token: bytes = jose.field(\n        \"token\", encoder=jose.encode_b64jose, decoder=functools.partial(\n            jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))\n\n    # XXX: rename to ~token_good_for_url\n    @property\n    def good_token(self) -> bool:  # XXX: @token.decoder\n        \"\"\"Is `token` good?\n\n        .. todo:: acme-spec wants \"It MUST NOT contain any non-ASCII\n           characters\", but it should also warrant that it doesn't\n           contain \"..\" or \"/\"...\n\n        \"\"\"\n        # TODO: check that path combined with uri does not go above\n        # URI_ROOT_PATH!\n        # pylint: disable=unsupported-membership-test\n        return b'..' not in self.token and b'/' not in self.token\n\n\nclass KeyAuthorizationChallengeResponse(ChallengeResponse):\n    \"\"\"Response to Challenges based on Key Authorization.\n\n    :param str key_authorization:\n\n    \"\"\"\n    key_authorization: str = jose.field(\"keyAuthorization\")\n    thumbprint_hash_function = hashes.SHA256\n\n    def verify(self, chall: 'KeyAuthorizationChallenge', account_public_key: jose.JWK) -> bool:\n        \"\"\"Verify the key authorization.\n\n        :param KeyAuthorization chall: Challenge that corresponds to\n            this response.\n        :param JWK account_public_key:\n\n        :return: ``True`` iff verification of the key authorization was\n            successful.\n        :rtype: bool\n\n        \"\"\"\n        parts = self.key_authorization.split('.')  # pylint: disable=no-member\n        if len(parts) != 2:\n            logger.debug(\"Key authorization (%r) is not well formed\",\n                         self.key_authorization)\n            return False\n\n        if parts[0] != chall.encode(\"token\"):\n            logger.debug(\"Mismatching token in key authorization: \"\n                         \"%r instead of %r\", parts[0], chall.encode(\"token\"))\n            return False\n\n        thumbprint = jose.b64encode(account_public_key.thumbprint(\n            hash_function=self.thumbprint_hash_function)).decode()\n        if parts[1] != thumbprint:\n            logger.debug(\"Mismatching thumbprint in key authorization: \"\n                         \"%r instead of %r\", parts[0], thumbprint)\n            return False\n\n        return True\n\n    def to_partial_json(self) -> dict[str, Any]:\n        jobj = super().to_partial_json()\n        jobj.pop('keyAuthorization', None)\n        return jobj\n\n\n# TODO: Make this method a generic of K (bound=KeyAuthorizationChallenge), response_cls of type\n#  Type[K] and use it in response/response_and_validation return types once Python 3.6 support is\n#  dropped (do not support generic ABC classes, see https://github.com/python/typing/issues/449).\nclass KeyAuthorizationChallenge(_TokenChallenge, metaclass=abc.ABCMeta):\n    \"\"\"Challenge based on Key Authorization.\n\n    :param response_cls: Subclass of `KeyAuthorizationChallengeResponse`\n        that will be used to generate ``response``.\n    :param str typ: type of the challenge\n    \"\"\"\n    typ: str = NotImplemented\n    response_cls: type[KeyAuthorizationChallengeResponse] = NotImplemented\n    thumbprint_hash_function = (\n        KeyAuthorizationChallengeResponse.thumbprint_hash_function)\n\n    def key_authorization(self, account_key: jose.JWK) -> str:\n        \"\"\"Generate Key Authorization.\n\n        :param JWK account_key:\n        :rtype str:\n\n        \"\"\"\n        return self.encode(\"token\") + \".\" + jose.b64encode(\n            account_key.thumbprint(\n                hash_function=self.thumbprint_hash_function)).decode()\n\n    def response(self, account_key: jose.JWK) -> KeyAuthorizationChallengeResponse:\n        \"\"\"Generate response to the challenge.\n\n        :param JWK account_key:\n\n        :returns: Response (initialized `response_cls`) to the challenge.\n        :rtype: KeyAuthorizationChallengeResponse\n\n        \"\"\"\n        return self.response_cls(  # pylint: disable=not-callable\n            key_authorization=self.key_authorization(account_key))\n\n    @abc.abstractmethod\n    def validation(self, account_key: jose.JWK, **kwargs: Any) -> Any:\n        \"\"\"Generate validation for the challenge.\n\n        Subclasses must implement this method, but they are likely to\n        return completely different data structures, depending on what's\n        necessary to complete the challenge. Interpretation of that\n        return value must be known to the caller.\n\n        :param JWK account_key:\n        :returns: Challenge-specific validation.\n\n        \"\"\"\n        raise NotImplementedError()  # pragma: no cover\n\n    def response_and_validation(self, account_key: jose.JWK, *args: Any, **kwargs: Any\n                                ) -> tuple[KeyAuthorizationChallengeResponse, Any]:\n        \"\"\"Generate response and validation.\n\n        Convenience function that return results of `response` and\n        `validation`.\n\n        :param JWK account_key:\n        :rtype: tuple\n\n        \"\"\"\n        return (self.response(account_key),\n                self.validation(account_key, *args, **kwargs))\n\n\n@ChallengeResponse.register\nclass DNS01Response(KeyAuthorizationChallengeResponse):\n    \"\"\"ACME dns-01 challenge response.\"\"\"\n    typ = \"dns-01\"\n\n    def simple_verify(self, chall: 'DNS01', domain: str, account_public_key: jose.JWK) -> bool:  # pylint: disable=unused-argument\n        \"\"\"Simple verify.\n\n        This method no longer checks DNS records and is a simple wrapper\n        around `KeyAuthorizationChallengeResponse.verify`.\n\n        :param challenges.DNS01 chall: Corresponding challenge.\n        :param str domain: Domain name being verified.\n        :param JWK account_public_key: Public key for the key pair\n            being authorized.\n\n        :return: ``True`` iff verification of the key authorization was\n            successful.\n        :rtype: bool\n\n        \"\"\"\n        verified = self.verify(chall, account_public_key)\n        if not verified:\n            logger.debug(\"Verification of key authorization in response failed\")\n        return verified\n\n\n@Challenge.register\nclass DNS01(KeyAuthorizationChallenge):\n    \"\"\"ACME dns-01 challenge.\"\"\"\n    response_cls = DNS01Response\n    typ = response_cls.typ\n\n    LABEL = \"_acme-challenge\"\n    \"\"\"Label clients prepend to the domain name being validated.\"\"\"\n\n    def validation(self, account_key: jose.JWK, **unused_kwargs: Any) -> str:\n        \"\"\"Generate validation.\n\n        :param JWK account_key:\n        :rtype: str\n\n        \"\"\"\n        return jose.b64encode(hashlib.sha256(self.key_authorization(\n            account_key).encode(\"utf-8\")).digest()).decode()\n\n    def validation_domain_name(self, name: str) -> str:\n        \"\"\"Domain name for TXT validation record.\n\n        :param str name: Domain name being validated.\n        :rtype: str\n\n        \"\"\"\n        return f\"{self.LABEL}.{name}\"\n\n\n@ChallengeResponse.register\nclass HTTP01Response(KeyAuthorizationChallengeResponse):\n    \"\"\"ACME http-01 challenge response.\"\"\"\n    typ = \"http-01\"\n\n    PORT = 80\n    \"\"\"Verification port as defined by the protocol.\n\n    You can override it (e.g. for testing) by passing ``port`` to\n    `simple_verify`.\n\n    \"\"\"\n\n    WHITESPACE_CUTSET = \"\\n\\r\\t \"\n    \"\"\"Whitespace characters which should be ignored at the end of the body.\"\"\"\n\n    def simple_verify(self, chall: 'HTTP01', domain: str, account_public_key: jose.JWK,\n                      port: Optional[int] = None, timeout: int = 30) -> bool:\n        \"\"\"Simple verify.\n\n        :param challenges.SimpleHTTP chall: Corresponding challenge.\n        :param str domain: Domain name being verified.\n        :param JWK account_public_key: Public key for the key pair\n            being authorized.\n        :param int port: Port used in the validation.\n        :param int timeout: Timeout in seconds.\n\n        :returns: ``True`` iff validation with the files currently served by the\n            HTTP server is successful.\n        :rtype: bool\n\n        \"\"\"\n        if not self.verify(chall, account_public_key):\n            logger.debug(\"Verification of key authorization in response failed\")\n            return False\n\n        # TODO: ACME specification defines URI template that doesn't\n        # allow to use a custom port... Make sure port is not in the\n        # request URI, if it's standard.\n        if port is not None and port != self.PORT:\n            logger.warning(\n                \"Using non-standard port for http-01 verification: %s\", port)\n            domain += \":{0}\".format(port)\n\n        uri = chall.uri(domain)\n        logger.debug(\"Verifying %s at %s...\", chall.typ, uri)\n        try:\n            http_response = requests.get(uri, verify=False, timeout=timeout)\n        except requests.exceptions.RequestException as error:\n            logger.error(\"Unable to reach %s: %s\", uri, error)\n            return False\n        # By default, http_response.text will try to guess the encoding to use\n        # when decoding the response to Python unicode strings. This guesswork\n        # is error prone. RFC 8555 specifies that HTTP-01 responses should be\n        # key authorizations with possible trailing whitespace. Since key\n        # authorizations must be composed entirely of the base64url alphabet\n        # plus \".\", we tell requests that the response should be ASCII. See\n        # https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 for more\n        # info.\n        http_response.encoding = \"ascii\"\n        logger.debug(\"Received %s: %s. Headers: %s\", http_response,\n                     http_response.text, http_response.headers)\n\n        challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET)\n        if self.key_authorization != challenge_response:\n            logger.debug(\"Key authorization from response (%r) doesn't match \"\n                         \"HTTP response (%r)\", self.key_authorization,\n                         challenge_response)\n            return False\n\n        return True\n\n\n@Challenge.register\nclass HTTP01(KeyAuthorizationChallenge):\n    \"\"\"ACME http-01 challenge.\"\"\"\n    response_cls = HTTP01Response\n    typ = response_cls.typ\n\n    URI_ROOT_PATH = \".well-known/acme-challenge\"\n    \"\"\"URI root path for the server provisioned resource.\"\"\"\n\n    @property\n    def path(self) -> str:\n        \"\"\"Path (starting with '/') for provisioned resource.\n\n        :rtype: str\n\n        \"\"\"\n        return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')\n\n    def uri(self, identifier: str) -> str:\n        \"\"\"Create an URI to the provisioned resource.\n\n        Forms an URI to the HTTPS server provisioned resource\n        (containing :attr:`~SimpleHTTP.token`).\n\n        :param str identifier: Domain name or IP address being verified.\n        :rtype: str\n\n        \"\"\"\n        try:\n            # https://datatracker.ietf.org/doc/html/rfc2732#section-2\n            # IPv6 addresses in URLs should be enclosed in brackets.\n            ipaddress.IPv6Address(identifier)\n            identifier = \"[\" + identifier + \"]\"\n        except ipaddress.AddressValueError:\n            pass\n        return \"http://\" + identifier + self.path\n\n    def validation(self, account_key: jose.JWK, **unused_kwargs: Any) -> str:\n        \"\"\"Generate validation.\n\n        :param JWK account_key:\n        :rtype: str\n\n        \"\"\"\n        return self.key_authorization(account_key)\n\n\n@Challenge.register\nclass DNS(_TokenChallenge):\n    \"\"\"ACME \"dns\" challenge.\"\"\"\n    typ = \"dns\"\n\n    LABEL = \"_acme-challenge\"\n    \"\"\"Label clients prepend to the domain name being validated.\"\"\"\n\n    def gen_validation(self, account_key: jose.JWK, alg: jose.JWASignature = jose.RS256,\n                       **kwargs: Any) -> jose.JWS:\n        \"\"\"Generate validation.\n\n        :param .JWK account_key: Private account key.\n        :param .JWA alg:\n\n        :returns: This challenge wrapped in `.JWS`\n        :rtype: .JWS\n\n        \"\"\"\n        return jose.JWS.sign(\n            payload=self.json_dumps(sort_keys=True).encode('utf-8'),\n            key=account_key, alg=alg, **kwargs)\n\n    def check_validation(self, validation: jose.JWS, account_public_key: jose.JWK) -> bool:\n        \"\"\"Check validation.\n\n        :param JWS validation:\n        :param JWK account_public_key:\n        :rtype: bool\n\n        \"\"\"\n        if not validation.verify(key=account_public_key):\n            return False\n        try:\n            return self == self.json_loads(\n                validation.payload.decode('utf-8'))\n        except jose.DeserializationError as error:\n            logger.debug(\"Checking validation for DNS failed: %s\", error)\n            return False\n\n    def gen_response(self, account_key: jose.JWK, **kwargs: Any) -> 'DNSResponse':\n        \"\"\"Generate response.\n\n        :param .JWK account_key: Private account key.\n        :param .JWA alg:\n\n        :rtype: DNSResponse\n\n        \"\"\"\n        return DNSResponse(validation=self.gen_validation(account_key, **kwargs))\n\n    def validation_domain_name(self, name: str) -> str:\n        \"\"\"Domain name for TXT validation record.\n\n        :param str name: Domain name being validated.\n\n        \"\"\"\n        return \"{0}.{1}\".format(self.LABEL, name)\n\n\n@ChallengeResponse.register\nclass DNSResponse(ChallengeResponse):\n    \"\"\"ACME \"dns\" challenge response.\n\n    :param JWS validation:\n\n    \"\"\"\n    typ = \"dns\"\n\n    validation: jose.JWS = jose.field(\"validation\", decoder=jose.JWS.from_json)\n\n    def check_validation(self, chall: 'DNS', account_public_key: jose.JWK) -> bool:\n        \"\"\"Check validation.\n\n        :param challenges.DNS chall:\n        :param JWK account_public_key:\n\n        :rtype: bool\n\n        \"\"\"\n        return chall.check_validation(self.validation, account_public_key)\n"
  },
  {
    "path": "acme/src/acme/client.py",
    "content": "\"\"\"ACME client API.\"\"\"\nimport base64\nimport datetime\nfrom email.utils import parsedate_tz\nimport http.client as http_client\nimport logging\nimport math\nimport random\nimport time\nfrom typing import Any\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\n\nfrom cryptography import x509\n\nimport josepy as jose\nimport requests\nfrom requests.adapters import HTTPAdapter\nfrom requests.utils import parse_header_links\n\nfrom acme import challenges\nfrom acme import crypto_util\nfrom acme import errors\nfrom acme import jws\nfrom acme import messages\n\nlogger = logging.getLogger(__name__)\n\nDEFAULT_NETWORK_TIMEOUT = 45\n\n\nclass ClientV2:\n    \"\"\"ACME client for a v2 API.\n\n    :ivar messages.Directory directory:\n    :ivar .ClientNetwork net: Client network.\n    \"\"\"\n\n    def __init__(self, directory: messages.Directory, net: 'ClientNetwork') -> None:\n        \"\"\"Initialize.\n\n        :param .messages.Directory directory: Directory Resource\n        :param .ClientNetwork net: Client network.\n        \"\"\"\n        self.directory = directory\n        self.net = net\n\n    def new_account(self, new_account: messages.NewRegistration) -> messages.RegistrationResource:\n        \"\"\"Register.\n\n        :param .NewRegistration new_account:\n\n        :raises .ConflictError: in case the account already exists\n\n        :returns: Registration Resource.\n        :rtype: `.RegistrationResource`\n        \"\"\"\n        response = self._post(self.directory['newAccount'], new_account)\n        # if account already exists\n        if response.status_code == 200 and 'Location' in response.headers:\n            raise errors.ConflictError(response.headers['Location'])\n        # \"Instance of 'Field' has no key/contact member\" bug:\n        regr = self._regr_from_response(response)\n        self.net.account = regr\n        return regr\n\n    def query_registration(self, regr: messages.RegistrationResource\n                           ) -> messages.RegistrationResource:\n        \"\"\"Query server about registration.\n\n        :param messages.RegistrationResource regr: Existing Registration\n            Resource.\n\n        \"\"\"\n        self.net.account = self._get_v2_account(regr, True)\n\n        return self.net.account\n\n    def update_registration(self, regr: messages.RegistrationResource,\n                            update: Optional[messages.Registration] = None\n                            ) -> messages.RegistrationResource:\n        \"\"\"Update registration.\n\n        :param messages.RegistrationResource regr: Registration Resource.\n        :param messages.Registration update: Updated body of the\n            resource. If not provided, body will be taken from `regr`.\n\n        :returns: Updated Registration Resource.\n        :rtype: `.RegistrationResource`\n\n        \"\"\"\n        # https://github.com/certbot/certbot/issues/6155\n        regr = self._get_v2_account(regr)\n\n        update = regr.body if update is None else update\n        body = messages.UpdateRegistration(**dict(update))\n        updated_regr = self._send_recv_regr(regr, body=body)\n        self.net.account = updated_regr\n        return updated_regr\n\n    def _get_v2_account(self, regr: messages.RegistrationResource, update_body: bool = False\n                       ) -> messages.RegistrationResource:\n        self.net.account = None\n        only_existing_reg = regr.body.update(only_return_existing=True)\n        response = self._post(self.directory['newAccount'], only_existing_reg)\n        updated_uri = response.headers['Location']\n        new_regr = regr.update(body=messages.Registration.from_json(response.json())\n                               if update_body else regr.body,\n                               uri=updated_uri)\n        self.net.account = new_regr\n        return new_regr\n\n    def new_order(self, csr_pem: bytes, profile: Optional[str] = None) -> messages.OrderResource:\n        \"\"\"Request a new Order object from the server.\n\n        :param bytes csr_pem: A CSR in PEM format.\n\n        :returns: The newly created order.\n        :rtype: OrderResource\n        \"\"\"\n        csr = x509.load_pem_x509_csr(csr_pem)\n        dns_names, ip_addrs  = crypto_util.get_identifiers_from_x509(csr.subject, csr.extensions)\n        identifiers = []\n        for name in dns_names:\n            identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,\n                value=name))\n        for ip in ip_addrs:\n            identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_IP,\n                value=str(ip)))\n        if profile is None:\n            profile = \"\"\n        order = messages.NewOrder(identifiers=identifiers, profile=profile)\n        response = self._post(self.directory['newOrder'], order)\n        body = messages.Order.from_json(response.json())\n        authorizations = []\n        # pylint has trouble understanding our josepy based objects which use\n        # things like custom metaclass logic. body.authorizations should be a\n        # list of strings containing URLs so let's disable this check here.\n        for url in body.authorizations:  # pylint: disable=not-an-iterable\n            authorizations.append(self._authzr_from_response(self._post_as_get(url), uri=url))\n        return messages.OrderResource(\n            body=body,\n            uri=response.headers.get('Location'),\n            authorizations=authorizations,\n            csr_pem=csr_pem)\n\n    def poll(self, authzr: messages.AuthorizationResource\n             ) -> tuple[messages.AuthorizationResource, requests.Response]:\n        \"\"\"Poll Authorization Resource for status.\n\n        :param authzr: Authorization Resource\n        :type authzr: `.AuthorizationResource`\n\n        :returns: Updated Authorization Resource and HTTP response.\n\n        :rtype: (`.AuthorizationResource`, `requests.Response`)\n\n        \"\"\"\n        response = self._post_as_get(authzr.uri)\n        updated_authzr = self._authzr_from_response(\n            response, authzr.body.identifier, authzr.uri)\n        return updated_authzr, response\n\n    def poll_and_finalize(self, orderr: messages.OrderResource,\n                          deadline: Optional[datetime.datetime] = None) -> messages.OrderResource:\n        \"\"\"Poll authorizations and finalize the order.\n\n        If no deadline is provided, this method will timeout after 90\n        seconds.\n\n        :param messages.OrderResource orderr: order to finalize\n        :param datetime.datetime deadline: when to stop polling and timeout\n\n        :returns: finalized order\n        :rtype: messages.OrderResource\n\n        \"\"\"\n        if deadline is None:\n            deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)\n        orderr = self.poll_authorizations(orderr, deadline)\n        return self.finalize_order(orderr, deadline)\n\n    def poll_authorizations(self, orderr: messages.OrderResource, deadline: datetime.datetime\n                            ) -> messages.OrderResource:\n        \"\"\"Poll Order Resource for status.\"\"\"\n        responses = []\n        for url in orderr.body.authorizations:\n            while datetime.datetime.now() < deadline:\n                authzr = self._authzr_from_response(self._post_as_get(url), uri=url)\n                if authzr.body.status != messages.STATUS_PENDING:  # pylint: disable=no-member\n                    responses.append(authzr)\n                    break\n                time.sleep(1)\n        # If we didn't get a response for every authorization, we fell through\n        # the bottom of the loop due to hitting the deadline.\n        if len(responses) < len(orderr.body.authorizations):\n            raise errors.TimeoutError()\n        failed = []\n        for authzr in responses:\n            if authzr.body.status != messages.STATUS_VALID:\n                for chall in authzr.body.challenges:\n                    if chall.error is not None:\n                        failed.append(authzr)\n        if failed:\n            raise errors.ValidationError(failed)\n        return orderr.update(authorizations=responses)\n\n    def begin_finalization(self, orderr: messages.OrderResource\n                           ) -> messages.OrderResource:\n        \"\"\"Start the process of finalizing an order.\n\n        :param messages.OrderResource orderr: order to finalize\n        :param datetime.datetime deadline: when to stop polling and timeout\n\n        :returns: updated order\n        :rtype: messages.OrderResource\n\n        :raises .messages.Error: If server indicates order is not yet in ready state,\n            it will return a 403 (Forbidden) error with a problem document/error code of type\n            \"orderNotReady\"\n\n        \"\"\"\n        csr = x509.load_pem_x509_csr(orderr.csr_pem)\n        wrapped_csr = messages.CertificateRequest(csr=csr)\n        res = self._post(orderr.body.finalize, wrapped_csr)\n        orderr = orderr.update(body=messages.Order.from_json(res.json()))\n        return orderr\n\n    def poll_finalization(self, orderr: messages.OrderResource,\n                          deadline: datetime.datetime,\n                          fetch_alternative_chains: bool = False\n                          ) -> messages.OrderResource:\n        \"\"\"\n        Poll an order that has been finalized for its status.\n        If it becomes valid, obtain the certificate.\n\n        If a finalization request previously returned `orderNotReady`,\n        poll until ready, send a new finalization request, and continue\n        polling until valid as above.\n\n        :returns: finalized order (with certificate)\n        :rtype: messages.OrderResource\n        \"\"\"\n        sleep_seconds: float = 1\n        while datetime.datetime.now() < deadline:\n            if sleep_seconds > 0:\n                time.sleep(sleep_seconds)\n            response = self._post_as_get(orderr.uri)\n            body = messages.Order.from_json(response.json())\n            if body.status == messages.STATUS_INVALID:\n                # \"invalid\": The certificate will not be issued.  Consider this\n                # order process abandoned.\n                if body.error is not None:\n                    raise errors.IssuanceError(body.error)\n                raise errors.Error(\n                    \"The certificate order failed. No further information was provided \"\n                    \"by the server.\")\n            elif body.status == messages.STATUS_READY:\n                # \"ready\": The server agrees that the requirements have been\n                # fulfilled, and is awaiting finalization.  Submit a finalization\n                # request.\n                self.begin_finalization(orderr)\n                sleep_seconds = 1\n            elif body.status == messages.STATUS_VALID and body.certificate is not None:\n                # \"valid\": The server has issued the certificate and provisioned its\n                # URL to the \"certificate\" field of the order.  Download the\n                # certificate.\n                certificate_response = self._post_as_get(body.certificate)\n                orderr = orderr.update(body=body, fullchain_pem=certificate_response.text)\n                if fetch_alternative_chains:\n                    alt_chains_urls = self._get_links(certificate_response, 'alternate')\n                    alt_chains = [self._post_as_get(url).text for url in alt_chains_urls]\n                    orderr = orderr.update(alternative_fullchains_pem=alt_chains)\n                return orderr\n            elif body.status == messages.STATUS_PROCESSING:\n                # \"processing\": The certificate is being issued.  Send a POST-as-GET request after\n                # the time given in the Retry-After header field of the response, if any.\n                retry_after = self.retry_after(response, 1)\n                # Whatever Retry-After the ACME server requests, the polling must not take\n                # longer than the overall deadline\n                retry_after = min(retry_after, deadline)\n                sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds()\n        raise errors.TimeoutError()\n\n    def finalize_order(self, orderr: messages.OrderResource, deadline: datetime.datetime,\n                       fetch_alternative_chains: bool = False) -> messages.OrderResource:\n        \"\"\"Finalize an order and obtain a certificate.\n\n        :param messages.OrderResource orderr: order to finalize\n        :param datetime.datetime deadline: when to stop polling and timeout\n        :param bool fetch_alternative_chains: whether to also fetch alternative\n            certificate chains\n\n        :returns: finalized order\n        :rtype: messages.OrderResource\n\n        \"\"\"\n        try:\n            self.begin_finalization(orderr)\n        except messages.Error as e:\n            if e.code != 'orderNotReady':\n                raise e\n        return self.poll_finalization(orderr, deadline, fetch_alternative_chains)\n\n    def renewal_time(self, cert_pem: bytes\n        ) -> tuple[Optional[datetime.datetime], datetime.datetime]:\n        \"\"\"Return an appropriate time to attempt renewal of the certificate,\n        and the next time to ask the ACME server for renewal info.\n\n        If the certificate has already expired, renewal info isn't checked.\n        Instead, the certificate's notAfter time is returned and the certificate\n        should be immediately renewed.\n\n        If the ACME directory has a \"renewalInfo\" field, the response will be\n        based on a fetch of the renewal info resource for the certificate\n        (https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html).\n\n        If there is no \"renewalInfo\" field, this function will return a tuple of\n        None, and the next time to ask the ACME server for renewal info.\n\n        This function may make other network calls in the future (e.g., OCSP\n        or CRL).\n\n        :param bytes cert_pem: cert as pem file\n\n        :returns: Tuple of time to attempt renewal, next time to ask for renewal info\n\n        :raises errors.ARIError: If an error occurs fetching ARI from the\n            server. Explicit exception chaining is used so the original error\n            can be accessed through the __cause__ attribute on the ARIError if\n            desired.\n\n        \"\"\"\n        now = datetime.datetime.now()\n        # https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html#section-4.3.3\n        default_retry_after = datetime.timedelta(seconds=6 * 60 * 60)\n\n        cert = x509.load_pem_x509_certificate(cert_pem)\n\n        # from https://www.ietf.org/archive/id/draft-ietf-acme-ari-08.html#section-4.3, \"Clients\n        # MUST NOT check a certificate's RenewalInfo after the certificate has expired.\"\n        #\n        # we call datetime.datetime.now here with the UTC argument to create a timezone aware\n        # datetime object that can be compared with the UTC notAfter from cryptography\n        if cert.not_valid_after_utc < datetime.datetime.now(datetime.timezone.utc):\n            return cert.not_valid_after_utc, now + default_retry_after\n\n        try:\n            renewal_info_base_url = self.directory['renewalInfo']\n        except KeyError:\n            return None, now + default_retry_after\n\n        ari_url = renewal_info_base_url + '/' + _renewal_info_path_component(cert)\n        try:\n            resp = self.net.get(ari_url, content_type='application/json')\n        except Exception as e:  # pylint: disable=broad-except\n            error_msg = f'failed to fetch renewal_info URL {ari_url}'\n            raise errors.ARIError(error_msg, now + default_retry_after) from e\n        renewal_info: messages.RenewalInfo = messages.RenewalInfo.from_json(resp.json())\n\n        start = renewal_info.suggested_window.start # pylint: disable=no-member\n        end = renewal_info.suggested_window.end # pylint: disable=no-member\n\n        delta_seconds = (end - start).total_seconds()\n        random_seconds = random.uniform(0, delta_seconds)\n        random_time = start + datetime.timedelta(seconds=random_seconds)\n\n        retry_after = self.retry_after(resp, default_retry_after.seconds)\n        return random_time, retry_after\n\n\n    def revoke(self, cert: x509.Certificate, rsn: int) -> None:\n        \"\"\"Revoke certificate.\n\n        :param x509.Certificate cert: `x509.Certificate`\n\n        :param int rsn: Reason code for certificate revocation.\n\n        :raises .ClientError: If revocation is unsuccessful.\n\n        \"\"\"\n        self._revoke(cert, rsn, self.directory['revokeCert'])\n\n    def external_account_required(self) -> bool:\n        \"\"\"Checks if ACME server requires External Account Binding authentication.\"\"\"\n        return hasattr(self.directory, 'meta') and \\\n               hasattr(self.directory.meta, 'external_account_required') and \\\n               self.directory.meta.external_account_required\n\n    def _post_as_get(self, *args: Any, **kwargs: Any) -> requests.Response:\n        \"\"\"\n        Send GET request using the POST-as-GET protocol.\n        :param args:\n        :param kwargs:\n        :return:\n        \"\"\"\n        new_args = args[:1] + (None,) + args[1:]\n        return self._post(*new_args, **kwargs)\n\n    def _get_links(self, response: requests.Response, relation_type: str) -> list[str]:\n        \"\"\"\n        Retrieves all Link URIs of relation_type from the response.\n        :param requests.Response response: The requests HTTP response.\n        :param str relation_type: The relation type to filter by.\n        \"\"\"\n        # Can't use response.links directly because it drops multiple links\n        # of the same relation type, which is possible in RFC8555 responses.\n        if 'Link' not in response.headers:\n            return []\n        links = parse_header_links(response.headers['Link'])\n        return [link['url'] for link in links\n                if 'rel' in link and 'url' in link and link['rel'] == relation_type]\n\n    @classmethod\n    def get_directory(cls, url: str, net: 'ClientNetwork') -> messages.Directory:\n        \"\"\"\n        Retrieves the ACME directory (RFC 8555 section 7.1.1) from the ACME server.\n        :param str url: the URL where the ACME directory is available\n        :param ClientNetwork net: the ClientNetwork to use to make the request\n\n        :returns: the ACME directory object\n        :rtype: messages.Directory\n        \"\"\"\n        return messages.Directory.from_json(net.get(url).json())\n\n    @classmethod\n    def _regr_from_response(cls, response: requests.Response, uri: Optional[str] = None,\n                            terms_of_service: Optional[str] = None\n                            ) -> messages.RegistrationResource:\n        if 'terms-of-service' in response.links:\n            terms_of_service = response.links['terms-of-service']['url']\n\n        return messages.RegistrationResource(\n            body=messages.Registration.from_json(response.json()),\n            uri=response.headers.get('Location', uri),\n            terms_of_service=terms_of_service)\n\n    def _send_recv_regr(self, regr: messages.RegistrationResource,\n                        body: messages.Registration) -> messages.RegistrationResource:\n        response = self._post(regr.uri, body)\n\n        # TODO: Boulder returns httplib.ACCEPTED\n        #assert response.status_code == httplib.OK\n\n        # TODO: Boulder does not set Location or Link on update\n        # (c.f. acme-spec #94)\n\n        return self._regr_from_response(\n            response, uri=regr.uri,\n            terms_of_service=regr.terms_of_service)\n\n    def _post(self, *args: Any, **kwargs: Any) -> requests.Response:\n        \"\"\"Wrapper around self.net.post that adds the newNonce URL.\n\n        This is used to retry the request in case of a badNonce error.\n\n        \"\"\"\n        kwargs.setdefault('new_nonce_url', getattr(self.directory, 'newNonce'))\n        return self.net.post(*args, **kwargs)\n\n    def deactivate_registration(self, regr: messages.RegistrationResource\n                                ) -> messages.RegistrationResource:\n        \"\"\"Deactivate registration.\n\n        :param messages.RegistrationResource regr: The Registration Resource\n            to be deactivated.\n\n        :returns: The Registration resource that was deactivated.\n        :rtype: `.RegistrationResource`\n\n        \"\"\"\n        return self.update_registration(regr, messages.Registration.from_json(\n            {\"status\": \"deactivated\", \"contact\": None}))\n\n    def deactivate_authorization(self,\n                                 authzr: messages.AuthorizationResource\n                                 ) -> messages.AuthorizationResource:\n        \"\"\"Deactivate authorization.\n\n        :param messages.AuthorizationResource authzr: The Authorization resource\n            to be deactivated.\n\n        :returns: The Authorization resource that was deactivated.\n        :rtype: `.AuthorizationResource`\n\n        \"\"\"\n        body = messages.UpdateAuthorization(status='deactivated')\n        response = self._post(authzr.uri, body)\n        return self._authzr_from_response(response,\n            authzr.body.identifier, authzr.uri)\n\n    def _authzr_from_response(self, response: requests.Response,\n                              identifier: Optional[messages.Identifier] = None,\n                              uri: Optional[str] = None) -> messages.AuthorizationResource:\n        authzr = messages.AuthorizationResource(\n            body=messages.Authorization.from_json(response.json()),\n            uri=response.headers.get('Location', uri))\n        if identifier is not None and authzr.body.identifier != identifier:  # pylint: disable=no-member\n            raise errors.UnexpectedUpdate(authzr)\n        return authzr\n\n    def answer_challenge(self, challb: messages.ChallengeBody,\n                         response: challenges.ChallengeResponse) -> messages.ChallengeResource:\n        \"\"\"Answer challenge.\n\n        :param challb: Challenge Resource body.\n        :type challb: `.ChallengeBody`\n\n        :param response: Corresponding Challenge response\n        :type response: `.challenges.ChallengeResponse`\n\n        :returns: Challenge Resource with updated body.\n        :rtype: `.ChallengeResource`\n\n        :raises .UnexpectedUpdate:\n\n        \"\"\"\n        resp = self._post(challb.uri, response)\n        try:\n            authzr_uri = resp.links['up']['url']\n        except KeyError:\n            raise errors.ClientError('\"up\" Link header missing')\n        challr = messages.ChallengeResource(\n            authzr_uri=authzr_uri,\n            body=messages.ChallengeBody.from_json(resp.json()))\n        # TODO: check that challr.uri == resp.headers['Location']?\n        if challr.uri != challb.uri:\n            raise errors.UnexpectedUpdate(challr.uri)\n        return challr\n\n    @classmethod\n    def retry_after(cls, response: requests.Response, default: int) -> datetime.datetime:\n        \"\"\"Compute next `poll` time based on response ``Retry-After`` header.\n\n        Handles integers and various datestring formats per\n        https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37\n\n        :param requests.Response response: Response from `poll`.\n        :param int default: Default value (in seconds), used when\n            ``Retry-After`` header is not present or invalid.\n\n        :returns: Time point when next `poll` should be performed.\n        :rtype: `datetime.datetime`\n\n        \"\"\"\n        retry_after = response.headers.get('Retry-After', str(default))\n        try:\n            seconds = int(retry_after)\n        except ValueError:\n            # The RFC 2822 parser handles all of RFC 2616's cases in modern\n            # environments (primarily HTTP 1.1+ but also py27+)\n            when = parsedate_tz(retry_after)\n            if when is not None:\n                try:\n                    tz_secs = datetime.timedelta(when[-1] if when[-1] is not None else 0)\n                    return datetime.datetime(*when[:7]) - tz_secs\n                except (ValueError, OverflowError):\n                    pass\n            seconds = default\n\n        return datetime.datetime.now() + datetime.timedelta(seconds=seconds)\n\n    def _revoke(self, cert: x509.Certificate, rsn: int, url: str) -> None:\n        \"\"\"Revoke certificate.\n\n        :param .x509.Certificate cert: `x509.Certificate`\n\n        :param int rsn: Reason code for certificate revocation.\n\n        :param str url: ACME URL to post to\n\n        :raises .ClientError: If revocation is unsuccessful.\n\n        \"\"\"\n        response = self._post(url,\n                              messages.Revocation(\n                                certificate=cert,\n                                reason=rsn))\n        if response.status_code != http_client.OK:\n            raise errors.ClientError(\n                'Successful revocation must return HTTP OK status')\n\n\nclass ClientNetwork:\n    \"\"\"Wrapper around requests that signs POSTs for authentication.\n\n    Also adds user agent, and handles Content-Type.\n    \"\"\"\n    JSON_CONTENT_TYPE = 'application/json'\n    JOSE_CONTENT_TYPE = 'application/jose+json'\n    JSON_ERROR_CONTENT_TYPE = 'application/problem+json'\n    REPLAY_NONCE_HEADER = 'Replay-Nonce'\n\n    \"\"\"Initialize.\n\n    :param josepy.JWK key: Account private key. Required to use .post().\n    :param messages.RegistrationResource account: Account object. Required if you are\n            planning to use .post() for anything other than creating a new account;\n            may be set later after registering.\n    :param josepy.JWASignature alg: Algorithm to use in signing JWS.\n    :param bool verify_ssl: Whether to verify certificates on SSL connections.\n    :param str user_agent: String to send as User-Agent header.\n    :param int timeout: Timeout for requests.\n    \"\"\"\n    def __init__(self, key: Optional[jose.JWK] = None,\n                 account: Optional[messages.RegistrationResource] = None,\n                 alg: jose.JWASignature = jose.RS256, verify_ssl: bool = True,\n                 user_agent: str = 'acme-python', timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:\n        self.key = key\n        self.account = account\n        self.alg = alg\n        self.verify_ssl = verify_ssl\n        self._nonces: set[str] = set()\n        self.user_agent = user_agent\n        self.session = requests.Session()\n        self._default_timeout = timeout\n        adapter = HTTPAdapter()\n\n        self.session.mount(\"http://\", adapter)\n        self.session.mount(\"https://\", adapter)\n\n    def __del__(self) -> None:\n        # Try to close the session, but don't show exceptions to the\n        # user if the call to close() fails. See #4840.\n        try:\n            self.session.close()\n        except Exception:  # pylint: disable=broad-except\n            pass\n\n    def _wrap_in_jws(self, obj: jose.JSONDeSerializable, nonce: str, url: str) -> str:\n        \"\"\"Wrap `JSONDeSerializable` object in JWS.\n\n        .. todo:: Implement ``acmePath``.\n\n        :param josepy.JSONDeSerializable obj:\n        :param str url: The URL to which this object will be POSTed\n        :param str nonce:\n        :rtype: str\n\n        \"\"\"\n        jobj = obj.json_dumps(indent=2).encode() if obj else b''\n        logger.debug('JWS payload:\\n%s', jobj)\n        assert self.key\n        kwargs = {\n            \"alg\": self.alg,\n            \"nonce\": nonce,\n            \"url\": url,\n            \"key\": self.key\n        }\n        # newAccount and revokeCert work without the kid\n        # newAccount must not have kid\n        if self.account is not None:\n            kwargs[\"kid\"] = self.account[\"uri\"]\n        return jws.JWS.sign(jobj, **cast(Mapping[str, Any], kwargs)).json_dumps(indent=2)\n\n    @classmethod\n    def _check_response(cls, response: requests.Response,\n                        content_type: Optional[str] = None) -> requests.Response:\n        \"\"\"Check response content and its type.\n\n        .. note::\n           Checking is not strict: wrong server response ``Content-Type``\n           HTTP header is ignored if response is an expected JSON object\n           (c.f. Boulder #56).\n\n        :param str content_type: Expected Content-Type response header.\n            If JSON is expected and not present in server response, this\n            function will raise an error. Otherwise, wrong Content-Type\n            is ignored, but logged.\n\n        :raises .messages.Error: If server response body\n            carries HTTP Problem (https://datatracker.ietf.org/doc/html/rfc7807).\n        :raises .ClientError: In case of other networking errors.\n\n        \"\"\"\n        response_ct = response.headers.get('Content-Type')\n        # Strip parameters from the media-type (rfc2616#section-3.7)\n        if response_ct:\n            response_ct = response_ct.split(';')[0].strip()\n        try:\n            # TODO: response.json() is called twice, once here, and\n            # once in _get and _post clients\n            jobj = response.json()\n        except ValueError:\n            jobj = None\n\n        if response.status_code == 409:\n            raise errors.ConflictError(response.headers.get('Location', 'UNKNOWN-LOCATION'))\n\n        if not response.ok:\n            if jobj is not None:\n                if response_ct != cls.JSON_ERROR_CONTENT_TYPE:\n                    logger.debug(\n                        'Ignoring wrong Content-Type (%r) for JSON Error',\n                        response_ct)\n                try:\n                    raise messages.Error.from_json(jobj)\n                except jose.DeserializationError as error:\n                    # Couldn't deserialize JSON object\n                    raise errors.ClientError((response, error))\n            else:\n                # response is not JSON object\n                raise errors.ClientError(response)\n        else:\n            if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:\n                logger.debug(\n                    'Ignoring wrong Content-Type (%r) for JSON decodable '\n                    'response', response_ct)\n\n            if content_type == cls.JSON_CONTENT_TYPE and jobj is None:\n                raise errors.ClientError(f'Unexpected response Content-Type: {response_ct}')\n\n        return response\n\n    def _send_request(self, method: str, url: str, *args: Any, **kwargs: Any) -> requests.Response:\n        \"\"\"Send HTTP request.\n\n        Makes sure that `verify_ssl` is respected. Logs request and\n        response (with headers). For allowed parameters please see\n        `requests.request`.\n\n        :param str method: method for the new `requests.Request` object\n        :param str url: URL for the new `requests.Request` object\n\n        :raises requests.exceptions.RequestException: in case of any problems\n\n        :returns: HTTP Response\n        :rtype: `requests.Response`\n\n\n        \"\"\"\n        if method == \"POST\":\n            logger.debug('Sending POST request to %s:\\n%s',\n                          url, kwargs['data'])\n        else:\n            logger.debug('Sending %s request to %s.', method, url)\n        kwargs['verify'] = self.verify_ssl\n        kwargs.setdefault('headers', {})\n        kwargs['headers'].setdefault('User-Agent', self.user_agent)\n        kwargs.setdefault('timeout', self._default_timeout)\n        response = self.session.request(method, url, *args, **kwargs)\n\n        # If an Accept header was sent in the request, the response may not be\n        # UTF-8 encoded. In this case, we don't set response.encoding and log\n        # the base64 response instead of raw bytes to keep binary data out of the logs.\n        debug_content: Union[bytes, str]\n        if \"Accept\" in kwargs[\"headers\"]:\n            debug_content = base64.b64encode(response.content)\n        else:\n            # We set response.encoding so response.text knows the response is\n            # UTF-8 encoded instead of trying to guess the encoding that was\n            # used which is error prone. This setting affects all future\n            # accesses of .text made on the returned response object as well.\n            response.encoding = \"utf-8\"\n            debug_content = response.text\n        logger.debug('Received response:\\nHTTP %d\\n%s\\n\\n%s',\n                     response.status_code,\n                     \"\\n\".join(\"{0}: {1}\".format(k, v)\n                                for k, v in response.headers.items()),\n                     debug_content)\n        return response\n\n    def head(self, *args: Any, **kwargs: Any) -> requests.Response:\n        \"\"\"Send HEAD request without checking the response.\n\n        Note, that `_check_response` is not called, as it is expected\n        that status code other than successfully 2xx will be returned, or\n        messages2.Error will be raised by the server.\n\n        \"\"\"\n        return self._send_request('HEAD', *args, **kwargs)\n\n    def get(self, url: str, content_type: str = JSON_CONTENT_TYPE,\n            **kwargs: Any) -> requests.Response:\n        \"\"\"Send GET request and check response.\"\"\"\n        return self._check_response(\n            self._send_request('GET', url, **kwargs), content_type=content_type)\n\n    def _add_nonce(self, response: requests.Response) -> None:\n        if self.REPLAY_NONCE_HEADER in response.headers:\n            nonce = response.headers[self.REPLAY_NONCE_HEADER]\n            try:\n                decoded_nonce = jws.Header._fields['nonce'].decode(nonce)\n            except jose.DeserializationError as error:\n                raise errors.BadNonce(nonce, error)\n            logger.debug('Storing nonce: %s', nonce)\n            self._nonces.add(decoded_nonce)\n        else:\n            raise errors.MissingNonce(response)\n\n    def _get_nonce(self, url: str, new_nonce_url: str) -> str:\n        if not self._nonces:\n            logger.debug('Requesting fresh nonce')\n            if new_nonce_url is None:\n                response = self.head(url)\n            else:\n                # request a new nonce from the acme newNonce endpoint\n                response = self._check_response(self.head(new_nonce_url), content_type=None)\n            self._add_nonce(response)\n        return self._nonces.pop()\n\n    def post(self, *args: Any, **kwargs: Any) -> requests.Response:\n        \"\"\"POST object wrapped in `.JWS` and check response.\n\n        If the server responded with a badNonce error, the request will\n        be retried once.\n\n        \"\"\"\n        try:\n            return self._post_once(*args, **kwargs)\n        except messages.Error as error:\n            if error.code == 'badNonce':\n                logger.debug('Retrying request after error:\\n%s', error)\n                return self._post_once(*args, **kwargs)\n            raise\n\n    def _post_once(self, url: str, obj: jose.JSONDeSerializable,\n                   content_type: str = JOSE_CONTENT_TYPE, **kwargs: Any) -> requests.Response:\n        new_nonce_url = kwargs.pop('new_nonce_url', None)\n        if not self.key:\n            raise errors.Error(\"acme.ClientNetwork with no private key can't POST.\")\n        data = self._wrap_in_jws(obj, self._get_nonce(url, new_nonce_url), url)\n        kwargs.setdefault('headers', {'Content-Type': content_type})\n        response = self._send_request('POST', url, data=data, **kwargs)\n        response = self._check_response(response, content_type=content_type)\n        self._add_nonce(response)\n        return response\n\ndef _renewal_info_path_component(cert: x509.Certificate) -> str:\n    akid_ext = cert.extensions.get_extension_for_oid(x509.ExtensionOID.AUTHORITY_KEY_IDENTIFIER)\n    key_identifier = akid_ext.value.key_identifier # type: ignore[attr-defined]\n\n    akid_encoded = base64.urlsafe_b64encode(key_identifier).decode('ascii').replace(\"=\", \"\")\n\n    # We add one to the reported bit_length so there is room for the sign bit.\n    # https://docs.python.org/3/library/stdtypes.html#int.bit_length\n    # \"Return the number of bits necessary to represent an integer in binary, excluding\n    # the sign and leading zeros\"\n    serial = cert.serial_number\n    encoded_serial_len = math.ceil((serial.bit_length()+1)/8)\n    # Serials are encoded as ASN.1 INTEGERS, which means big endian and signed (two's complement).\n    # https://letsencrypt.org/docs/a-warm-welcome-to-asn1-and-der/#integer-encoding\n    serial_bytes = serial.to_bytes(encoded_serial_len, byteorder='big', signed=True)\n    serial_encoded = base64.urlsafe_b64encode(serial_bytes).decode('ascii').replace(\"=\", \"\")\n\n    return f\"{akid_encoded}.{serial_encoded}\"\n"
  },
  {
    "path": "acme/src/acme/crypto_util.py",
    "content": "\"\"\"Crypto utilities.\"\"\"\nimport enum\nfrom datetime import datetime, timedelta, timezone\nimport ipaddress\nimport logging\nfrom types import ModuleType\nimport typing\nfrom typing import Any\nfrom typing import Literal\nfrom typing import Optional\nfrom typing import Union\nimport warnings\nimport sys\n\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import hashes, serialization\nfrom cryptography.hazmat.primitives.asymmetric import dsa, rsa, ec, ed25519, ed448, types\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom OpenSSL import crypto\n\nlogger = logging.getLogger(__name__)\n\n\n# https://github.com/pyca/cryptography/blob/1eab7a67dcc34b568e3c0df64e6222a2ac74b1ee/src/cryptography/utils.py#L66-L113\nclass _ClientDeprecationModule(ModuleType):\n    \"\"\"\n    Internal class delegating to a module, and displaying warnings when attributes\n    related to deprecated attributes in the acme.client module.\n    \"\"\"\n    def __init__(self, module: ModuleType) -> None:\n        super().__init__(module.__name__)\n        self.__dict__['_module'] = module\n\n    def __getattr__(self, attr: str) -> Any:\n        if attr == 'Format':\n            warnings.warn(\"acme.crypto_util.Format is deprecated and will be removed in \"\n                \"the next major release.\", DeprecationWarning)\n        return getattr(self._module, attr)\n\n    def __setattr__(self, attr: str, value: Any) -> None:  # pragma: no cover\n        setattr(self._module, attr, value)\n\n    def __delattr__(self, attr: str) -> None:  # pragma: no cover\n        delattr(self._module, attr)\n\n    def __dir__(self) -> list[str]:  # pragma: no cover\n        return ['_module'] + dir(self._module)\n\n\n# Patching ourselves to warn about deprecation and planned removal of some elements in the module.\nsys.modules[__name__] = _ClientDeprecationModule(sys.modules[__name__])\n\n\nclass Format(enum.IntEnum):\n    \"\"\"File format to be used when parsing or serializing X.509 structures. Deprecated.\n\n    Backwards compatible with the `FILETYPE_ASN1` and `FILETYPE_PEM` constants\n    from pyOpenSSL.\n    \"\"\"\n    DER = crypto.FILETYPE_ASN1\n    PEM = crypto.FILETYPE_PEM\n\n    def to_cryptography_encoding(self) -> Encoding:\n        \"\"\"Converts the Format to the corresponding cryptography `Encoding`.\n        \"\"\"\n        if self == Format.DER:\n            return Encoding.DER\n        else:\n            return Encoding.PEM\n\n\n# Annoyingly, due to a mypy bug, we can't use Union[] types in\n# isinstance expressions without causing false mypy errors. So we have to\n# recreate the type collection as a tuple here. And no, typing.get_args doesn't\n# work due to another mypy bug.\n#\n# mypy issues:\n#  * https://github.com/python/mypy/issues/17680\n#  * https://github.com/python/mypy/issues/15106\nCertificateIssuerPrivateKeyTypesTpl = (\n    dsa.DSAPrivateKey,\n    rsa.RSAPrivateKey,\n    ec.EllipticCurvePrivateKey,\n    ed25519.Ed25519PrivateKey,\n    ed448.Ed448PrivateKey,\n)\n\n\ndef make_csr(\n    private_key_pem: bytes,\n    domains: Optional[Union[set[str], list[str]]] = None,\n    must_staple: bool = False,\n    ipaddrs: Optional[list[Union[ipaddress.IPv4Address, ipaddress.IPv6Address]]] = None,\n) -> bytes:\n    \"\"\"Generate a CSR containing domains or IPs as subjectAltNames.\n\n    Parameters are ordered this way for backwards compatibility when called using positional\n    arguments.\n\n    :param buffer private_key_pem: Private key, in PEM PKCS#8 format.\n    :param list domains: List of DNS names to include in subjectAltNames of CSR.\n    :param bool must_staple: Whether to include the TLS Feature extension (aka\n        OCSP Must Staple: https://tools.ietf.org/html/rfc7633).\n    :param list ipaddrs: List of IPaddress(type ipaddress.IPv4Address or ipaddress.IPv6Address)\n        names to include in subbjectAltNames of CSR.\n\n    :returns: buffer PEM-encoded Certificate Signing Request.\n\n    \"\"\"\n    private_key = serialization.load_pem_private_key(private_key_pem, password=None)\n    if not isinstance(private_key, CertificateIssuerPrivateKeyTypesTpl):\n        raise ValueError(f\"Invalid private key type: {type(private_key)}\")\n    if domains is None:\n        domains = []\n    if ipaddrs is None:\n        ipaddrs = []\n    if len(domains) + len(ipaddrs) == 0:\n        raise ValueError(\n            \"At least one of domains or ipaddrs parameter need to be not empty\"\n        )\n\n    builder = (\n        x509.CertificateSigningRequestBuilder()\n        .subject_name(x509.Name([]))\n        .add_extension(\n            x509.SubjectAlternativeName(\n                [x509.DNSName(d) for d in domains]\n                + [x509.IPAddress(i) for i in ipaddrs]\n            ),\n            critical=False,\n        )\n    )\n    if must_staple:\n        builder = builder.add_extension(\n            # \"status_request\" is the feature commonly known as OCSP\n            # Must-Staple\n            x509.TLSFeature([x509.TLSFeatureType.status_request]),\n            critical=False,\n        )\n\n    csr = builder.sign(private_key, hashes.SHA256())\n    return csr.public_bytes(Encoding.PEM)\n\n\ndef get_names_from_subject_and_extensions(\n    subject: x509.Name, exts: x509.Extensions\n) -> list[str]:\n    \"\"\"Gets all DNS SANs as well as the first Common Name from subject.\n\n    :param subject: Name of the x509 object, which may include Common Name\n    :type subject: `cryptography.x509.Name`\n    :param exts: Extensions of the x509 object, which may include SANs\n    :type exts: `cryptography.x509.Extensions`\n\n    :returns: List of DNS Subject Alternative Names and first Common Name\n    :rtype: `list` of `str`\n    \"\"\"\n    dns_names, _ = get_identifiers_from_x509(subject, exts)\n    return dns_names\n\n\ndef get_identifiers_from_x509(\n    subject: x509.Name, exts: x509.Extensions\n) -> tuple[list[str], list[str]]:\n    \"\"\"Gets all DNS and/or IP address SANs as well as the first Common Name from subject.\n\n    The CN will be first in the list of DNS names, if present.\n\n    :param subject: Name of the x509 object, which may include Common Name\n    :type subject: `cryptography.x509.Name`\n    :param exts: Extensions of the x509 object, which may include SANs\n    :type exts: `cryptography.x509.Extensions`\n\n    :returns: Tuple containing DNS names and IP addresses.\n    \"\"\"\n    # We know these are always `str` because `bytes` is only possible for\n    # other OIDs.\n    cns = [\n        typing.cast(str, c.value)\n        for c in subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)\n    ]\n    try:\n        san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)\n    except x509.ExtensionNotFound:\n        dns_names = []\n        ip_addresses = []\n    else:\n        dns_names = san_ext.value.get_values_for_type(x509.DNSName)\n        ip_addresses = [str(ip) for ip in san_ext.value.get_values_for_type(x509.IPAddress)]\n\n    if not cns:\n        return dns_names, ip_addresses\n    else:\n        # We only include the first CN, if there are multiple. This matches\n        # the behavior of the previous implementation using pyOpenSSL.\n        return [cns[0]] + [d for d in dns_names if d != cns[0]], ip_addresses\n\n\ndef _cryptography_cert_or_req_san(\n    cert_or_req: Union[x509.Certificate, x509.CertificateSigningRequest],\n) -> list[str]:\n    \"\"\"Get Subject Alternative Names from certificate or CSR using cryptography.\n\n    .. note:: Although this is `acme` internal API, it is used by\n        `letsencrypt`.\n\n    :param cert_or_req: Certificate or CSR.\n    :type cert_or_req: `x509.Certificate` or `x509.CertificateSigningRequest`.\n\n    :returns: A list of Subject Alternative Names that is DNS.\n    :rtype: `list` of `str`\n\n    Deprecated\n    .. deprecated: 3.2.1\n    \"\"\"\n    # ???: is this translation needed?\n    exts = cert_or_req.extensions\n    try:\n        san_ext = exts.get_extension_for_class(x509.SubjectAlternativeName)\n    except x509.ExtensionNotFound:\n        return []\n\n    return san_ext.value.get_values_for_type(x509.DNSName)\n\n\n# Helper function that can be mocked in unit tests\ndef _now() -> datetime:\n    return datetime.now(tz=timezone.utc)\n\n\ndef make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes,\n                          domains: Optional[list[str]] = None,\n                          not_before: Optional[datetime] = None,\n                          validity: Optional[timedelta] = None, force_san: bool = True,\n                          extensions: Optional[list[x509.Extension]] = None,\n                          ips: Optional[list[Union[ipaddress.IPv4Address,\n                                                   ipaddress.IPv6Address]]] = None\n                          ) -> x509.Certificate:\n    \"\"\"Generate new self-signed certificate.\n    :param buffer private_key_pem: Private key, in PEM PKCS#8 format.\n    :type domains: `list` of `str`\n    :param int not_before: A datetime after which the cert is valid. If no\n    timezone is specified, UTC is assumed\n    :type not_before: `datetime.datetime`\n    :param validity: Duration for which the cert will be valid. Defaults to 1\n    week\n    :type validity: `datetime.timedelta`\n    :param buffer private_key_pem: One of\n    `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes`\n    :param bool force_san:\n    :param extensions: List of additional extensions to include in the cert.\n    :type extensions: `list` of `x509.Extension[x509.ExtensionType]`\n    :type ips: `list` of (`ipaddress.IPv4Address` or `ipaddress.IPv6Address`)\n    If more than one domain is provided, all of the domains are put into\n    ``subjectAltName`` X.509 extension and first domain is set as the\n    subject CN. If only one domain is provided no ``subjectAltName``\n    extension is used, unless `force_san` is ``True``.\n    \"\"\"\n    warnings.warn(\"make_self_signed_cert is deprecated and will be removed in \"\n                  \"an upcoming release\", DeprecationWarning)\n    assert domains or ips, \"Must provide one or more hostnames or IPs for the cert.\"\n\n    builder = x509.CertificateBuilder()\n    builder = builder.serial_number(x509.random_serial_number())\n\n    if extensions is not None:\n        for ext in extensions:\n            builder = builder.add_extension(ext.value, ext.critical)\n    if domains is None:\n        domains = []\n    if ips is None:\n        ips = []\n    builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)\n\n    name_attrs = []\n    if len(domains) > 0:\n        name_attrs.append(x509.NameAttribute(\n            x509.OID_COMMON_NAME,\n            domains[0]\n        ))\n\n    builder = builder.subject_name(x509.Name(name_attrs))\n    builder = builder.issuer_name(x509.Name(name_attrs))\n\n    sanlist: list[x509.GeneralName] = []\n    for address in domains:\n        sanlist.append(x509.DNSName(address))\n    for ip in ips:\n        sanlist.append(x509.IPAddress(ip))\n    if force_san or len(domains) > 1 or len(ips) > 0:\n        builder = builder.add_extension(\n            x509.SubjectAlternativeName(sanlist),\n            critical=False\n        )\n\n    if not_before is None:\n        not_before = _now()\n    if validity is None:\n        validity = timedelta(seconds=7 * 24 * 60 * 60)\n    builder = builder.not_valid_before(not_before)\n    builder = builder.not_valid_after(not_before + validity)\n\n    public_key = private_key.public_key()\n    builder = builder.public_key(public_key)\n    return builder.sign(private_key, hashes.SHA256())\n\n\ndef dump_cryptography_chain(\n    chain: list[x509.Certificate],\n    encoding: Literal[Encoding.PEM, Encoding.DER] = Encoding.PEM,\n) -> bytes:\n    \"\"\"Dump certificate chain into a bundle.\n\n    :param list chain: List of `cryptography.x509.Certificate`.\n\n    :returns: certificate chain bundle\n    :rtype: bytes\n\n    Deprecated\n    .. deprecated: 3.2.1\n    \"\"\"\n    # XXX: returns empty string when no chain is available, which\n    # shuts up RenewableCert, but might not be the best solution...\n\n    def _dump_cert(cert: x509.Certificate) -> bytes:\n        return cert.public_bytes(encoding)\n\n    # assumes that x509.Certificate.public_bytes includes ending\n    # newline character\n    return b\"\".join(_dump_cert(cert) for cert in chain)\n"
  },
  {
    "path": "acme/src/acme/errors.py",
    "content": "\"\"\"ACME errors.\"\"\"\nimport datetime\nimport typing\nfrom typing import Any\nfrom typing import Mapping\n\nfrom josepy import errors as jose_errors\nimport requests\n\n# We import acme.messages only during type check to avoid circular dependencies. Type references\n# to acme.message.* must be quoted to be lazily initialized and avoid compilation errors.\nif typing.TYPE_CHECKING:\n    from acme import messages  # pragma: no cover\n\n\nclass Error(Exception):\n    \"\"\"Generic ACME error.\"\"\"\n\n\nclass DependencyError(Error):\n    \"\"\"Dependency error\"\"\"\n\n\nclass SchemaValidationError(jose_errors.DeserializationError):\n    \"\"\"JSON schema ACME object validation error.\"\"\"\n\n\nclass ClientError(Error):\n    \"\"\"Network error.\"\"\"\n\n\nclass UnexpectedUpdate(ClientError):\n    \"\"\"Unexpected update error.\"\"\"\n\n\nclass NonceError(ClientError):\n    \"\"\"Server response nonce error.\"\"\"\n\n\nclass BadNonce(NonceError):\n    \"\"\"Bad nonce error.\"\"\"\n    def __init__(self, nonce: str, error: Exception, *args: Any) -> None:\n        super().__init__(*args)\n        self.nonce = nonce\n        self.error = error\n\n    def __str__(self) -> str:\n        return 'Invalid nonce ({0!r}): {1}'.format(self.nonce, self.error)\n\n\nclass MissingNonce(NonceError):\n    \"\"\"Missing nonce error.\n\n    According to the specification an \"ACME server MUST include an\n    Replay-Nonce header field in each successful response to a POST it\n    provides to a client (...)\".\n\n    :ivar requests.Response ~.response: HTTP Response\n\n    \"\"\"\n    def __init__(self, response: requests.Response, *args: Any) -> None:\n        super().__init__(*args)\n        self.response = response\n\n    def __str__(self) -> str:\n        return ('Server {0} response did not include a replay '\n                'nonce, headers: {1} (This may be a service outage)'.format(\n                    self.response.request.method, self.response.headers))\n\n\nclass PollError(ClientError):\n    \"\"\"Generic error when polling for authorization fails.\n\n    This might be caused by either timeout (`exhausted` will be non-empty)\n    or by some authorization being invalid.\n\n    :ivar exhausted: Set of `.AuthorizationResource` that didn't finish\n        within max allowed attempts.\n    :ivar updated: Mapping from original `.AuthorizationResource`\n        to the most recently updated one\n\n    \"\"\"\n    def __init__(self, exhausted: set['messages.AuthorizationResource'],\n                 updated: Mapping['messages.AuthorizationResource',\n                                  'messages.AuthorizationResource']\n                 ) -> None:\n        self.exhausted = exhausted\n        self.updated = updated\n        super().__init__()\n\n    @property\n    def timeout(self) -> bool:\n        \"\"\"Was the error caused by timeout?\"\"\"\n        return bool(self.exhausted)\n\n    def __repr__(self) -> str:\n        return '{0}(exhausted={1!r}, updated={2!r})'.format(\n            self.__class__.__name__, self.exhausted, self.updated)\n\n\nclass ValidationError(Error):\n    \"\"\"Error for authorization failures. Contains a list of authorization\n    resources, each of which is invalid and should have an error field.\n    \"\"\"\n    def __init__(self, failed_authzrs: list['messages.AuthorizationResource']) -> None:\n        self.failed_authzrs = failed_authzrs\n        super().__init__()\n\n    def __str__(self) -> str:\n        msg = []\n        for authzr in self.failed_authzrs:\n            msg.append(f'Authorization for {authzr.body.identifier.value} ' \\\n                'failed due to one or more failed challenges:')\n            for challenge in authzr.body.challenges:\n                msg.append(f'  Challenge {challenge.chall.typ} failed ' \\\n                    f'with error {str(challenge.error)}')\n        return '\\n'.join(msg)\n\n\nclass TimeoutError(Error):  # pylint: disable=redefined-builtin\n    \"\"\"Error for when polling an authorization or an order times out.\"\"\"\n\n\nclass IssuanceError(Error):\n    \"\"\"Error sent by the server after requesting issuance of a certificate.\"\"\"\n\n    def __init__(self, error: 'messages.Error') -> None:\n        \"\"\"Initialize.\n\n        :param messages.Error error: The error provided by the server.\n        \"\"\"\n        self.error = error\n        super().__init__()\n\n\nclass ConflictError(ClientError):\n    \"\"\"Error for when the server returns a 409 (Conflict) HTTP status.\n\n    In the version of ACME implemented by Boulder, this is used to find an\n    account if you only have the private key, but don't know the account URL.\n\n    Also used in V2 of the ACME client for the same purpose.\n    \"\"\"\n    def __init__(self, location: str) -> None:\n        self.location = location\n        super().__init__()\n\n\nclass WildcardUnsupportedError(Error):\n    \"\"\"Error for when a wildcard is requested but is unsupported by ACME CA.\"\"\"\n\n\nclass ARIError(ClientError):\n    \"\"\"An error occurred during an ARI request and we want to suggest a Retry-After time.\"\"\"\n    def __init__(self, message: str, retry_after: datetime.datetime) -> None:\n        super().__init__(message, retry_after)\n        self.retry_after = retry_after\n"
  },
  {
    "path": "acme/src/acme/fields.py",
    "content": "\"\"\"ACME JSON fields.\"\"\"\nimport datetime\nimport logging\nfrom typing import Any\n\nimport josepy as jose\nimport pyrfc3339\n\nlogger = logging.getLogger(__name__)\n\n\nclass Fixed(jose.Field):\n    \"\"\"Fixed field.\"\"\"\n\n    def __init__(self, json_name: str, value: Any) -> None:\n        self.value = value\n        super().__init__(\n            json_name=json_name, default=value, omitempty=False)\n\n    def decode(self, value: Any) -> Any:\n        if value != self.value:\n            raise jose.DeserializationError('Expected {0!r}'.format(self.value))\n        return self.value\n\n    def encode(self, value: Any) -> Any:\n        if value != self.value:\n            logger.warning(\n                'Overriding fixed field (%s) with %r', self.json_name, value)\n        return value\n\n\nclass RFC3339Field(jose.Field):\n    \"\"\"RFC3339 field encoder/decoder.\n\n    Handles decoding/encoding between RFC3339 strings and aware (not\n    naive) `datetime.datetime` objects\n    (e.g. ``datetime.datetime.now(datetime.timezone.utc)``).\n\n    \"\"\"\n\n    @classmethod\n    def default_encoder(cls, value: datetime.datetime) -> str:\n        return pyrfc3339.generate(value)\n\n    @classmethod\n    def default_decoder(cls, value: str) -> datetime.datetime:\n        try:\n            return pyrfc3339.parse(value)\n        except ValueError as error:\n            raise jose.DeserializationError(error)\n\n\ndef fixed(json_name: str, value: Any) -> Any:\n    \"\"\"Generates a type-friendly Fixed field.\"\"\"\n    return Fixed(json_name, value)\n\n\ndef rfc3339(json_name: str, omitempty: bool = False) -> Any:\n    \"\"\"Generates a type-friendly RFC3339 field.\"\"\"\n    return RFC3339Field(json_name, omitempty=omitempty)\n"
  },
  {
    "path": "acme/src/acme/jws.py",
    "content": "\"\"\"ACME-specific JWS.\n\nThe JWS implementation in josepy only implements the base JOSE standard. In\norder to support the new header fields defined in ACME, this module defines some\nACME-specific classes that layer on top of josepy.\n\"\"\"\nfrom typing import Optional\n\nimport josepy as jose\n\n\nclass Header(jose.Header):\n    \"\"\"ACME-specific JOSE Header. Implements nonce, kid, and url.\n    \"\"\"\n    nonce: Optional[bytes] = jose.field('nonce', omitempty=True, encoder=jose.encode_b64jose)\n    kid: Optional[str] = jose.field('kid', omitempty=True)\n    url: Optional[str] = jose.field('url', omitempty=True)\n\n    # Mypy does not understand the josepy magic happening here, and falsely claims\n    # that nonce is redefined. Let's ignore the type check here.\n    @nonce.decoder  # type: ignore[no-redef,union-attr]\n    def nonce(value: str) -> bytes:  # type: ignore[misc]  # pylint: disable=no-self-argument,missing-function-docstring\n        try:\n            return jose.decode_b64jose(value)\n        except jose.DeserializationError as error:\n            # TODO: custom error\n            raise jose.DeserializationError(\"Invalid nonce: {0}\".format(error))\n\n\nclass Signature(jose.Signature):\n    \"\"\"ACME-specific Signature. Uses ACME-specific Header for customer fields.\"\"\"\n    __slots__ = jose.Signature._orig_slots  # pylint: disable=protected-access,no-member\n\n    # TODO: decoder/encoder should accept cls? Otherwise, subclassing\n    # JSONObjectWithFields is tricky...\n    header_cls = Header\n    header: Header = jose.field(\n        'header', omitempty=True, default=header_cls(),\n        decoder=header_cls.from_json)\n\n    # TODO: decoder should check that nonce is in the protected header\n\n\nclass JWS(jose.JWS):\n    \"\"\"ACME-specific JWS. Includes none, url, and kid in protected header.\"\"\"\n    signature_cls = Signature\n    __slots__ = jose.JWS._orig_slots  # pylint: disable=protected-access\n\n    @classmethod\n    # type: ignore[override]  # pylint: disable=arguments-differ\n    def sign(cls, payload: bytes, key: jose.JWK, alg: jose.JWASignature, nonce: Optional[bytes],\n             url: Optional[str] = None, kid: Optional[str] = None) -> jose.JWS:\n        # Per ACME spec, jwk and kid are mutually exclusive, so only include a\n        # jwk field if kid is not provided.\n        include_jwk = kid is None\n        return super().sign(payload, key=key, alg=alg,\n                            protect=frozenset(['nonce', 'url', 'kid', 'jwk', 'alg']),\n                            nonce=nonce, url=url, kid=kid,\n                            include_jwk=include_jwk)\n"
  },
  {
    "path": "acme/src/acme/messages.py",
    "content": "\"\"\"ACME protocol messages.\"\"\"\nfrom collections.abc import Hashable\nimport datetime\nimport json\nfrom typing import Any\nfrom typing import Iterator\nfrom typing import Mapping\nfrom typing import MutableMapping\nfrom typing import Optional\nfrom typing import TypeVar\n\nfrom cryptography import x509\n\nimport josepy as jose\n\nfrom acme import challenges\nfrom acme import errors\nfrom acme import fields\nfrom acme import jws\nfrom acme import util\n\nERROR_PREFIX = \"urn:ietf:params:acme:error:\"\n\nERROR_CODES = {\n    'accountDoesNotExist': 'The request specified an account that does not exist',\n    'alreadyRevoked': 'The request specified a certificate to be revoked that has' \\\n    ' already been revoked',\n    'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',\n    'badNonce': 'The client sent an unacceptable anti-replay nonce',\n    'badPublicKey': 'The JWS was signed by a public key the server does not support',\n    'badRevocationReason': 'The revocation reason provided is not allowed by the server',\n    'badSignatureAlgorithm': 'The JWS was signed with an algorithm the server does not support',\n    'caa': 'Certification Authority Authorization (CAA) records forbid the CA from issuing' \\\n    ' a certificate',\n    'compound': 'Specific error conditions are indicated in the \"subproblems\" array',\n    'connection': ('The server could not connect to the client to verify the'\n                   ' domain'),\n    'dns': 'There was a problem with a DNS query during identifier validation',\n    'dnssec': 'The server could not validate a DNSSEC signed domain',\n    'incorrectResponse': 'Response received didn\\'t match the challenge\\'s requirements',\n    # deprecate invalidEmail\n    'invalidEmail': 'The provided email for a registration was invalid',\n    'invalidContact': 'The provided contact URI was invalid',\n    'malformed': 'The request message was malformed',\n    'rejectedIdentifier': 'The server will not issue certificates for the identifier',\n    'orderNotReady': 'The request attempted to finalize an order that is not ready to be finalized',\n    'rateLimited': 'There were too many requests of a given type',\n    'serverInternal': 'The server experienced an internal error',\n    'tls': 'The server experienced a TLS error during domain verification',\n    'unauthorized': 'The client lacks sufficient authorization',\n    'unsupportedContact': 'A contact URL for an account used an unsupported protocol scheme',\n    'unknownHost': 'The server could not resolve a domain name',\n    'unsupportedIdentifier': 'An identifier is of an unsupported type',\n    'externalAccountRequired': 'The server requires external account binding',\n}\n\nERROR_TYPE_DESCRIPTIONS = {**{\n    ERROR_PREFIX + name: desc for name, desc in ERROR_CODES.items()\n}}\n\n\ndef is_acme_error(err: BaseException) -> bool:\n    \"\"\"Check if argument is an ACME error.\"\"\"\n    if isinstance(err, Error) and (err.typ is not None):\n        return ERROR_PREFIX in err.typ\n    return False\n\n\nclass _Constant(jose.JSONDeSerializable, Hashable):\n    \"\"\"ACME constant.\"\"\"\n    __slots__ = ('name',)\n    POSSIBLE_NAMES: dict[str, '_Constant'] = NotImplemented\n\n    def __init__(self, name: str) -> None:\n        super().__init__()\n        self.POSSIBLE_NAMES[name] = self  # pylint: disable=unsupported-assignment-operation\n        self.name = name\n\n    def to_partial_json(self) -> str:\n        return self.name\n\n    @classmethod\n    def from_json(cls, jobj: str) -> '_Constant':\n        if jobj not in cls.POSSIBLE_NAMES:  # pylint: disable=unsupported-membership-test\n            raise jose.DeserializationError(f'{cls.__name__} not recognized')\n        return cls.POSSIBLE_NAMES[jobj]\n\n    def __repr__(self) -> str:\n        return f'{self.__class__.__name__}({self.name})'\n\n    def __eq__(self, other: Any) -> bool:\n        return isinstance(other, type(self)) and other.name == self.name\n\n    def __hash__(self) -> int:\n        return hash((self.__class__, self.name))\n\n\nclass IdentifierType(_Constant):\n    \"\"\"ACME identifier type.\"\"\"\n    POSSIBLE_NAMES: dict[str, _Constant] = {}\n\n\nIDENTIFIER_FQDN = IdentifierType('dns')  # IdentifierDNS in Boulder\nIDENTIFIER_IP = IdentifierType('ip') # IdentifierIP in pebble - not in Boulder yet\n\n\nclass Identifier(jose.JSONObjectWithFields):\n    \"\"\"ACME identifier.\n\n    :ivar IdentifierType typ:\n    :ivar str value:\n\n    \"\"\"\n    typ: IdentifierType = jose.field('type', decoder=IdentifierType.from_json)\n    value: str = jose.field('value')\n\n\nclass Error(jose.JSONObjectWithFields, errors.Error):\n    \"\"\"ACME error.\n\n    https://datatracker.ietf.org/doc/html/rfc7807\n\n    Note: Although Error inherits from JSONObjectWithFields, which is immutable,\n    we add mutability for Error to comply with the Python exception API.\n\n    :ivar str typ:\n    :ivar str title:\n    :ivar str detail:\n    :ivar Identifier identifier:\n    :ivar tuple subproblems: An array of ACME Errors which may be present when the CA\n            returns multiple errors related to the same request, `tuple` of `Error`.\n\n    \"\"\"\n    typ: str = jose.field('type', omitempty=True, default='about:blank')\n    title: str = jose.field('title', omitempty=True)\n    detail: str = jose.field('detail', omitempty=True)\n    identifier: Optional['Identifier'] = jose.field(\n        'identifier', decoder=Identifier.from_json, omitempty=True)\n    subproblems: Optional[tuple['Error', ...]] = jose.field('subproblems', omitempty=True)\n\n    # Mypy does not understand the josepy magic happening here, and falsely claims\n    # that subproblems is redefined. Let's ignore the type check here.\n    @subproblems.decoder  # type: ignore\n    def subproblems(value: list[dict[str, Any]]) -> tuple['Error', ...]:  # pylint: disable=no-self-argument,missing-function-docstring\n        return tuple(Error.from_json(subproblem) for subproblem in value)\n\n    @classmethod\n    def with_code(cls, code: str, **kwargs: Any) -> 'Error':\n        \"\"\"Create an Error instance with an ACME Error code.\n\n        :str code: An ACME error code, like 'dnssec'.\n        :kwargs: kwargs to pass to Error.\n\n        \"\"\"\n        if code not in ERROR_CODES:\n            raise ValueError(\"The supplied code: %s is not a known ACME error\"\n                             \" code\" % code)\n        typ = ERROR_PREFIX + code\n        # Mypy will not understand that the Error constructor accepts a named argument\n        # \"typ\" because of josepy magic. Let's ignore the type check here.\n        return cls(typ=typ, **kwargs)\n\n    @property\n    def description(self) -> Optional[str]:\n        \"\"\"Hardcoded error description based on its type.\n\n        :returns: Description if standard ACME error or ``None``.\n        :rtype: str\n\n        \"\"\"\n        return ERROR_TYPE_DESCRIPTIONS.get(self.typ)\n\n    @property\n    def code(self) -> Optional[str]:\n        \"\"\"ACME error code.\n\n        Basically self.typ without the ERROR_PREFIX.\n\n        :returns: error code if standard ACME code or ``None``.\n        :rtype: str\n\n        \"\"\"\n        code = str(self.typ).rsplit(':', maxsplit=1)[-1]\n        if code in ERROR_CODES:\n            return code\n        return None\n\n    # Hack to allow mutability on Errors (see GH #9539)\n    def __setattr__(self, name: str, value: Any) -> None:\n        return object.__setattr__(self, name, value)\n\n    def __str__(self) -> str:\n        result = b' :: '.join(\n            part.encode('ascii', 'backslashreplace') for part in\n            (self.typ, self.description, self.detail, self.title)\n            if part is not None).decode()\n        if self.identifier:\n            result = f'Problem for {self.identifier.value}: ' + result # pylint: disable=no-member\n        if self.subproblems and len(self.subproblems) > 0:\n            for subproblem in self.subproblems:\n                result += f'\\n{subproblem}'\n        return result\n\n\nclass Status(_Constant):\n    \"\"\"ACME \"status\" field.\"\"\"\n    POSSIBLE_NAMES: dict[str, _Constant] = {}\n\n\nSTATUS_UNKNOWN = Status('unknown')\nSTATUS_PENDING = Status('pending')\nSTATUS_PROCESSING = Status('processing')\nSTATUS_VALID = Status('valid')\nSTATUS_INVALID = Status('invalid')\nSTATUS_REVOKED = Status('revoked')\nSTATUS_READY = Status('ready')\nSTATUS_DEACTIVATED = Status('deactivated')\n\n\nclass Directory(jose.JSONDeSerializable):\n    \"\"\"Directory.\n\n    Directory resources must be accessed by the exact field name in RFC8555 (section 9.7.5).\n    \"\"\"\n\n    class Meta(jose.JSONObjectWithFields):\n        \"\"\"Directory Meta.\"\"\"\n        _terms_of_service: str = jose.field('termsOfService', omitempty=True)\n        website: str = jose.field('website', omitempty=True)\n        caa_identities: list[str] = jose.field('caaIdentities', omitempty=True)\n        external_account_required: bool = jose.field('externalAccountRequired', omitempty=True)\n        profiles: dict[str, str] = jose.field('profiles', omitempty=True)\n\n        def __init__(self, **kwargs: Any) -> None:\n            kwargs = {self._internal_name(k): v for k, v in kwargs.items()}\n            super().__init__(**kwargs)\n\n        @property\n        def terms_of_service(self) -> str:\n            \"\"\"URL for the CA TOS\"\"\"\n            return self._terms_of_service\n\n        def __iter__(self) -> Iterator[str]:\n            # When iterating over fields, use the external name 'terms_of_service' instead of\n            # the internal '_terms_of_service'.\n            for name in super().__iter__():\n                yield name[1:] if name == '_terms_of_service' else name\n\n        def _internal_name(self, name: str) -> str:\n            return '_' + name if name == 'terms_of_service' else name\n\n    def __init__(self, jobj: Mapping[str, Any]) -> None:\n        self._jobj = jobj\n\n    def __getattr__(self, name: str) -> Any:\n        try:\n            return self[name]\n        except KeyError as error:\n            raise AttributeError(str(error))\n\n    def __getitem__(self, name: str) -> Any:\n        try:\n            return self._jobj[name]\n        except KeyError:\n            raise KeyError(f'Directory field \"{name}\" not found')\n\n    def to_partial_json(self) -> dict[str, Any]:\n        return util.map_keys(self._jobj, lambda k: k)\n\n    @classmethod\n    def from_json(cls, jobj: MutableMapping[str, Any]) -> 'Directory':\n        jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {}))\n        return cls(jobj)\n\n\nclass Resource(jose.JSONObjectWithFields):\n    \"\"\"ACME Resource.\n\n    :ivar acme.messages.ResourceBody body: Resource body.\n\n    \"\"\"\n    body: \"ResourceBody\" = jose.field('body')\n\n\nclass ResourceWithURI(Resource):\n    \"\"\"ACME Resource with URI.\n\n    :ivar str uri: Location of the resource.\n\n    \"\"\"\n    uri: str = jose.field('uri')  # no ChallengeResource.uri\n\n\nclass ResourceBody(jose.JSONObjectWithFields):\n    \"\"\"ACME Resource Body.\"\"\"\n\n\nclass ExternalAccountBinding:\n    \"\"\"ACME External Account Binding\"\"\"\n\n    @classmethod\n    def from_data(cls, account_public_key: jose.JWK, kid: str, hmac_key: str,\n                  directory: Directory, hmac_alg: str = \"HS256\") -> dict[str, Any]:\n        \"\"\"Create External Account Binding Resource from contact details, kid and hmac.\"\"\"\n\n        key_json = json.dumps(account_public_key.to_partial_json()).encode()\n        decoded_hmac_key = jose.b64.b64decode(hmac_key)\n        url = directory[\"newAccount\"]\n\n        hmac_alg_map = {\n            \"HS256\": jose.jwa.HS256,\n            \"HS384\": jose.jwa.HS384,\n            \"HS512\": jose.jwa.HS512,\n        }\n        alg = hmac_alg_map.get(hmac_alg)\n        if alg is None:\n            supported = \", \".join(hmac_alg_map.keys())\n            raise ValueError(f\"Invalid value for hmac_alg: {hmac_alg}. \"\n                             f\"Expected one of: {supported}.\")\n\n        eab = jws.JWS.sign(key_json, jose.jwk.JWKOct(key=decoded_hmac_key),\n                           alg, None,\n                           url, kid)\n\n        return eab.to_partial_json()\n\n\nGenericRegistration = TypeVar('GenericRegistration', bound='Registration')\n\n\nclass Registration(ResourceBody):\n    \"\"\"Registration Resource Body.\n\n    :ivar jose.JWK key: Public key.\n    :ivar tuple contact: Contact information following ACME spec,\n        `tuple` of `str`.\n    :ivar str agreement:\n\n    \"\"\"\n    # on new-reg key server ignores 'key' and populates it based on\n    # JWS.signature.combined.jwk\n    key: jose.JWK = jose.field('key', omitempty=True, decoder=jose.JWK.from_json)\n    # Contact field implements special behavior to allow messages that clear existing\n    # contacts while not expecting the `contact` field when loading from json.\n    # This is implemented in the constructor and *_json methods.\n    contact: tuple[str, ...] = jose.field('contact', omitempty=True, default=())\n    agreement: str = jose.field('agreement', omitempty=True)\n    status: Status = jose.field('status', omitempty=True)\n    terms_of_service_agreed: bool = jose.field('termsOfServiceAgreed', omitempty=True)\n    only_return_existing: bool = jose.field('onlyReturnExisting', omitempty=True)\n    external_account_binding: dict[str, Any] = jose.field('externalAccountBinding',\n                                                          omitempty=True)\n\n    phone_prefix = 'tel:'\n    email_prefix = 'mailto:'\n\n    @classmethod\n    def from_data(cls: type[GenericRegistration], phone: Optional[str] = None,\n                  email: Optional[str] = None,\n                  external_account_binding: Optional[dict[str, Any]] = None,\n                  **kwargs: Any) -> GenericRegistration:\n        \"\"\"\n        Create registration resource from contact details.\n\n        The `contact` keyword being passed to a Registration object is meaningful, so\n        this function represents empty iterables in its kwargs by passing on an empty\n        `tuple`.\n        \"\"\"\n\n        # Note if `contact` was in kwargs.\n        contact_provided = 'contact' in kwargs\n\n        # Pop `contact` from kwargs and add formatted email or phone numbers\n        details = list(kwargs.pop('contact', ()))\n        if phone is not None:\n            details.append(cls.phone_prefix + phone)\n        if email is not None:\n            details.extend([cls.email_prefix + mail for mail in email.split(',')])\n\n        # Insert formatted contact information back into kwargs\n        # or insert an empty tuple if `contact` provided.\n        if details or contact_provided:\n            kwargs['contact'] = tuple(details)\n\n        if external_account_binding:\n            kwargs['external_account_binding'] = external_account_binding\n\n        return cls(**kwargs)\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"Note if the user provides a value for the `contact` member.\"\"\"\n        if 'contact' in kwargs and kwargs['contact'] is not None:\n            # Avoid the __setattr__ used by jose.TypedJSONObjectWithFields\n            object.__setattr__(self, '_add_contact', True)\n        super().__init__(**kwargs)\n\n    def _filter_contact(self, prefix: str) -> tuple[str, ...]:\n        return tuple(\n            detail[len(prefix):] for detail in self.contact  # pylint: disable=not-an-iterable\n            if detail.startswith(prefix))\n\n    def _add_contact_if_appropriate(self, jobj: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"\n        The `contact` member of Registration objects should not be required when\n        de-serializing (as it would be if the Fields' `omitempty` flag were `False`), but\n        it should be included in serializations if it was provided.\n\n        :param jobj: Dictionary containing this Registrations' data\n        :type jobj: dict\n\n        :returns: Dictionary containing Registrations data to transmit to the server\n        :rtype: dict\n        \"\"\"\n        if getattr(self, '_add_contact', False):\n            jobj['contact'] = self.encode('contact')\n\n        return jobj\n\n    def to_partial_json(self) -> dict[str, Any]:\n        \"\"\"Modify josepy.JSONDeserializable.to_partial_json()\"\"\"\n        jobj = super().to_partial_json()\n        return self._add_contact_if_appropriate(jobj)\n\n    def fields_to_partial_json(self) -> dict[str, Any]:\n        \"\"\"Modify josepy.JSONObjectWithFields.fields_to_partial_json()\"\"\"\n        jobj = super().fields_to_partial_json()\n        return self._add_contact_if_appropriate(jobj)\n\n    @property\n    def phones(self) -> tuple[str, ...]:\n        \"\"\"All phones found in the ``contact`` field.\"\"\"\n        return self._filter_contact(self.phone_prefix)\n\n    @property\n    def emails(self) -> tuple[str, ...]:\n        \"\"\"All emails found in the ``contact`` field.\"\"\"\n        return self._filter_contact(self.email_prefix)\n\n\nclass NewRegistration(Registration):\n    \"\"\"New registration.\"\"\"\n\n\nclass UpdateRegistration(Registration):\n    \"\"\"Update registration.\"\"\"\n\n\nclass RegistrationResource(ResourceWithURI):\n    \"\"\"Registration Resource.\n\n    :ivar acme.messages.Registration body:\n    :ivar str new_authzr_uri: Deprecated. Do not use.\n    :ivar str terms_of_service: URL for the CA TOS.\n\n    \"\"\"\n    body: Registration = jose.field('body', decoder=Registration.from_json)\n    new_authzr_uri: str = jose.field('new_authzr_uri', omitempty=True)\n    terms_of_service: str = jose.field('terms_of_service', omitempty=True)\n\n\nclass ChallengeBody(ResourceBody):\n    \"\"\"Challenge Resource Body.\n\n    .. todo::\n       Confusingly, this has a similar name to `.challenges.Challenge`,\n       as well as `.achallenges.AnnotatedChallenge`. Please use names\n       such as ``challb`` to distinguish instances of this class from\n       ``achall``.\n\n    :ivar acme.challenges.Challenge: Wrapped challenge.\n        Conveniently, all challenge fields are proxied, i.e. you can\n        call ``challb.x`` to get ``challb.chall.x`` contents.\n    :ivar acme.messages.Status status:\n    :ivar datetime.datetime validated:\n    :ivar messages.Error error:\n\n    \"\"\"\n    __slots__ = ('chall',)\n    # ACMEv1 has a \"uri\" field in challenges. ACMEv2 has a \"url\" field. This\n    # challenge object supports either one, but should be accessed through the\n    # name \"uri\". In Client.answer_challenge, whichever one is set will be\n    # used.\n    _url: str = jose.field('url', omitempty=True, default=None)\n    status: Status = jose.field('status', decoder=Status.from_json,\n                        omitempty=True, default=STATUS_PENDING)\n    validated: datetime.datetime = fields.rfc3339('validated', omitempty=True)\n    error: Error = jose.field('error', decoder=Error.from_json,\n                       omitempty=True, default=None)\n\n    def __init__(self, **kwargs: Any) -> None:\n        kwargs = {self._internal_name(k): v for k, v in kwargs.items()}\n        super().__init__(**kwargs)\n\n    def encode(self, name: str) -> Any:\n        return super().encode(self._internal_name(name))\n\n    def to_partial_json(self) -> dict[str, Any]:\n        jobj = super().to_partial_json()\n        jobj.update(self.chall.to_partial_json())\n        return jobj\n\n    @classmethod\n    def fields_from_json(cls, jobj: Mapping[str, Any]) -> dict[str, Any]:\n        jobj_fields = super().fields_from_json(jobj)\n        jobj_fields['chall'] = challenges.Challenge.from_json(jobj)\n        return jobj_fields\n\n    @property\n    def uri(self) -> str:\n        \"\"\"The URL of this challenge.\"\"\"\n        return self._url\n\n    def __getattr__(self, name: str) -> Any:\n        return getattr(self.chall, name)\n\n    def __iter__(self) -> Iterator[str]:\n        # When iterating over fields, use the external name 'uri' instead of\n        # the internal '_uri'.\n        for name in super().__iter__():\n            yield 'uri' if name == '_url' else name\n\n    def _internal_name(self, name: str) -> str:\n        return '_url' if name == 'uri' else name\n\n\nclass ChallengeResource(Resource):\n    \"\"\"Challenge Resource.\n\n    :ivar acme.messages.ChallengeBody body:\n    :ivar str authzr_uri: URI found in the 'up' ``Link`` header.\n\n    \"\"\"\n    body: ChallengeBody = jose.field('body', decoder=ChallengeBody.from_json)\n    authzr_uri: str = jose.field('authzr_uri')\n\n    @property\n    def uri(self) -> str:\n        \"\"\"The URL of the challenge body.\"\"\"\n        return self.body.uri  # pylint: disable=no-member\n\n\nclass Authorization(ResourceBody):\n    \"\"\"Authorization Resource Body.\n\n    :ivar acme.messages.Identifier identifier:\n    :ivar list challenges: `list` of `.ChallengeBody`\n    :ivar acme.messages.Status status:\n    :ivar datetime.datetime expires:\n\n    \"\"\"\n    identifier: Identifier = jose.field('identifier', decoder=Identifier.from_json, omitempty=True)\n    challenges: list[ChallengeBody] = jose.field('challenges', omitempty=True)\n\n    status: Status = jose.field('status', omitempty=True, decoder=Status.from_json)\n    # TODO: 'expires' is allowed for Authorization Resources in\n    # general, but for Key Authorization '[t]he \"expires\" field MUST\n    # be absent'... then acme-spec gives example with 'expires'\n    # present... That's confusing!\n    expires: datetime.datetime = fields.rfc3339('expires', omitempty=True)\n    wildcard: bool = jose.field('wildcard', omitempty=True)\n\n    # Mypy does not understand the josepy magic happening here, and falsely claims\n    # that challenge is redefined. Let's ignore the type check here.\n    @challenges.decoder  # type: ignore\n    def challenges(value: list[dict[str, Any]]) -> tuple[ChallengeBody, ...]:  # pylint: disable=no-self-argument,missing-function-docstring\n        return tuple(ChallengeBody.from_json(chall) for chall in value)\n\n\nclass NewAuthorization(Authorization):\n    \"\"\"New authorization.\"\"\"\n\n\nclass UpdateAuthorization(Authorization):\n    \"\"\"Update authorization.\"\"\"\n\n\nclass AuthorizationResource(ResourceWithURI):\n    \"\"\"Authorization Resource.\n\n    :ivar acme.messages.Authorization body:\n    :ivar str new_cert_uri: Deprecated. Do not use.\n\n    \"\"\"\n    body: Authorization = jose.field('body', decoder=Authorization.from_json)\n    new_cert_uri: str = jose.field('new_cert_uri', omitempty=True)\n\n\nclass CertificateRequest(jose.JSONObjectWithFields):\n    \"\"\"ACME newOrder request.\n\n    :ivar x509.CertificateSigningRequest csr: `x509.CertificateSigningRequest`\n\n    \"\"\"\n    csr: x509.CertificateSigningRequest = jose.field(\n        'csr', decoder=jose.decode_csr, encoder=jose.encode_csr)\n\n\nclass CertificateResource(ResourceWithURI):\n    \"\"\"Certificate Resource.\n\n    :ivar x509.Certificate body: `x509.Certificate`\n    :ivar str cert_chain_uri: URI found in the 'up' ``Link`` header\n    :ivar tuple authzrs: `tuple` of `AuthorizationResource`.\n\n    \"\"\"\n    cert_chain_uri: str = jose.field('cert_chain_uri')\n    authzrs: tuple[AuthorizationResource, ...] = jose.field('authzrs')\n\n\nclass Revocation(jose.JSONObjectWithFields):\n    \"\"\"Revocation message.\n\n    :ivar x509.Certificate certificate: `x509.Certificate`\n\n    \"\"\"\n    certificate: x509.Certificate = jose.field(\n        'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)\n    reason: int = jose.field('reason')\n\n\nclass Order(ResourceBody):\n    \"\"\"Order Resource Body.\n\n    :ivar profile: The profile to request.\n    :vartype profile: str\n    :ivar identifiers: List of identifiers for the certificate.\n    :vartype identifiers: `list` of `.Identifier`\n    :ivar acme.messages.Status status:\n    :ivar authorizations: URLs of authorizations.\n    :vartype authorizations: `list` of `str`\n    :ivar str certificate: URL to download certificate as a fullchain PEM.\n    :ivar str finalize: URL to POST to to request issuance once all\n        authorizations have \"valid\" status.\n    :ivar datetime.datetime expires: When the order expires.\n    :ivar ~.Error error: Any error that occurred during finalization, if applicable.\n    \"\"\"\n    # https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/\n    profile: str = jose.field('profile', omitempty=True)\n    identifiers: list[Identifier] = jose.field('identifiers', omitempty=True)\n    status: Status = jose.field('status', decoder=Status.from_json, omitempty=True)\n    authorizations: list[str] = jose.field('authorizations', omitempty=True)\n    certificate: str = jose.field('certificate', omitempty=True)\n    finalize: str = jose.field('finalize', omitempty=True)\n    expires: datetime.datetime = fields.rfc3339('expires', omitempty=True)\n    error: Error = jose.field('error', omitempty=True, decoder=Error.from_json)\n\n    # Mypy does not understand the josepy magic happening here, and falsely claims\n    # that identifiers is redefined. Let's ignore the type check here.\n    @identifiers.decoder  # type: ignore\n    def identifiers(value: list[dict[str, Any]]) -> tuple[Identifier, ...]:  # pylint: disable=no-self-argument,missing-function-docstring\n        return tuple(Identifier.from_json(identifier) for identifier in value)\n\n\nclass OrderResource(ResourceWithURI):\n    \"\"\"Order Resource.\n\n    :ivar acme.messages.Order body:\n    :ivar bytes csr_pem: The CSR this Order will be finalized with.\n    :ivar authorizations: Fully-fetched AuthorizationResource objects.\n    :vartype authorizations: `list` of `acme.messages.AuthorizationResource`\n    :ivar str fullchain_pem: The fetched contents of the certificate URL\n        produced once the order was finalized, if it's present.\n    :ivar alternative_fullchains_pem: The fetched contents of alternative certificate\n        chain URLs produced once the order was finalized, if present and requested during\n        finalization.\n    :vartype alternative_fullchains_pem: `list` of `str`\n    \"\"\"\n    body: Order = jose.field('body', decoder=Order.from_json)\n    csr_pem: bytes = jose.field('csr_pem', omitempty=True,\n                                # This looks backwards, but it's not -\n                                # we want the deserialized value to be\n                                # `bytes`, but anything we put into\n                                # JSON needs to be `str`, so we encode\n                                # to decode and decode to\n                                # encode. Otherwise we end up with an\n                                # array of ints on serialization\n                                decoder=lambda s: s.encode(\"utf-8\"),\n                                encoder=lambda b: b.decode(\"utf-8\"))\n\n    authorizations: list[AuthorizationResource] = jose.field('authorizations')\n    fullchain_pem: str = jose.field('fullchain_pem', omitempty=True)\n    alternative_fullchains_pem: list[str] = jose.field('alternative_fullchains_pem',\n                                                       omitempty=True)\n\n    # Mypy does not understand the josepy magic happening here, and falsely claims\n    # that authorizations is redefined. Let's ignore the type check here.\n    @authorizations.decoder  # type: ignore\n    def authorizations(value: list[dict[str, Any]]) -> tuple[AuthorizationResource, ...]: # pylint: disable=no-self-argument,missing-function-docstring\n        return tuple(AuthorizationResource.from_json(authz) for authz in value)\n\n\nclass NewOrder(Order):\n    \"\"\"New order.\"\"\"\n\n\nclass RenewalInfo(ResourceBody):\n    \"\"\"Renewal Info Resource Body.\n    :ivar acme.messages.SuggestedWindow window: The suggested renewal window.\n    \"\"\"\n    class SuggestedWindow(jose.JSONObjectWithFields):\n        \"\"\"Suggested Renewal Window, sub-resource of Renewal Info Resource.\n        :ivar datetime.datetime start: Beginning of suggested renewal window\n        :ivar datetime.datetime end: End of suggested renewal window (inclusive)\n        \"\"\"\n        start: datetime.datetime = fields.rfc3339('start', omitempty=True)\n        end: datetime.datetime = fields.rfc3339('end', omitempty=True)\n\n    suggested_window: SuggestedWindow = jose.field('suggestedWindow',\n                                                   decoder=SuggestedWindow.from_json)\n"
  },
  {
    "path": "acme/src/acme/py.typed",
    "content": ""
  },
  {
    "path": "acme/src/acme/standalone.py",
    "content": "\"\"\"Support for standalone client challenge solvers. \"\"\"\nfrom __future__ import annotations\n\nimport collections\nimport functools\nimport http.client as http_client\nimport http.server as BaseHTTPServer\nimport logging\nimport socket\nimport socketserver\nimport threading\nfrom typing import Any\nfrom typing import Optional\n\nfrom acme import challenges\n\nlogger = logging.getLogger(__name__)\n\n\nclass ACMEServerMixin:\n    \"\"\"ACME server common settings mixin.\"\"\"\n    # TODO: c.f. #858\n    server_version = \"ACME client standalone challenge solver\"\n    allow_reuse_address = True\n\n\nclass BaseDualNetworkedServers:\n    \"\"\"Base class for a pair of IPv6 and IPv4 servers that tries to do everything\n       it's asked for both servers, but where failures in one server don't\n       affect the other.\n\n       If two servers are instantiated, they will serve on the same port.\n       \"\"\"\n\n    def __init__(self, ServerClass: type[socketserver.TCPServer], server_address: tuple[str, int],\n                 *remaining_args: Any, **kwargs: Any) -> None:\n        port = server_address[1]\n        self.threads: list[threading.Thread] = []\n        self.servers: list[socketserver.BaseServer] = []\n\n        # Preserve socket error for re-raising, if no servers can be started\n        last_socket_err: Optional[socket.error] = None\n\n        # Must try True first.\n        # Ubuntu, for example, will fail to bind to IPv4 if we've already bound\n        # to IPv6. But that's ok, since it will accept IPv4 connections on the IPv6\n        # socket. On the other hand, FreeBSD will successfully bind to IPv4 on the\n        # same port, which means that server will accept the IPv4 connections.\n        # If Python is compiled without IPv6, we'll error out but (probably) successfully\n        # create the IPv4 server.\n        for ip_version in [True, False]:\n            try:\n                kwargs[\"ipv6\"] = ip_version\n                new_address = (server_address[0],) + (port,) + server_address[2:]\n                new_args = (new_address,) + remaining_args\n                server = ServerClass(*new_args, **kwargs)\n                logger.debug(\n                    \"Successfully bound to %s:%s using %s\", new_address[0],\n                    new_address[1], \"IPv6\" if ip_version else \"IPv4\")\n            except OSError as e:\n                last_socket_err = e\n                if self.servers:\n                    # Already bound using IPv6.\n                    logger.debug(\n                        \"Certbot wasn't able to bind to %s:%s using %s, this \"\n                        \"is often expected due to the dual stack nature of \"\n                        \"IPv6 socket implementations.\",\n                        new_address[0], new_address[1],\n                        \"IPv6\" if ip_version else \"IPv4\")\n                else:\n                    logger.debug(\n                        \"Failed to bind to %s:%s using %s\", new_address[0],\n                        new_address[1], \"IPv6\" if ip_version else \"IPv4\")\n            else:\n                self.servers.append(server)\n                # If two servers are set up and port 0 was passed in, ensure we always\n                # bind to the same port for both servers.\n                port = server.socket.getsockname()[1]\n        if not self.servers:\n            if last_socket_err:\n                raise last_socket_err\n            else: # pragma: no cover\n                raise OSError(\"Could not bind to IPv4 or IPv6.\")\n\n    def serve_forever(self) -> None:\n        \"\"\"Wraps socketserver.TCPServer.serve_forever\"\"\"\n        for server in self.servers:\n            thread = threading.Thread(\n                target=server.serve_forever)\n            thread.start()\n            self.threads.append(thread)\n\n    def getsocknames(self) -> list[tuple[str, int]]:\n        \"\"\"Wraps socketserver.TCPServer.socket.getsockname\"\"\"\n        return [server.socket.getsockname() for server in self.servers]\n\n    def shutdown_and_server_close(self) -> None:\n        \"\"\"Wraps socketserver.TCPServer.shutdown, socketserver.TCPServer.server_close, and\n           threading.Thread.join\"\"\"\n        for server in self.servers:\n            server.shutdown()\n            server.server_close()\n        for thread in self.threads:\n            thread.join()\n        self.threads = []\n\n\nclass HTTPServer(BaseHTTPServer.HTTPServer):\n    \"\"\"Generic HTTP Server.\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self.ipv6 = kwargs.pop(\"ipv6\", False)\n        if self.ipv6:\n            self.address_family = socket.AF_INET6\n        else:\n            self.address_family = socket.AF_INET\n        super().__init__(*args, **kwargs)\n\n\nclass HTTP01Server(HTTPServer, ACMEServerMixin):\n    \"\"\"HTTP01 Server.\"\"\"\n\n    def __init__(self, server_address: tuple[str, int],\n                 resources: set[HTTP01RequestHandler.HTTP01Resource],\n                 ipv6: bool = False, timeout: int = 30) -> None:\n        super().__init__(\n            server_address, HTTP01RequestHandler.partial_init(\n                simple_http_resources=resources, timeout=timeout), ipv6=ipv6)\n\n\nclass HTTP01DualNetworkedServers(BaseDualNetworkedServers):\n    \"\"\"HTTP01Server Wrapper. Tries everything for both. Failures for one don't\n       affect the other.\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(HTTP01Server, *args, **kwargs)\n\n\nclass HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):\n    \"\"\"HTTP01 challenge handler.\n\n    Adheres to the stdlib's `socketserver.BaseRequestHandler` interface.\n\n    :ivar set simple_http_resources: A set of `HTTP01Resource`\n        objects. TODO: better name?\n\n    \"\"\"\n    HTTP01Resource = collections.namedtuple(\n        \"HTTP01Resource\", \"chall response validation\")\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self.simple_http_resources = kwargs.pop(\"simple_http_resources\", set())\n        self._timeout = kwargs.pop('timeout', 30)\n        super().__init__(*args, **kwargs)\n        self.server: HTTP01Server\n\n    # In parent class BaseHTTPRequestHandler, 'timeout' is a class-level property but we\n    # need to define its value during the initialization phase in HTTP01RequestHandler.\n    # However MyPy does not appreciate that we dynamically shadow a class-level property\n    # with an instance-level property (eg. self.timeout = ... in __init__()). So to make\n    # everyone happy, we statically redefine 'timeout' as a method property, and set the\n    # timeout value in a new internal instance-level property _timeout.\n    @property\n    def timeout(self) -> int:  # type: ignore[override]\n        \"\"\"\n        The default timeout this server should apply to requests.\n        :return: timeout to apply\n        :rtype: int\n        \"\"\"\n        return self._timeout\n\n    def log_message(self, format: str, *args: Any) -> None:  # pylint: disable=redefined-builtin\n        \"\"\"Log arbitrary message.\"\"\"\n        logger.debug(\"%s - - %s\", self.client_address[0], format % args)\n\n    def handle(self) -> None:\n        \"\"\"Handle request.\"\"\"\n        self.log_message(\"Incoming request\")\n        BaseHTTPServer.BaseHTTPRequestHandler.handle(self)\n\n    def do_GET(self) -> None:  # pylint: disable=invalid-name,missing-function-docstring\n        if self.path == \"/\":\n            self.handle_index()\n        elif self.path.startswith(\"/\" + challenges.HTTP01.URI_ROOT_PATH):\n            self.handle_simple_http_resource()\n        else:\n            self.handle_404()\n\n    def handle_index(self) -> None:\n        \"\"\"Handle index page.\"\"\"\n        self.send_response(200)\n        self.send_header(\"Content-Type\", \"text/html\")\n        self.end_headers()\n        self.wfile.write(self.server.server_version.encode())\n\n    def handle_404(self) -> None:\n        \"\"\"Handler 404 Not Found errors.\"\"\"\n        self.send_response(http_client.NOT_FOUND, message=\"Not Found\")\n        self.send_header(\"Content-type\", \"text/html\")\n        self.end_headers()\n        self.wfile.write(b\"404\")\n\n    def handle_simple_http_resource(self) -> None:\n        \"\"\"Handle HTTP01 provisioned resources.\"\"\"\n        for resource in self.simple_http_resources:\n            if resource.chall.path == self.path:\n                self.log_message(\"Serving HTTP01 with token %r\",\n                                 resource.chall.encode(\"token\"))\n                self.send_response(http_client.OK)\n                self.end_headers()\n                self.wfile.write(resource.validation.encode())\n                return\n        else:  # pylint: disable=useless-else-on-loop\n            self.log_message(\"No resources to serve\")\n        self.log_message(\"%s does not correspond to any resource. ignoring\",\n                         self.path)\n\n    @classmethod\n    def partial_init(cls, simple_http_resources: set[HTTP01RequestHandler.HTTP01Resource],\n                     timeout: int) -> functools.partial[HTTP01RequestHandler]:\n        \"\"\"Partially initialize this handler.\n\n        This is useful because `socketserver.BaseServer` takes\n        uninitialized handler and initializes it with the current\n        request.\n\n        \"\"\"\n        return functools.partial(\n            cls, simple_http_resources=simple_http_resources,\n            timeout=timeout)\n"
  },
  {
    "path": "acme/src/acme/util.py",
    "content": "\"\"\"ACME utilities.\"\"\"\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Mapping\n\n\ndef map_keys(dikt: Mapping[Any, Any], func: Callable[[Any], Any]) -> dict[Any, Any]:\n    \"\"\"Map dictionary keys.\"\"\"\n    return {func(key): value for key, value in dikt.items()}\n"
  },
  {
    "path": "certbot/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot/CHANGELOG.md",
    "content": "# Certbot change log\n\nCertbot adheres to [Semantic Versioning](https://semver.org/).\n\n<!-- towncrier release notes start -->\n\n## 5.4.0 - 2026-03-10\n\n### Added\n\n- The webroot plugin now supports IP address issuance. ([#10543](https://github.com/certbot/certbot/issues/10543))\n\n### Changed\n\n- certbot-nginx now requires pyparsing>=3.0.0. ([#10560](https://github.com/certbot/certbot/issues/10560))\n\n\n## 5.3.1 - 2026-02-09\n\n### Fixed\n\n- We rebuilt our snaps to include updated versions our dependencies. ([#10569](https://github.com/certbot/certbot/issues/10569))\n\n\n## 5.3.0 - 2026-02-03\n\n### Added\n\n- A new command line flag, --ip-address, has been added. This requests certificates with IP address SANs when using the standalone or manual plugin.  Note that for Let's Encrypt's implementation of IP address certificates, you'll also need to pass `--preferred-profile shortlived`. ([#10495](https://github.com/certbot/certbot/issues/10495), [#10544](https://github.com/certbot/certbot/pull/10544))\n\n### Changed\n\n- Deploy directory hooks are now also run when using `certbot certonly` or `certbot run` to get a new cert. This change was made for pre and post directory hooks in our 3.2.0 release so this change unifies Certbot's behavior here. ([#9978](https://github.com/certbot/certbot/issues/9978))\n- A few largely unused functions/types have been deprecated in our effort to remove our pyOpenSSL dependency:\n      * Deprecated: `certbot.crypto_util.get_sans_from_cert`\n      * Deprecated: `certbot.crypto_util.get_names_from_cert`\n      * Deprecated: `certbot.crypto_util.get_names_from_req`\n      * Deprecated: `certbot.crypto_util.import_csr_file` (and replaced by `certbot.crypto_util.read_csr_file`)\n      * Deprecated: `acme.crypto_util.Format` ([#10433](https://github.com/certbot/certbot/issues/10433))\n- `achallenges.KeyAuthorizationAnnotatedChallenge`, `achallenges.DNS`, and `achallenges.Other` have a new field `identifier`, of type `acme.messages.Identifier`. This should be used in place of the `domain` field, which is now deprecated both as an attribute and during object creation. ([#10491](https://github.com/certbot/certbot/issues/10491))\n- Authenticator.get_chall_pref's argument has been renamed from `domain` to `identifier`, and can now receive string-formatted IP addresses in addition to domain names. ([#10495](https://github.com/certbot/certbot/issues/10495))\n- san.DNSName now calls util.enforce_domain_sanity to reduce code duplication ([#10519](https://github.com/certbot/certbot/issues/10519))\n\n### Fixed\n\n- Removed the outdated email address from our Python packages' metadata. ([#10533](https://github.com/certbot/certbot/issues/10533))\n- The HTTP01.uri method will now properly enclose IPv6 addresses in square brackets. ([#10548](https://github.com/certbot/certbot/issues/10548))\n\n\n## 5.2.2 - 2025-12-10\n\n### Fixed\n\n- Fixed a regression that caused certbot to crash if multiple --webroot-path\n  values were set on the command line.\n  ([#10509](https://github.com/certbot/certbot/issues/10509))\n\n\n## 5.2.1 - 2025-12-03\n\n### Added\n\n- Support for Python 3.14 was added.\n  ([#10477](https://github.com/certbot/certbot/issues/10477))\n\n### Changed\n\n- While nothing significant should have changed from the user's perspective,\n  we've been doing a lot of internal refactoring in preparation for soon adding\n  support for IP address certificates to Certbot.\n  ([#10468](https://github.com/certbot/certbot/issues/10468),\n  [#10478](https://github.com/certbot/certbot/issues/10478))\n\n### Fixed\n\n- Removed `vhost_combined` and `vhost_common` log formats from included Apache\n  configuration file. ([#9769](https://github.com/certbot/certbot/issues/9769))\n- Due to a mistake on our end playing with GitHub's new [immutable\n  releases](https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/)\n  feature that prevented our CI from uploading additional release assets,\n  Certbot 5.2.0 was not and will not be uploaded to most platforms. Instead,\n  that version number will be skipped and we'll go straight to 5.2.1.\n  ([#10501](https://github.com/certbot/certbot/issues/10501))\n\n\n## 5.1.0 - 2025-10-07\n\n### Changed\n\n- certbot-nginx no longer creates and uses self-signed certificates as an\n  intermediate step when installing certificates. The certificates the user\n  requested Certbot install are now always used instead.\n  ([#10465](https://github.com/certbot/certbot/issues/10465))\n- The function `acme.crypto_util.make_self_signed_cert` was deprecated and will\n  be removed in a future release.\n  ([#10466](https://github.com/certbot/certbot/issues/10466))\n\n### Fixed\n\n- Fixed a bug in certbot-nginx that'd leave nginx configured with self-signed\n  certificates if a user ran `certbot enhance` and they didn't have matching\n  SSL server blocks. `certbot enhance` now requires the user to have a matching\n  SSL server block to enable HSTS or OCSP stapling enhancements.\n  ([#10455](https://github.com/certbot/certbot/issues/10455))\n\n\n## 5.0.0 - 2025-09-02\n\n### Added\n\n- Certbot now stores the Retry-After value given by ACME Renewal Info (ARI) so\n  the value can be respected across multiple Certbot runs.\n  ([#10377](https://github.com/certbot/certbot/issues/10377))\n- Added `uv` as a test dependency, and switched most `pip` invocations to `uv\n  pip` for faster installs.\n  ([#10428](https://github.com/certbot/certbot/issues/10428))\n\n### Changed\n\n- Removed final instances of pyopenssl x509 and PKey objects\n  * Removed `acme.crypto_util.SSLSocket`\n  * Removed `acme.crypto_util.probe_sni`\n\n  ([#10079](https://github.com/certbot/certbot/issues/10079),\n  [#10381](https://github.com/certbot/certbot/issues/10381))\n- Removed a number of deprecated classes/interfaces\n  * Removed `acme.challenges.TLSALPN01Response`\n  * Removed `acme.challenges.TLSALPN01`\n  * Removed `acme.standalone.TLSServer`\n  * Removed `acme.standalone.TLSALPN01Server`\n\n  ([#10274](https://github.com/certbot/certbot/issues/10274))\n- certbot.ocsp.RevocationChecker.__init__ no longer accepts the parameter\n  `enforce_openssl_binary_usage` and always uses the cryptography Python\n  library for OCSP checking.\n  ([#10291](https://github.com/certbot/certbot/issues/10291))\n- Python 3.9 support was removed.\n  ([#10389](https://github.com/certbot/certbot/issues/10389))\n- Migrated most functionality from `certbot/setup.py` to\n  `certbot/pyproject.toml`\n  ([#10402](https://github.com/certbot/certbot/issues/10402))\n- Migrated most functionality from `setup.py` to `pyproject.toml` for acme,\n  certbot-apache, and certbot-nginx.\n  ([#10417](https://github.com/certbot/certbot/issues/10417))\n- Migrated most functionality from `setup.py` to `pyproject.toml` for certbot\n  dns plugins. ([#10425](https://github.com/certbot/certbot/issues/10425))\n- Updated apache TLS configuration options based on changes to Mozilla's\n  intermediate configuration recommendations.\n  * Added `DHE-RSA-CHACHA20-POLY1305` to `SSLCipherSuite` list for better\n  compliance\n  * Configured curves using `SSLOpenSSLConfCmd` so FFDH won't be used with\n  OpenSSL 3.0\n\n  ([#10443](https://github.com/certbot/certbot/issues/10443))\n\n### Fixed\n\n- certbot-apache no longer prints a warning claiming the version of OpenSSL\n  used by Apache is too old when we were unable determine the OpenSSL version.\n  ([#10444](https://github.com/certbot/certbot/issues/10444))\n- certbot-nginx no longer uses socket.gethostname when generating self-signed\n  certificates for use as a temporary step of installing certificates as it\n  would sometimes result in strings that are too long to be used in the common\n  name of a certificate. The static domain \"temp-certbot-nginx.invalid\" is now\n  used instead. ([#10447](https://github.com/certbot/certbot/issues/10447))\n\n\n## 4.2.0 - 2025-08-05\n\n### Added\n\n- Added `--eab-hmac-alg` parameter to support custom HMAC algorithm for\n  External Account Binding.\n  ([#10281](https://github.com/certbot/certbot/issues/10281))\n\n### Changed\n\n- Catches and ignores errors during the directory fetch for ARI checking so\n  that these errors do not hinder the actual certificate issuance.\n  ([#10342](https://github.com/certbot/certbot/issues/10342))\n- Removed the dependency on `pytz`.\n  ([#10350](https://github.com/certbot/certbot/issues/10350))\n- Deprecated `acme.crypto_util.probe_sni`\n  ([#10386](https://github.com/certbot/certbot/issues/10386))\n- Support for Python 3.9 was deprecated and will be removed in our next planned\n  release. ([#10390](https://github.com/certbot/certbot/issues/10390))\n\n### Fixed\n\n- The Certbot snap no longer sets the environment variable PYTHONPATH stopping\n  it from picking up Python files in the current directory and polluting the\n  environment for Certbot hooks written in Python.\n  ([#10176](https://github.com/certbot/certbot/issues/10176),\n  [#10257](https://github.com/certbot/certbot/issues/10257))\n- Previously, we claimed to set FAILED_DOMAINS and RENEWED_DOMAINS env\n  variables for use by post-hooks when certificate renewals fail, but we were\n  not actually setting them. Now, we are.\n  ([#10259](https://github.com/certbot/certbot/issues/10259))\n- Certbot now always uses the server value from the renewal configuration file\n  for ARI checks instead of the server value from the current invocation of\n  Certbot. This helps prevent ARI requests from going to the wrong server if\n  the user changes CAs.\n  ([#10339](https://github.com/certbot/certbot/issues/10339))\n\n\n## 4.1.1 - 2025-06-12\n\n### Fixed\n\n* When a CA fails to issue a certificate after finalization, print the ACME error from the order\n* No longer checks ARI during certbot --dry-run, because --dry-run uses staging when used\n  with let's encrypt but the cert was issued against the default server. This would emit\n  a scary warning, even though the cert would renew successfully.\n* Contacting the CA to check ARI is now skipped for certificate lineages that\n  have autorenew set to False.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 4.1.0 - 2025-06-10\n\n### Added\n\n* ACME Renewal Info (ARI) support. https://datatracker.ietf.org/doc/draft-ietf-acme-ari/\n  `certbot renew` will automatically check ARI when using an ACME server that supports it,\n  and may renew early based on the ARI information. For Let's Encrypt certificates this\n  will typically cause renewal at around 2/3rds of the certificate's lifetime, even if\n  the renew_before_expiry field of a lineage renewal config is set a later date.\n\n### Changed\n\n* Switched to src-layout from flat-layout to accommodate PEP 517 pip editable installs\n* acme.client.ClientNetwork now makes the \"key\" parameter optional.\n* Deprecated `acme.challenges.TLSALPN01Response`\n* Deprecated `acme.challenges.TLSALPN01`\n* Deprecated parameter `alpn_protocols` from `acme.crypto_util.probe_sni`\n* Deprecated `acme.crypto_util.SSLSocket`\n* Deprecated `acme.standalone.TLSServer`\n* Deprecated `acme.standalone.TLSALPN01Server`\n* Deprecated parameter `enforce_openssl_binary_usage` from certbot.ocsp.RevocationChecker.\n* Dropped support for Python 3.9.0 and 3.9.1 for compatibility with newer\n  versions of the cryptography Python package. Python 3.9.2+ is still\n  supported.\n\n### Fixed\n\n* Order finalization now catches `orderNotReady` response, polls until order status is\n  `ready`, and resubmits finalization request before polling for `valid` to download\n  certificate. This conforms to RFC 8555 more accurately and avoids race conditions where\n  all authorizations are fulfilled but order has not yet transitioned to ready state on\n  the server when the finalization request is sent. It also respects retry-after when\n  polling for finalization readiness.\n* The --preferred-profile and --required-profile flags now have their values stored in\n  the renewal configuration so the same setting will be used on renewal.\n* Fixed an unintended change introduced in 4.0.0 where `renew_before_expiry` could not be\n  shorter than certbot's default renewal time. If the server does not provide an ARI\n  response, `renew_before_expiry` will continue to override certbot's default. However,\n  an early ARI response will override a later `renew_before_expiry` time, to account for\n  notifications in case of certificate revocation, especially with the impending deprecation\n  of OCSP (https://letsencrypt.org/2024/12/05/ending-ocsp/). To force a later date, users\n  can replace certbot's default cron job and/or systemd timer with one of their own timing.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 4.0.0 - 2025-04-07\n\n### Added\n\n* The --preferred-profile and --required-profile flags allow requesting a profile.\n  https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/\n\n### Changed\n\n* Certificates now renew with 1/3rd of lifetime left (or 1/2 of lifetime left,\n  if the lifetime is shorter than 10 days). This is a change from a hardcoded\n  renewal at 30 days before expiration. The config field renew_before_expiry\n  still overrides this default.\n\n* removed `acme.crypto_util._pyopenssl_cert_or_req_all_names`\n* removed `acme.crypto_util._pyopenssl_cert_or_req_san`\n* removed `acme.crypto_util.dump_pyopenssl_chain`\n* removed `acme.crypto_util.gen_ss_cert`\n* removed `certbot.crypto_util.dump_pyopenssl_chain`\n* removed `certbot.crypto_util.pyopenssl_load_certificate`\n\n\n### Fixed\n\n* Moved `RewriteEngine on` directive added during apache http01 authentication\n  to the end of the virtual host, so that it overwrites any `RewriteEngine off`\n  directives that already exist and allows redirection to the challenge URL.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 3.3.0 - 2025-03-11\n\n### Added\n\n*\n\n### Changed\n\n* The --register-unsafely-without-email flag is no longer needed in non-interactive mode.\n* In interactive mode, pressing Enter at the email prompt will register without an email.\n* deprecated `acme.crypto_util._pyopenssl_cert_or_req_all_names`\n* deprecated `acme.crypto_util._pyopenssl_cert_or_req_san`\n* deprecated `acme.crypto_util.dump_pyopenssl_chain`\n* deprecated `certbot.crypto_util.dump_pyopenssl_chain`\n* deprecated `certbot.crypto_util.pyopenssl_load_certificate`\n\n### Fixed\n\n* Fixed a bug introduced in Certbot 3.1.0 where OpenSSL environment variables\n  needed in our snap configuration were persisted in calls to external programs\n  like nginx which could cause them to fail to load OpenSSL.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 3.2.0 - 2025-02-11\n\n### Added\n\n*\n\n### Changed\n\n* certbot-nginx now requires pyparsing>=2.4.7.\n* certbot and its acme library now require cryptography>=43.0.0.\n* certbot-nginx and our acme library now require pyOpenSSL>=25.0.0.\n* Deprecated `gen_ss_cert` in `acme.crypto_util` as it uses deprecated\n  pyOpenSSL API.\n* Add `make_self_signed_cert` to `acme.crypto_util` to replace `gen_ss_cert.\n* Directory hooks are now run on all commands by default, not just `renew`\n* Help output now shows `False` as default when it can be set via `cli.ini` instead of `None`\n* Changed terms of service agreement text to have a newline after the TOS link\n* certbot-cloudflare-dns is now pinned to version 2.19 of Cloudflare's python library\n* Removed support for Linode API v3 which was sunset at the end of July 203.\n\n### Fixed\n\n* Private keys are now saved in PKCS#8 format instead of PKCS#1. Using PKCS#1\n  was a regression introduced in Certbot 3.1.0.\n* Allow nginx plugin to parse non-breaking spaces in nginx configuration files.\n* Honor --reuse-key when --allow-subset-of-names is set\n* Fixed regression in symlink parsing on Windows that was introduced in Certbot\n  3.1.0.\n* When adding ssl listen directives in nginx server blocks, IP addresses are now\n  preserved.\n* Nginx configurations can now have the http block in files other than the root (nginx.conf)\n* Nginx `server_name` directives with internal comments now ignore commented names\n\nMore details about these changes can be found on our GitHub repo.\n\n## 3.1.0 - 2025-01-07\n\n### Added\n\n* Support for Python 3.13 was added.\n\n### Changed\n\n* Python 3.8 support was removed.\n* certbot-dns-rfc2136's minimum required version of dnspython is now 2.6.1.\n* Updated our Docker images to be based on Alpine Linux 3.20.\n* Our runtime dependency on setuptools has been dropped from all Certbot\n  components.\n* Certbot's packages no longer depend on library importlib_resources.\n\n### Fixed\n\n* Included an OpenSSL library that was missing in our Certbot snap fixing\n  crashes affecting 32-bit ARM users.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 3.0.1 - 2024-11-14\n\n### Fixed\n\n* Removed a CryptographyDeprecationWarning that was being displayed to users\n  when checking OCSP status.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 3.0.0 - 2024-11-05\n\n### Added\n\n*\n\n### Changed\n\n* The update_symlinks command was removed.\n* The `csr_dir` and `key_dir` attributes on\n  `certbot.configuration.NamespaceConfig` were removed.\n* The `--manual-public-ip-logging-ok` command line flag was removed.\n* The `--dns-route53-propagation-seconds` command line flag was removed.\n* The `certbot_dns_route53.authenticator` module has been removed. This should\n  not affect any users of the plugin and instead would only affect developers\n  trying to develop on top of the old code.\n* Support for Python 3.8 was deprecated and will be removed in our next planned\n  release.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.11.0 - 2024-06-05\n\n### Added\n\n*\n\n### Changed\n\n* In anticipation of backwards incompatible changes, certbot-dns-cloudflare now\n  requires less than version 2.20 of Cloudflare's python library.\n\n### Fixed\n\n* Fixed a bug in Certbot where a CSR's SANs did not always follow the order of\n  the domain names that the user requested interactively. In some cases, the\n  resulting cert's common name might seem picked up randomly from the SANs\n  when it should be the first item the user had in mind.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.10.0 - 2024-04-02\n\n### Added\n\n* The Python source packages which we upload to [PyPI](https://pypi.org/) are\n  now also being uploaded to\n  [our releases on GitHub](https://github.com/certbot/certbot/releases) where\n  we now also include a SHA256SUMS checksum file and a PGP signature for that\n  file.\n\n### Changed\n\n* We no longer publish our beta Windows installer as was originally announced\n  [here](https://community.letsencrypt.org/t/certbot-discontinuing-windows-beta-support-in-2024/208101).\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.9.0 - 2024-02-08\n\n### Added\n\n* Support for Python 3.12 was added.\n\n### Changed\n\n*\n\n### Fixed\n\n* Updates `joinpath` syntax to only use one addition per call, because the multiple inputs\n  version was causing mypy errors on Python 3.10.\n* Makes the `reconfigure` verb actually use the staging server for the dry run to check the new\n  configuration.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.8.0 - 2023-12-05\n\n### Added\n\n* Added support for [Alpine Linux](https://www.alpinelinux.org) distribution when is used the apache plugin\n\n### Changed\n\n* Support for Python 3.7 was removed.\n\n### Fixed\n\n* Stop using the deprecated `pkg_resources` API included in `setuptools`.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.7.4 - 2023-11-01\n\n### Fixed\n\n* Fixed a bug introduced in version 2.7.0 that caused interactively entered\n  webroot plugin values to not be saved for renewal.\n* Fixed a bug introduced in version 2.7.0 of our Lexicon based DNS plugins that\n  caused them to fail to find the DNS zone that needs to be modified in some\n  cases.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.7.3 - 2023-10-24\n\n### Fixed\n\n* Fixed a bug where arguments with contained spaces weren't being handled correctly\n* Fixed a bug that caused the ACME account to not be properly restored on\n  renewal causing problems in setups where the user had multiple accounts with\n  the same ACME server.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.7.2 - 2023-10-19\n\n### Fixed\n\n* `certbot-dns-ovh` plugin now requires `lexicon>=3.15.1` to ensure a consistent behavior with OVH APIs.\n* Fixed a bug where argument sources weren't correctly detected in abbreviated\n  arguments, short arguments, and some other circumstances\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.7.1 - 2023-10-10\n\n### Fixed\n\n* Fixed a bug that broke the DNS plugin for DNSimple that was introduced in\n  version 2.7.0 of the plugin.\n* Correctly specified the new minimum version of the ConfigArgParse package\n  that Certbot requires which is 1.5.3.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.7.0 - 2023-10-03\n\n### Added\n\n* Add `certbot.util.LooseVersion` class. See [GH #9489](https://github.com/certbot/certbot/issues/9489).\n* Add a new base class `certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator` to implement a DNS\n  authenticator plugin backed by Lexicon to communicate with the provider DNS API. This approach relies\n  heavily on conventions to reduce the implementation complexity of a new plugin.\n* Add a new test base class `certbot.plugins.dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest` to\n  help testing DNS plugins implemented on top of `LexiconDNSAuthenticator`.\n\n### Changed\n\n* `NamespaceConfig` now tracks how its arguments were set via a dictionary, allowing us to remove a bunch\n  of global state previously needed to inspect whether a user set an argument or not.\n* Support for Python 3.7 was deprecated and will be removed in our next planned release.\n* Added `RENEWED_DOMAINS` and `FAILED_DOMAINS` environment variables for consumption by post renewal hooks.\n* Deprecates `LexiconClient` base class and `build_lexicon_config` function in\n  `certbot.plugins.dns_common_lexicon` module in favor of `LexiconDNSAuthenticator`.\n* Deprecates `BaseLexiconAuthenticatorTest` and `BaseLexiconClientTest` test base classes of\n  `certbot.plugins.dns_test_common_lexicon` module in favor of `BaseLexiconDNSAuthenticatorTest`.\n\n### Fixed\n\n* Do not call deprecated datetime.utcnow() and datetime.utcfromtimestamp()\n* Filter zones in `certbot-dns-google` to avoid usage of private DNS zones to create records\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.6.0 - 2023-05-09\n\n### Added\n\n* `--dns-google-project` optionally allows for specifying the project that the DNS zone(s) reside in,\n  which allows for Certbot usage in scenarios where the auth credentials reside in a different\n  project to the zone(s) that are being managed.\n* There is now a new `Other` annotated challenge object to allow plugins to support entirely novel challenges.\n\n### Changed\n\n* Optionally sign the SOA query for dns-rfc2136, to help resolve problems with split-view\n  DNS setups and hidden primary setups.\n  * Certbot versions prior to v1.32.0 did not sign queries with the specified TSIG key\n    resulting in difficulty with split-horizon implementations.\n  * Certbot v1.32.0 through v2.5.0 signed queries by default, potentially causing\n    incompatibility with hidden primary setups with `allow-update-forwarding` enabled\n    if the secondary did not also have the TSIG key within its config.\n  * Certbot v2.6.0 and later no longer signs queries by default, but allows\n    the user to optionally sign these queries by explicit configuration using the\n    `dns_rfc2136_sign_query` option in the credentials .ini file.\n* Lineage name validity is performed for new lineages. `--cert-name` may no longer contain\n  filepath separators (i.e. `/` or `\\`, depending on the platform).\n* `certbot-dns-google` now loads credentials using the standard [Application Default\n  Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) strategy,\n  rather than explicitly requiring the Google Compute metadata server to be present if a service account\n  is not provided using `--dns-google-credentials`.\n* `--dns-google-credentials` now supports additional types of file-based credential, such as\n  [External Account Credentials](https://google.aip.dev/auth/4117) created by Workload Identity\n  Federation. All file-based credentials implemented by the Google Auth library are supported.\n\n### Fixed\n\n* `certbot-dns-google` no longer requires deprecated `oauth2client` library.\n* Certbot will no longer try to invoke plugins which do not subclass from the proper\n  `certbot.interfaces.{Installer,Authenticator}` interface (e.g. `certbot -i standalone`\n  will now be ignored). See [GH-9664](https://github.com/certbot/certbot/issues/9664).\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.5.0 - 2023-04-04\n\n### Added\n\n* `acme.messages.OrderResource` now supports being round-tripped\n  through JSON\n* acme.client.ClientV2 now provides separate `begin_finalization`\n  and `poll_finalization` methods, in addition to the existing\n  `finalize_order` method.\n\n### Changed\n\n* `--dns-route53-propagation-seconds` is now deprecated. The Route53 plugin relies on the\n  [GetChange API](https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html)\n  to determine if a DNS update is complete. The flag has never had any effect and will be\n  removed in a future version of Certbot.\n* Packaged tests for all Certbot components besides josepy were moved inside\n  the `_internal/tests` module.\n\n### Fixed\n\n* Fixed `renew` sometimes not preserving the key type of RSA certificates.\n  * Users who upgraded from Certbot <v1.25.0 to Certbot >=v2.0.0 may\n    have had their RSA certificates inadvertently changed to ECDSA certificates. If desired,\n    the key type may be changed back to RSA. See the [User Guide](https://eff-certbot.readthedocs.io/en/stable/using.html#changing-a-certificate-s-key-type).\n* Deprecated flags were inadvertently not printing warnings since v1.16.0. This is now fixed.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.4.0 - 2023-03-07\n\n### Added\n\n* We deprecated support for the update_symlinks command. Support will be removed in a following\n  version of Certbot.\n\n### Changed\n\n* Docker build and deploy scripts now generate multiarch manifests for non-architecture-specific tags, instead of defaulting to amd64 images.\n\n### Fixed\n\n* Reverted [#9475](https://github.com/certbot/certbot/pull/9475) due to a performance regression in large nginx deployments.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.3.0 - 2023-02-14\n\n### Added\n\n* Allow a user to modify the configuration of a certificate without renewing it using the new `reconfigure` subcommand. See `certbot help reconfigure` for details.\n* `certbot show_account` now displays the [ACME Account Thumbprint](https://datatracker.ietf.org/doc/html/rfc8555#section-8.1).\n\n### Changed\n\n* Certbot will no longer save previous CSRs and certificate private keys to `/etc/letsencrypt/csr` and `/etc/letsencrypt/keys`, respectively. These directories may be safely deleted.\n* Certbot will now only keep the current and 5 previous certificates in the `/etc/letsencrypt/archive` directory for each certificate lineage. Any prior certificates will be automatically deleted upon renewal. This number may be further lowered in future releases.\n  * As always, users should only reference the certificate files within `/etc/letsencrypt/live` and never use `/etc/letsencrypt/archive` directly. See [Where are my certificates?](https://eff-certbot.readthedocs.io/en/stable/using.html#where-are-my-certificates) in the Certbot User Guide.\n* `certbot.configuration.NamespaceConfig.key_dir` and `.csr_dir` are now deprecated.\n* All Certbot components now require `pytest` to run tests.\n\n### Fixed\n\n* Fixed a crash when registering an account with BuyPass' ACME server.\n* Fixed a bug where Certbot would crash with `AttributeError: can't set attribute` on ACME server errors in Python 3.11. See [GH #9539](https://github.com/certbot/certbot/issues/9539).\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.2.0 - 2023-01-11\n\n### Added\n\n*\n\n### Changed\n\n* Certbot will no longer respect very long challenge polling intervals, which may be suggested\n  by some ACME servers. Certbot will continue to wait up to 90 seconds by default, or up to a\n  total of 30 minutes if requested by the server via `Retry-After`.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.32.2 - 2022-12-16\n\n### Fixed\n\n* Our snaps and Docker images were rebuilt to include updated versions of our dependencies.\n\nThis release was not pushed to PyPI since those packages were unaffected.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.1.1 - 2022-12-15\n\n### Fixed\n\n* Our snaps, Docker images, and Windows installer were rebuilt to include updated versions of our dependencies.\n\nThis release was not pushed to PyPI since those packages were unaffected.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.1.0 - 2022-12-07\n\n### Added\n\n*\n\n### Changed\n\n*\n\n### Fixed\n\n* Interfaces which plugins register themselves as implementing without inheriting from them now show up in `certbot plugins` output.\n* `IPluginFactory`, `IPlugin`, `IAuthenticator` and `IInstaller` have been re-added to\n  `certbot.interfaces`.\n    - This is to fix compatibility with a number of third-party DNS plugins which may\n      have started erroring with `AttributeError` in Certbot v2.0.0.\n    - Plugin authors can find more information about Certbot 2.x compatibility\n      [here](https://github.com/certbot/certbot/wiki/Certbot-v2.x-Plugin-Compatibility).\n* A bug causing our certbot-apache tests to crash on some systems has been resolved.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.32.1 - 2022-12-05\n\n### Fixed\n\n* Our snaps and docker images were rebuilt to include updated versions of our dependencies.\n\nThis release was not pushed to PyPI since those packages were unaffected.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 2.0.0 - 2022-11-21\n\n### Added\n\n* Support for Python 3.11 was added to Certbot and all of its components.\n* `acme.challenges.HTTP01Response.simple_verify` now accepts a timeout argument which defaults to 30 that causes the verification request to timeout after that many seconds.\n\n### Changed\n\n* The default key type for new certificates is now ECDSA `secp256r1` (P-256). It was previously RSA 2048-bit. Existing certificates are not affected.\n* The Apache plugin no longer supports Apache 2.2.\n* `acme` and Certbot no longer support versions of ACME from before the RFC 8555 standard.\n* `acme` and Certbot no longer support the old `urn:acme:error:` ACME error prefix.\n* Removed the deprecated `certbot-dns-cloudxns` plugin.\n* Certbot will now error if a certificate has `--reuse-key` set and a conflicting `--key-type`, `--key-size` or `--elliptic-curve` is requested on the CLI. Use `--new-key` to change the key while preserving `--reuse-key`.\n* 3rd party plugins no longer support the `dist_name:plugin_name` format on the CLI and in configuration files. Use the shorter `plugin_name` format.\n* `acme.client.Client`, `acme.client.ClientBase`, `acme.client.BackwardsCompatibleClientV2`, `acme.mixins`, `acme.client.DER_CONTENT_TYPE`, `acme.fields.Resource`, `acme.fields.resource`, `acme.magic_typing`, `acme.messages.OLD_ERROR_PREFIX`, `acme.messages.Directory.register`, `acme.messages.Authorization.resolved_combinations`, `acme.messages.Authorization.combinations` have been removed.\n* `acme.messages.Directory` now only supports lookups by the exact resource name string in the ACME directory (e.g. `directory['newOrder']`).\n* Removed the deprecated `source_address` argument for `acme.client.ClientNetwork`.\n* The `zope` based interfaces in `certbot.interfaces` have been removed in favor of the `abc` based interfaces found in the same module.\n* Certbot no longer depends on `zope`.\n* Removed deprecated function `certbot.util.get_strict_version`.\n* Removed deprecated functions `certbot.crypto_util.init_save_csr`, `certbot.crypto_util.init_save_key`,\n  and `certbot.compat.misc.execute_command`\n* The attributes `FileDisplay`, `NoninteractiveDisplay`, `SIDE_FRAME`, `input_with_timeout`, `separate_list_input`, `summarize_domain_list`, `HELP`, and `ESC` from `certbot.display.util` have been removed.\n* Removed deprecated functions `certbot.tests.util.patch_get_utility*`. Plugins should now\n  patch `certbot.display.util` themselves in their tests or use\n  `certbot.tests.util.patch_display_util` as a temporary workaround.\n* Certbot's test API under `certbot.tests` now uses `unittest.mock` instead of the 3rd party `mock` library.\n\n### Fixed\n\n* Fixes a bug where the certbot working directory has unusably restrictive permissions on systems with stricter default umasks.\n* Requests to subscribe to the EFF mailing list now time out after 60 seconds.\n\nWe plan to slowly roll out Certbot 2.0 to all of our snap users in the coming months. If you want to use the Certbot 2.0 snap now, please follow the instructions at https://community.letsencrypt.org/t/certbot-2-0-beta-call-for-testing/185945.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.32.0 - 2022-11-08\n\n### Added\n\n*\n\n### Changed\n\n* DNS RFC2136 module now uses the TSIG key to check for an authoritative SOA record. Helps the use of split-horizon and multiple views in BIND9 using the key in an ACL to determine which view to use.\n\n### Fixed\n\n* CentOS 9 and other RHEL-derived OSes now correctly use httpd instead of apachectl for\n  various Apache-related commands\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.31.0 - 2022-10-04\n\n### Added\n\n*\n\n### Changed\n\n* If Certbot exits before setting up its usual log files, the temporary directory created to save logging information will begin with the name `certbot-log-` rather than a generic name. This should not be considered a [stable aspect of Certbot](https://certbot.eff.org/docs/compatibility.html) and may change again in the future.\n\n### Fixed\n\n* Fixed an incompatibility in the certbot-dns-cloudflare plugin and the Cloudflare library\n  which was introduced in the Cloudflare library version 2.10.1. The library would raise\n  an error if a token was specified in the Certbot `--dns-cloudflare-credentials` file as\n  well as the `cloudflare.cfg` configuration file of the Cloudflare library.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.30.0 - 2022-09-07\n\n### Added\n\n*\n\n### Changed\n\n* `acme.client.ClientBase`, `acme.messages.Authorization.resolved_combinations`,\n  `acme.messages.Authorization.combinations`, `acme.mixins`, `acme.fields.resource`,\n  and `acme.fields.Resource` are deprecated and will be removed in a future release.\n* `acme.messages.OLD_ERROR_PREFIX` (`urn:acme:error:`) is deprecated and support for\n  the old ACME error prefix in Certbot will be removed in the next major release of\n  Certbot.\n* `acme.messages.Directory.register` is deprecated and will be removed in the next\n  major release of Certbot. Furthermore, `.Directory` will only support lookups\n  by the exact resource name string in the ACME directory  (e.g. `directory['newOrder']`).\n* The `certbot-dns-cloudxns` plugin is now deprecated and will be removed in the\n  next major release of Certbot.\n* The `source_address` argument for `acme.client.ClientNetwork` is deprecated\n  and support for it will be removed in the next major release.\n* Add UI text suggesting users create certs for multiple domains, when possible\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.29.0 - 2022-07-05\n\n### Added\n\n* Updated Windows installer to be signed and trusted in Windows\n\n### Changed\n\n* `--allow-subset-of-names` will now additionally retry in cases where domains are rejected while creating or finalizing orders. This requires subproblem support from the ACME server.\n\n### Fixed\n\n* The `show_account` subcommand now uses the \"newAccount\" ACME endpoint to fetch the account\n  data, so it doesn't rely on the locally stored account URL. This fixes situations where Certbot\n  would use old ACMEv1 registration info with non-functional account URLs.\n\n* The generated Certificate Signing Requests are now generated as version 1 instead of version 3. This resolves situations in where strict enforcement of PKCS#10 meant that CSRs that were generated as version 3 were rejected.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.28.0 - 2022-06-07\n\n### Added\n\n* Updated Apache/NGINX TLS configs to document contents are based on ssl-config.mozilla.org\n\n\n### Changed\n\n* A change to order finalization has been made to the `acme` module and Certbot:\n  - An order's `certificate` field will only be processed if the order's `status` is `valid`.\n  - An order's `error` field will only be processed if the order's `status` is `invalid`.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.27.0 - 2022-05-03\n\n### Added\n\n* Added support for RFC8555 subproblems to our acme library.\n\n### Changed\n\n* The PGP key `F2871B4152AE13C49519111F447BF683AA3B26C3` was added as an\n  additional trusted key to sign our PyPI packages\n* When `certonly` is run with an installer specified (e.g.  `--nginx`),\n  `certonly` will now also run `restart` for that installer\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.26.0 - 2022-04-05\n\n### Added\n\n* Added `--new-key`. When renewing or replacing a certificate that has `--reuse-key`\n  set, it will force a new private key to be generated, one time.\n\n  As before, `--reuse-key` and `--no-reuse-key` can be used to enable and disable key\n  reuse.\n\n### Changed\n\n* The default propagation timeout for the OVH DNS plugin (`--dns-ovh-propagation-seconds`)\n  has been increased from 30 seconds to 120 seconds, based on user feedback.\n\n### Fixed\n\n* Certbot for Windows has been upgraded to use Python 3.9.11, in response to\n  https://www.openssl.org/news/secadv/20220315.txt.\n* Previously, when Certbot was in the process of registering a new ACME account\n  and the ACME server did not present any Terms of Service, the user was asked to\n  agree with a non-existent Terms of Service (\"None\"). This bug is now fixed, so\n  that if an ACME server does not provide any Terms of Service to agree with, the\n  user is not asked to agree to a non-existent Terms of Service any longer.\n* If account registration fails, Certbot did not relay the error from the ACME server\n  back to the user. This is now fixed: the error message from the ACME server is now\n  presented to the user when account registration fails.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.25.0 - 2022-03-16\n\n### Added\n\n*\n\n### Changed\n\n* Dropped 32 bit support for the Windows beta installer\n* Windows beta installer is now distributed as \"certbot-beta-installer-win_amd64.exe\".\n  Users of the Windows beta should uninstall the old version before running this.\n* Added a check whether OCSP stapling is supported by the installer when requesting a\n  certificate with the `run` subcommand in combination with the `--must-staple` option.\n  If the installer does not support OCSP and the `--must-staple` option is used, Certbot\n  will raise an error and quit.\n* Certbot and its acme module now depend on josepy>=1.13.0 due to better type annotation\n  support.\n\n### Fixed\n\n* Updated dependencies to use new version of cryptography that uses OpenSSL 1.1.1n, in\n  response to https://www.openssl.org/news/secadv/20220315.txt.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.24.0 - 2022-03-01\n\n### Added\n\n* When the `--debug-challenges` option is used in combination with `-v`, Certbot\n  now displays the challenge URLs (for `http-01` challenges) or FQDNs (for\n  `dns-01` challenges) and their expected return values.\n*\n\n### Changed\n\n* Support for Python 3.6 was removed.\n* All Certbot components now require setuptools>=41.6.0.\n* The acme library now requires requests>=2.20.0.\n* Certbot and its acme library now require pytz>=2019.3.\n* certbot-nginx now requires pyparsing>=2.2.1.\n* certbot-dns-route53 now requires boto3>=1.15.15.\n\n### Fixed\n\n* Nginx plugin now checks included files for the singleton server_names_hash_bucket_size directive.\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.23.0 - 2022-02-08\n\n### Added\n\n* Added `show_account` subcommand, which will fetch the account information\n  from the ACME server and show the account details (account URL and, if\n  applicable, email address or addresses)\n* We deprecated support for Python 3.6 in Certbot and its ACME library.\n  Support for Python 3.6 will be removed in the next major release of Certbot.\n\n### Changed\n\n*\n\n### Fixed\n\n* GCP Permission list for certbot-dns-google in plugin documentation\n* dns-digitalocean used the SOA TTL for newly created records, rather than 30 seconds.\n* Revoking a certificate based on an ECDSA key can now be done with `--key-path`.\n  See [GH #8569](https://github.com/certbot/certbot/issues/8569).\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.22.0 - 2021-12-07\n\n### Added\n\n* Support for Python 3.10 was added to Certbot and all of its components.\n* The function certbot.util.parse_loose_version was added to parse version\n  strings in the same way as the now deprecated distutils.version.LooseVersion\n  class from the Python standard library.\n* Added `--issuance-timeout`. This option specifies how long (in seconds) Certbot will wait\n  for the server to issue a certificate.\n\n### Changed\n\n* The function certbot.util.get_strict_version was deprecated and will be\n  removed in a future release.\n\n### Fixed\n\n* Fixed an issue on Windows where the `web.config` created by Certbot would sometimes\n  conflict with preexisting configurations (#9088).\n* Fixed an issue on Windows where the `webroot` plugin would crash when multiple domains\n  had the same webroot. This affected Certbot 1.21.0.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.21.0 - 2021-11-02\n\n### Added\n\n* Certbot will generate a `web.config` file on Windows in the challenge path\n  when the `webroot` plugin is used, if one does not exist. This `web.config` file\n  lets IIS serve challenge files while they do not have an extension.\n\n### Changed\n\n* We changed the PGP key used to sign the packages we upload to PyPI. Going\n  forward, releases will be signed with one of three different keys. All of\n  these keys are available on major key servers and signed by our previous PGP\n  key. The fingerprints of these new keys are:\n    * BF6BCFC89E90747B9A680FD7B6029E8500F7DB16\n    * 86379B4F0AF371B50CD9E5FF3402831161D1D280\n    * 20F201346BF8F3F455A73F9A780CC99432A28621\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.20.0 - 2021-10-05\n\n### Added\n\n* Added `--no-reuse-key`. This remains the default behavior, but the flag may be\n  useful to unset the `--reuse-key` option on existing certificates.\n\n### Changed\n\n*\n\n### Fixed\n\n* The certbot-dns-rfc2136 plugin in Certbot 1.19.0 inadvertently had an implicit\n  dependency on `dnspython>=2.0`. This has been relaxed to `dnspython>=1.15.0`.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.19.0 - 2021-09-07\n\n### Added\n\n* The certbot-dns-rfc2136 plugin always assumed the use of an IP address as the\n  target server, but this was never checked. Until now. The plugin raises an error\n  if the configured target server is not a valid IPv4 or IPv6 address.\n* Our acme library now supports requesting certificates for IP addresses.\n  This feature is still unsupported by Certbot and Let's Encrypt.\n\n### Changed\n\n* Several attributes in `certbot.display.util` module are deprecated and will\n  be removed in a future release of Certbot. Any import of these attributes will\n  emit a warning to prepare the transition for developers.\n* `zope` based interfaces in `certbot.interfaces` module are deprecated and will\n  be removed in a future release of Certbot. Any import of these interfaces will\n  emit a warning to prepare the transition for developers.\n* We removed the dependency on `chardet` from our acme library. Except for when\n  downloading a certificate in an alternate format, our acme library now\n  assumes all server responses are UTF-8 encoded which is required by RFC 8555.\n\n### Fixed\n\n* Fixed parsing of `Define`d values in the Apache plugin to allow for `=` in the value.\n* Fixed a relatively harmless crash when issuing a certificate with `--quiet`/`-q`.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.18.0 - 2021-08-03\n\n### Added\n\n* New functions that Certbot plugins can use to interact with the user have\n  been added to `certbot.display.util`. We plan to deprecate using `IDisplay`\n  with `zope` in favor of these new functions in the future.\n* The `Plugin`, `Authenticator` and `Installer` classes are added to\n  `certbot.interfaces` module as alternatives to Certbot's current `zope` based\n  plugin interfaces. The API of these interfaces is identical, but they are\n  based on Python's `abc` module instead of `zope`. Certbot will continue to\n  detect plugins that implement either interface, but we plan to drop support\n  for `zope` based interfaces in a future version of Certbot.\n* The class `certbot.configuration.NamespaceConfig` is added to the Certbot's\n  public API.\n\n### Changed\n\n* When self-validating HTTP-01 challenges using\n  acme.challenges.HTTP01Response.simple_verify, we now assume that the response\n  is composed of only ASCII characters. Previously we were relying on the\n  default behavior of the requests library which tries to guess the encoding of\n  the response which was error prone.\n* `acme`: the `.client.Client` and `.client.BackwardsCompatibleClientV2` classes\n  are now deprecated in favor of `.client.ClientV2`.\n* The `certbot.tests.patch_get_utility*` functions have been deprecated.\n  Plugins should now patch `certbot.display.util` themselves in their tests or\n  use `certbot.tests.util.patch_display_util` as a temporary workaround.\n* In order to simplify the transition to Certbot's new plugin interfaces, the\n  classes `Plugin` and `Installer` in `certbot.plugins.common` module and\n  `certbot.plugins.dns_common.DNSAuthenticator` now implement Certbot's new\n  plugin interfaces. The Certbot plugins based on these classes are now\n  automatically detected as implementing these interfaces.\n* We added a dependency on `chardet` to our acme library so that it will be\n  used over `charset_normalizer` in newer versions of `requests`.\n\n### Fixed\n\n* The Apache authenticator no longer crashes with \"Unable to insert label\"\n  when encountering a completely empty vhost. This issue affected Certbot 1.17.0.\n* Users of the Certbot snap on Debian 9 (Stretch) should no longer encounter an\n  \"access denied\" error when installing DNS plugins.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.17.0 - 2021-07-06\n\n### Added\n\n* Add Void Linux overrides for certbot-apache.\n\n### Changed\n\n* We changed how dependencies are specified between Certbot packages. For this\n  and future releases, higher level Certbot components will require that lower\n  level components are the same version or newer. More specifically, version X\n  of the Certbot package will now always require acme>=X and version Y of a\n  plugin package will always require acme>=Y and certbot=>Y. Specifying\n  dependencies in this way simplifies testing and development.\n* The Apache authenticator now always configures virtual hosts which do not have\n  an explicit `ServerName`. This should make it work more reliably with the\n  default Apache configuration in Debian-based environments.\n\n### Fixed\n\n* When we increased the logging level on our nginx \"Could not parse file\" message,\n  it caused a previously-existing inability to parse empty files to become more\n  visible. We have now added the ability to correctly parse empty files, so that\n  message should only show for more significant errors.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.16.0 - 2021-06-01\n\n### Added\n\n*\n\n### Changed\n\n* DNS plugins based on lexicon now require dns-lexicon >= v3.1.0\n* Use UTF-8 encoding for renewal configuration files\n* Windows installer now cleans up old Certbot dependency packages\n  before installing the new ones to avoid version conflicts.\n* This release contains a substantial command-line UX overhaul,\n  based on previous user research. The main goal was to streamline\n  and clarify output. If you would like to see more verbose output, use\n  the -v or -vv flags. UX improvements are an iterative process and\n  the Certbot team welcomes constructive feedback.\n* Functions `certbot.crypto_util.init_save_key` and `certbot.crypto_util.init_save_csr`,\n  whose behaviors rely on the global Certbot `config` singleton, are deprecated and will\n  be removed in a future release. Please use `certbot.crypto_util.generate_key` and\n  `certbot.crypto_util.generate_csr` instead.\n\n### Fixed\n\n* Fix TypeError due to incompatibility with lexicon >= v3.6.0\n* Installers (e.g. nginx, Apache) were being restarted unnecessarily after dry-run renewals.\n* Colors and bold text should properly render in all supported versions of Windows.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.15.0 - 2021-05-04\n\n### Added\n\n*\n\n### Changed\n\n*\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.14.0 - 2021-04-06\n\n### Added\n\n*\n\n### Changed\n\n* certbot-auto no longer checks for updates on any operating system.\n* The module `acme.magic_typing` is deprecated and will be removed in a future release.\n  Please use the built-in module `typing` instead.\n* The DigitalOcean plugin now creates TXT records for the DNS-01 challenge with a lower 30s TTL.\n\n### Fixed\n\n* Don't output an empty line for a hidden certificate when `certbot certificates` is being used\n  in combination with `--cert-name` or `-d`.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.13.0 - 2021-03-02\n\n### Added\n\n*\n\n### Changed\n\n* CLI flags `--os-packages-only`, `--no-self-upgrade`, `--no-bootstrap` and `--no-permissions-check`,\n  which are related to certbot-auto, are deprecated and will be removed in a future release.\n* Certbot no longer conditionally depends on an external mock module. Certbot's\n  test API will continue to use it if it is available for backwards\n  compatibility, however, this behavior has been deprecated and will be removed\n  in a future release.\n* The acme library no longer depends on the `security` extras from `requests`\n  which was needed to support SNI in TLS requests when using old versions of\n  Python 2.\n* Certbot and all of its components no longer depend on the library `six`.\n* The update of certbot-auto itself is now disabled on all RHEL-like systems.\n* When revoking a certificate by `--cert-name`, it is no longer necessary to specify the `--server`\n  if the certificate was obtained from a non-default ACME server.\n* The nginx authenticator now configures all matching HTTP and HTTPS vhosts for the HTTP-01\n  challenge. It is now compatible with external HTTPS redirection by a CDN or load balancer.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.12.0 - 2021-02-02\n\n### Added\n\n*\n\n### Changed\n\n* The `--preferred-chain` flag now only checks the Issuer Common Name of the\n  topmost (closest to the root) certificate in the chain, instead of checking\n  every certificate in the chain.\n  See [#8577](https://github.com/certbot/certbot/issues/8577).\n* Support for Python 2 has been removed.\n* In previous releases, we caused certbot-auto to stop updating its Certbot\n  installation. In this release, we are beginning to disable updates to the\n  certbot-auto script itself. This release includes Amazon Linux users, and all\n  other systems that are not based on Debian or RHEL. We plan to make this\n  change to the certbot-auto script for all users in the coming months.\n\n### Fixed\n\n* Fixed the apache component on openSUSE Tumbleweed which no longer provides\n  an apache2ctl symlink and uses apachectl instead.\n* Fixed a typo in `certbot/crypto_util.py` causing an error upon attempting `secp521r1` key generation\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.11.0 - 2021-01-05\n\n### Added\n\n*\n\n### Changed\n\n* We deprecated support for Python 2 in Certbot and its ACME library.\n  Support for Python 2 will be removed in the next planned release of Certbot.\n* certbot-auto was deprecated on all systems. For more information about this\n  change, see\n  https://community.letsencrypt.org/t/certbot-auto-no-longer-works-on-debian-based-systems/139702/7.\n* We deprecated support for Apache 2.2 in the certbot-apache plugin and it will\n  be removed in a future release of Certbot.\n\n### Fixed\n\n* The Certbot snap no longer loads packages installed via `pip install --user`. This\n  was unintended and DNS plugins should be installed via `snap` instead.\n* `certbot-dns-google` would sometimes crash with HTTP 409/412 errors when used with very large zones. See [#6036](https://github.com/certbot/certbot/issues/6036).\n* `certbot-dns-google` would sometimes crash with an HTTP 412 error if preexisting records had an unexpected TTL, i.e.: different than Certbot's default TTL for this plugin. See [#8551](https://github.com/certbot/certbot/issues/8551).\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.10.1 - 2020-12-03\n\n### Fixed\n\n* Fixed a bug in `certbot.util.add_deprecated_argument` that caused the\n  deprecated `--manual-public-ip-logging-ok` flag to crash Certbot in some\n  scenarios.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.10.0 - 2020-12-01\n\n### Added\n\n* Added timeout to DNS query function calls for dns-rfc2136 plugin.\n* Confirmation when deleting certificates\n* CLI flag `--key-type` has been added to specify 'rsa' or 'ecdsa' (default 'rsa').\n* CLI flag `--elliptic-curve` has been added which takes an NIST/SECG elliptic curve. Any of\n  `secp256r1`, `secp384r1` and `secp521r1` are accepted values.\n* The command `certbot certificates` lists the which type of the private key that was used\n  for the private key.\n* Support for Python 3.9 was added to Certbot and all of its components.\n\n### Changed\n\n* certbot-auto was deprecated on Debian based systems.\n* CLI flag `--manual-public-ip-logging-ok` is now a no-op, generates a\n  deprecation warning, and will be removed in a future release.\n\n### Fixed\n\n* Fixed a Unicode-related crash in the nginx plugin when running under Python 2.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.9.0 - 2020-10-06\n\n### Added\n\n* `--preconfigured-renewal` flag, for packager use only.\n  See the [packaging guide](https://certbot.eff.org/docs/packaging.html).\n\n### Changed\n\n* certbot-auto was deprecated on all systems except for those based on Debian or RHEL.\n* Update the packaging instructions to promote usage of `python -m pytest` to test Certbot\n  instead of the deprecated `python setup.py test` setuptools approach.\n* Reduced CLI logging when reloading nginx, if it is not running.\n* Reduced CLI logging when handling some kinds of errors.\n\n### Fixed\n\n* Fixed `server_name` case-sensitivity in the nginx plugin.\n* The minimum version of the `acme` library required by Certbot was corrected.\n  In the previous release, Certbot said it required `acme>=1.6.0` when it\n  actually required `acme>=1.8.0` to properly support removing contact\n  information from an ACME account.\n* Upgraded the version of httplib2 used in our snaps and Docker images to add\n  support for proxy environment variables and fix the plugin for Google Cloud\n  DNS.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.8.0 - 2020-09-08\n\n### Added\n\n* Added the ability to remove email and phone contact information from an account\n  using `update_account --register-unsafely-without-email`\n\n### Changed\n\n* Support for Python 3.5 has been removed.\n\n### Fixed\n\n* The problem causing the Apache plugin in the Certbot snap on ARM systems to\n  fail to load the Augeas library it depends on has been fixed.\n* The `acme` library can now tell the ACME server to clear contact information by passing an empty\n  `tuple` to the `contact` field of a `Registration` message.\n* Fixed the `*** stack smashing detected ***` error in the Certbot snap on some systems.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.7.0 - 2020-08-04\n\n### Added\n\n* Third-party plugins can be used without prefix (`plugin_name` instead of `dist_name:plugin_name`):\n  this concerns the plugin name, CLI flags, and keys in credential files.\n  The prefixed form is still supported but is deprecated, and will be removed in a future release.\n* Added `--nginx-sleep-seconds` (default `1`) for environments where nginx takes a long time to reload.\n\n### Changed\n\n* The Linode DNS plugin now waits 120 seconds for DNS propagation, instead of 1200,\n  due to https://www.linode.com/blog/linode/linode-turns-17/\n* We deprecated support for Python 3.5 in Certbot and its ACME library.\n  Support for Python 3.5 will be removed in the next major release of Certbot.\n\n### Fixed\n\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.6.0 - 2020-07-07\n\n### Added\n\n* Certbot snaps are now available for the arm64 and armhf architectures.\n* Add minimal code to run Nginx plugin on NetBSD.\n* Make Certbot snap find externally snapped plugins\n* Function `certbot.compat.filesystem.umask` is a drop-in replacement for `os.umask`\n  implementing umask for both UNIX and Windows systems.\n* Support for alternative certificate chains in the `acme` module.\n* Added `--preferred-chain <issuer CN>`. If a CA offers multiple certificate chains,\n  it may be  used to indicate to Certbot which chain should be preferred.\n  * e.g. `--preferred-chain \"DST Root CA X3\"`\n\n### Changed\n\n* Allow session tickets to be disabled in Apache when mod_ssl is statically linked.\n* Generalize UI warning message on renewal rate limits\n* Certbot behaves similarly on Windows to on UNIX systems regarding umask, and\n  the umask `022` is applied by default: all files/directories are not writable by anyone\n  other than the user running Certbot and the system/admin users.\n* Read acmev1 Let's Encrypt server URL from renewal config as acmev2 URL to prepare\n  for impending acmev1 deprecation.\n\n### Fixed\n\n* Cloudflare API Tokens may now be restricted to individual zones.\n* Don't use `StrictVersion`, but `LooseVersion` to check version requirements with setuptools,\n  to fix some packaging issues with libraries respecting PEP404 for version string,\n  with doesn't match `StrictVersion` requirements.\n* Certbot output doesn't refer to SSL Labs due to confusing scoring behavior.\n* Fix paths when calling to programs outside of the Certbot Snap, fixing the apache and nginx\n  plugins on, e.g., CentOS 7.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.5.0 - 2020-06-02\n\n### Added\n\n* Require explicit confirmation of snap plugin permissions before connecting.\n\n### Changed\n\n* Improved error message in apache installer when mod_ssl is not available.\n\n### Fixed\n\n* Add support for OCSP responses which use a public key hash ResponderID, fixing\n  interoperability with Sectigo CAs.\n* Fix TLS-ALPN test that fails when run with newer versions of OpenSSL.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.4.0 - 2020-05-05\n\n### Added\n\n* Turn off session tickets for apache plugin by default when appropriate.\n* Added serial number of certificate to the output of `certbot certificates`\n* Expose two new environment variables in the authenticator and cleanup scripts used by\n  the `manual` plugin: `CERTBOT_REMAINING_CHALLENGES` is equal to the number of challenges\n  remaining after the current challenge, `CERTBOT_ALL_DOMAINS` is a comma-separated list\n  of all domains challenged for the current certificate.\n* Added TLS-ALPN-01 challenge support in the `acme` library. Support of this\n  challenge in the Certbot client is planned to be added in a future release.\n* Added minimal proxy support for OCSP verification.\n* On Windows, hooks are now executed in a Powershell shell instead of a CMD shell,\n  allowing both `*.ps1` and `*.bat` as valid scripts for Certbot.\n\n### Changed\n\n* Reorganized error message when a user entered an invalid email address.\n* Stop asking interactively if the user would like to add a redirect.\n* `mock` dependency is now conditional on Python 2 in all of our packages.\n* Deprecate certbot-auto on Gentoo, macOS, and FreeBSD.\n* Allow existing but empty archive and live dir to be used when creating new lineage.\n\n### Fixed\n\n* When using an RFC 8555 compliant endpoint, the `acme` library no longer sends the\n  `resource` field in any requests or the `type` field when responding to challenges.\n* Fix nginx plugin crash when non-ASCII configuration file is being read (instead,\n  the user will be warned that UTF-8 must be used).\n* Fix hanging OCSP queries during revocation checking - added a 10 second timeout.\n* Standalone servers now have a default socket timeout of 30 seconds, fixing\n  cases where an idle connection can cause the standalone plugin to hang.\n* Parsing of the RFC 8555 application/pem-certificate-chain now tolerates CRLF line\n  endings. This should fix interoperability with Buypass' services.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.3.0 - 2020-03-03\n\n### Added\n\n* Added certbot.ocsp Certbot's API. The certbot.ocsp module can be used to\n  determine the OCSP status of certificates.\n* Don't verify the existing certificate in HTTP01Response.simple_verify, for\n  compatibility with the real-world ACME challenge checks.\n* Added support for `$hostname` in nginx `server_name` directive\n\n### Changed\n\n* Certbot will now renew certificates early if they have been revoked according\n  to OCSP.\n* Fix acme module warnings when response Content-Type includes params (e.g. charset).\n* Fixed issue where webroot plugin would incorrectly raise `Read-only file system`\n  error when creating challenge directories (issue #7165).\n\n### Fixed\n\n* Fix Apache plugin to use less restrictive umask for making the challenge directory when a restrictive umask was set when certbot was started.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.2.0 - 2020-02-04\n\n### Added\n\n* Added support for Cloudflare's limited-scope API Tokens\n\n### Changed\n\n* Add directory field to error message when field is missing.\n* If MD5 hasher is not available, try it in non-security mode (fix for FIPS systems) -- [#1948](https://github.com/certbot/certbot/issues/1948)\n* Disable old SSL versions and ciphersuites and remove `SSLCompression off` setting to follow Mozilla recommendations in Apache.\n* Remove ECDHE-RSA-AES128-SHA from NGINX ciphers list now that Windows 2008 R2 and Windows 7 are EOLed\n* Support for Python 3.4 has been removed.\n\n### Fixed\n\n* Fix collections.abc imports for Python 3.9.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.1.0 - 2020-01-14\n\n### Added\n\n*\n\n### Changed\n\n* Removed the fallback introduced with 0.34.0 in `acme` to retry a POST-as-GET\n  request as a GET request when the targeted ACME CA server seems to not support\n  POST-as-GET requests.\n* certbot-auto no longer supports architectures other than x86_64 on RHEL 6\n  based systems. Existing certbot-auto installations affected by this will\n  continue to work, but they will no longer receive updates. To install a\n  newer version of Certbot on these systems, you should update your OS.\n* Support for Python 3.4 in Certbot and its ACME library is deprecated and will be\n  removed in the next release of Certbot. certbot-auto users on x86_64 systems running\n  RHEL 6 or derivatives will be asked to enable Software Collections (SCL) repository\n  so Python 3.6 can be installed. certbot-auto can enable the SCL repo for you on CentOS 6\n  while users on other RHEL 6 based systems will be asked to do this manually.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 1.0.0 - 2019-12-03\n\n### Added\n\n*\n\n### Removed\n\n* The `docs` extras for the `certbot-apache` and `certbot-nginx` packages\n  have been removed.\n\n### Changed\n\n* certbot-auto has deprecated support for systems using OpenSSL 1.0.1 that are\n  not running on x86-64. This primarily affects RHEL 6 based systems.\n* Certbot's `config_changes` subcommand has been removed\n* `certbot.plugins.common.TLSSNI01` has been removed.\n* Deprecated attributes related to the TLS-SNI-01 challenge in\n  `acme.challenges` and `acme.standalone`\n  have been removed.\n* The functions `certbot.client.view_config_changes`,\n  `certbot.main.config_changes`,\n  `certbot.plugins.common.Installer.view_config_changes`,\n  `certbot.reverter.Reverter.view_config_changes`, and\n  `certbot.util.get_systemd_os_info` have been removed\n* Certbot's `register --update-registration` subcommand has been removed\n* When possible, default to automatically configuring the webserver so all requests\n  redirect to secure HTTPS access. This is mostly relevant when running Certbot\n  in non-interactive mode. Previously, the default was to not redirect all requests.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.40.1 - 2019-11-05\n\n### Changed\n\n* Added back support for Python 3.4 to Certbot components and certbot-auto due\n  to a bug when requiring Python 2.7 or 3.5+ on RHEL 6 based systems.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.40.0 - 2019-11-05\n\n### Added\n\n*\n\n### Changed\n\n* We deprecated support for Python 3.4 in Certbot and its ACME library. Support\n  for Python 3.4 will be removed in the next major release of Certbot.\n  certbot-auto users on RHEL 6 based systems will be asked to enable Software\n  Collections (SCL) repository so Python 3.6 can be installed. certbot-auto can\n  enable the SCL repo for you on CentOS 6 while users on other RHEL 6 based\n  systems will be asked to do this manually.\n* `--server` may now be combined with `--dry-run`. Certbot will, as before, use the\n  staging server instead of the live server when `--dry-run` is used.\n* `--dry-run` now requests fresh authorizations every time, fixing the issue\n  where it was prone to falsely reporting success.\n* Updated certbot-dns-google to depend on newer versions of\n  google-api-python-client and oauth2client.\n* The OS detection logic again uses distro library for Linux OSes\n* certbot.plugins.common.TLSSNI01 has been deprecated and will be removed in a\n  future release.\n* CLI flags --tls-sni-01-port and --tls-sni-01-address have been removed.\n* The values tls-sni and tls-sni-01 for the --preferred-challenges flag are no\n  longer accepted.\n* Removed the flags: `--agree-dev-preview`, `--dialog`, and `--apache-init-script`\n* acme.standalone.BaseRequestHandlerWithLogging and\n  acme.standalone.simple_tls_sni_01_server have been deprecated and will be\n  removed in a future release of the library.\n* certbot-dns-rfc2136 now use TCP to query SOA records.\n\n### Fixed\n\n*\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.39.0 - 2019-10-01\n\n### Added\n\n* Support for Python 3.8 was added to Certbot and all of its components.\n* Support for CentOS 8 was added to certbot-auto.\n\n### Changed\n\n* Don't send OCSP requests for expired certificates\n* Return to using platform.linux_distribution instead of distro.linux_distribution in OS fingerprinting for Python < 3.8\n* Updated the Nginx plugin's TLS configuration to keep support for some versions of IE11.\n\n### Fixed\n\n* Fixed OS detection in the Apache plugin on RHEL 6.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.38.0 - 2019-09-03\n\n### Added\n\n* Disable session tickets for Nginx users when appropriate.\n\n### Changed\n\n* If Certbot fails to rollback your server configuration, the error message\n  links to the Let's Encrypt forum. Change the link to the Help category now\n  that the Server category has been closed.\n* Replace platform.linux_distribution with distro.linux_distribution as a step\n  towards Python 3.8 support in Certbot.\n\n### Fixed\n\n* Fixed OS detection in the Apache plugin on Scientific Linux.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.37.2 - 2019-08-21\n\n* Stop disabling TLS session tickets in Nginx as it caused TLS failures on\n  some systems.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.37.1 - 2019-08-08\n\n### Fixed\n\n* Stop disabling TLS session tickets in Apache as it caused TLS failures on\n  some systems.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.37.0 - 2019-08-07\n\n### Added\n\n* Turn off session tickets for apache plugin by default\n* acme: Authz deactivation added to `acme` module.\n\n### Changed\n\n* Follow updated Mozilla recommendations for Nginx ssl_protocols, ssl_ciphers,\n  and ssl_prefer_server_ciphers\n\n### Fixed\n\n* Fix certbot-auto failures on RHEL 8.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.36.0 - 2019-07-11\n\n### Added\n\n* Turn off session tickets for nginx plugin by default\n* Added missing error types from RFC8555 to acme\n\n### Changed\n\n* Support for Ubuntu 14.04 Trusty has been removed.\n* Update the 'manage your account' help to be more generic.\n* The error message when Certbot's Apache plugin is unable to modify your\n  Apache configuration has been improved.\n* Certbot's config_changes subcommand has been deprecated and will be\n  removed in a future release.\n* `certbot config_changes` no longer accepts a --num parameter.\n* The functions `certbot.plugins.common.Installer.view_config_changes` and\n  `certbot.reverter.Reverter.view_config_changes` have been deprecated and will\n  be removed in a future release.\n\n### Fixed\n\n* Replace some unnecessary platform-specific line separation.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.35.1 - 2019-06-10\n\n### Fixed\n\n* Support for specifying an authoritative base domain in our dns-rfc2136 plugin\n  has been removed. This feature was added in our last release but had a bug\n  which caused the plugin to fail so the feature has been removed until it can\n  be added properly.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* certbot-dns-rfc2136\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.35.0 - 2019-06-05\n\n### Added\n\n* dns_rfc2136 plugin now supports explicitly specifying an authoritative\n  base domain for cases when the automatic method does not work (e.g.\n  Split horizon DNS)\n\n### Changed\n\n*\n\n### Fixed\n\n* Renewal parameter `webroot_path` is always saved, avoiding some regressions\n  when `webroot` authenticator plugin is invoked with no challenge to perform.\n* Certbot now accepts OCSP responses when an explicit authorized\n  responder, different from the issuer, is used to sign OCSP\n  responses.\n* Scripts in Certbot hook directories are no longer executed when their\n  filenames end in a tilde.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* certbot\n* certbot-dns-rfc2136\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.34.2 - 2019-05-07\n\n### Fixed\n\n* certbot-auto no longer writes a check_permissions.py script at the root\n  of the filesystem.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\nchanges in this release were to certbot-auto.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.34.1 - 2019-05-06\n\n### Fixed\n\n* certbot-auto no longer prints a blank line when there are no permissions\n  problems.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\nchanges in this release were to certbot-auto.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.34.0 - 2019-05-01\n\n### Changed\n\n* Apache plugin now tries to restart httpd on Fedora using systemctl if a\n  configuration test error is detected. This has to be done due to the way\n  Fedora now generates the self signed certificate files upon first\n  restart.\n* Updated Certbot and its plugins to improve the handling of file system permissions\n  on Windows as a step towards adding proper Windows support to Certbot.\n* Updated urllib3 to 1.24.2 in certbot-auto.\n* Removed the fallback introduced with 0.32.0 in `acme` to retry a challenge response\n  with a `keyAuthorization` if sending the response without this field caused a\n  `malformed` error to be received from the ACME server.\n* Linode DNS plugin now supports api keys created from their new panel\n  at [cloud.linode.com](https://cloud.linode.com)\n\n### Fixed\n\n* Fixed Google DNS Challenge issues when private zones exist\n* Adding a warning noting that future versions of Certbot will automatically configure the\n  webserver so that all requests redirect to secure HTTPS access. You can control this\n  behavior and disable this warning with the --redirect and --no-redirect flags.\n* certbot-auto now prints warnings when run as root with insecure file system\n  permissions. If you see these messages, you should fix the problem by\n  following the instructions at\n  https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979/,\n  however, these warnings can be disabled as necessary with the flag\n  --no-permissions-check.\n* `acme` module uses now a POST-as-GET request to retrieve the registration\n  from an ACME v2 server\n* Convert the tsig algorithm specified in the certbot_dns_rfc2136 configuration file to\n  all uppercase letters before validating. This makes the value in the config case\n  insensitive.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-cloudflare\n* certbot-dns-cloudxns\n* certbot-dns-digitalocean\n* certbot-dns-dnsimple\n* certbot-dns-dnsmadeeasy\n* certbot-dns-gehirn\n* certbot-dns-google\n* certbot-dns-linode\n* certbot-dns-luadns\n* certbot-dns-nsone\n* certbot-dns-ovh\n* certbot-dns-rfc2136\n* certbot-dns-route53\n* certbot-dns-sakuracloud\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.33.1 - 2019-04-04\n\n### Fixed\n\n* A bug causing certbot-auto to print warnings or crash on some RHEL based\n  systems has been resolved.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\nchanges in this release were to certbot-auto.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.33.0 - 2019-04-03\n\n### Added\n\n* Fedora 29+ is now supported by certbot-auto. Since Python 2.x is on a deprecation\n  path in Fedora, certbot-auto will install and use Python 3.x on Fedora 29+.\n* CLI flag `--https-port` has been added for Nginx plugin exclusively, and replaces\n  `--tls-sni-01-port`. It defines the HTTPS port the Nginx plugin will use while\n  setting up a new SSL vhost. By default the HTTPS port is 443.\n\n### Changed\n\n* Support for TLS-SNI-01 has been removed from all official Certbot plugins.\n* Attributes related to the TLS-SNI-01 challenge in `acme.challenges` and `acme.standalone`\n  modules are deprecated and will be removed soon.\n* CLI flags `--tls-sni-01-port` and `--tls-sni-01-address` are now no-op, will\n  generate a deprecation warning if used, and will be removed soon.\n* Options `tls-sni` and `tls-sni-01` in `--preferred-challenges` flag are now no-op,\n  will generate a deprecation warning if used, and will be removed soon.\n* CLI flag `--standalone-supported-challenges` has been removed.\n\n### Fixed\n\n* Certbot uses the Python library cryptography for OCSP when cryptography>=2.5\n  is installed. We fixed a bug in Certbot causing it to interpret timestamps in\n  the OCSP response as being in the local timezone rather than UTC.\n* Issue causing the default CentOS 6 TLS configuration to ignore some of the\n  HTTPS VirtualHosts created by Certbot. mod_ssl loading is now moved to main\n  http.conf for this environment where possible.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.32.0 - 2019-03-06\n\n### Added\n\n* If possible, Certbot uses built-in support for OCSP from recent cryptography\n  versions instead of the OpenSSL binary: as a consequence Certbot does not need\n  the OpenSSL binary to be installed anymore if cryptography>=2.5 is installed.\n\n### Changed\n\n* Certbot and its acme module now depend on josepy>=1.1.0 to avoid printing the\n  warnings described at https://github.com/certbot/josepy/issues/13.\n* Apache plugin now respects CERTBOT_DOCS environment variable when adding\n  command line defaults.\n* The running of manual plugin hooks is now always included in Certbot's log\n  output.\n* Tests execution for certbot, certbot-apache and certbot-nginx packages now relies on pytest.\n* An ACME CA server may return a \"Retry-After\" HTTP header on authorization polling, as\n  specified in the ACME protocol, to indicate when the next polling should occur. Certbot now\n  reads this header if set and respect its value.\n* The `acme` module avoids sending the `keyAuthorization` field in the JWS\n  payload when responding to a challenge as the field is not included in the\n  current ACME protocol. To ease the migration path for ACME CA servers,\n  Certbot and its `acme` module will first try the request without the\n  `keyAuthorization` field but will temporarily retry the request with the\n  field included if a `malformed` error is received. This fallback will be\n  removed in version 0.34.0.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.31.0 - 2019-02-07\n\n### Added\n\n* Avoid reprocessing challenges that are already validated\n  when a certificate is issued.\n* Support for initiating (but not solving end-to-end) TLS-ALPN-01 challenges\n  with the `acme` module.\n\n### Changed\n\n* Certbot's official Docker images are now based on Alpine Linux 3.9 rather\n  than 3.7. The new version comes with OpenSSL 1.1.1.\n* Lexicon-based DNS plugins are now fully compatible with Lexicon 3.x (support\n  on 2.x branch is maintained).\n* Apache plugin now attempts to configure all VirtualHosts matching requested\n  domain name instead of only a single one when answering the HTTP-01 challenge.\n\n### Fixed\n\n* Fixed accessing josepy contents through acme.jose when the full acme.jose\n  path is used.\n* Clarify behavior for deleting certs as part of revocation.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-cloudxns\n* certbot-dns-dnsimple\n* certbot-dns-dnsmadeeasy\n* certbot-dns-gehirn\n* certbot-dns-linode\n* certbot-dns-luadns\n* certbot-dns-nsone\n* certbot-dns-ovh\n* certbot-dns-sakuracloud\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.30.2 - 2019-01-25\n\n### Fixed\n\n* Update the version of setuptools pinned in certbot-auto to 40.6.3 to\n  solve installation problems on newer OSes.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, this\nrelease only affects certbot-auto.\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.30.1 - 2019-01-24\n\n### Fixed\n\n* Always download the pinned version of pip in pipstrap to address breakages\n* Rename old,default.conf to old-and-default.conf to address commas in filenames\n  breaking recent versions of pip.\n* Add VIRTUALENV_NO_DOWNLOAD=1 to all calls to virtualenv to address breakages\n  from venv downloading the latest pip\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* certbot-apache\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.30.0 - 2019-01-02\n\n### Added\n\n* Added the `update_account` subcommand for account management commands.\n\n### Changed\n\n* Copied account management functionality from the `register` subcommand\n  to the `update_account` subcommand.\n* Marked usage `register --update-registration` for deprecation and\n  removal in a future release.\n\n### Fixed\n\n* Older modules in the josepy library can now be accessed through acme.jose\n  like it could in previous versions of acme. This is only done to preserve\n  backwards compatibility and support for doing this with new modules in josepy\n  will not be added. Users of the acme library should switch to using josepy\n  directly if they haven't done so already.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.29.1 - 2018-12-05\n\n### Added\n\n*\n\n### Changed\n\n*\n\n### Fixed\n\n* The default work and log directories have been changed back to\n  /var/lib/letsencrypt and /var/log/letsencrypt respectively.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* certbot\n\nMore details about these changes can be found on our GitHub repo.\n\n## 0.29.0 - 2018-12-05\n\n### Added\n\n* Noninteractive renewals with `certbot renew` (those not started from a\n  terminal) now randomly sleep 1-480 seconds before beginning work in\n  order to spread out load spikes on the server side.\n* Added External Account Binding support in cli and acme library.\n  Command line arguments --eab-kid and --eab-hmac-key added.\n\n### Changed\n\n* Private key permissioning changes: Renewal preserves existing group mode\n  & gid of previous private key material. Private keys for new\n  lineages (i.e. new certs, not renewed) default to 0o600.\n\n### Fixed\n\n* Update code and dependencies to clean up Resource and Deprecation Warnings.\n* Only depend on imgconverter extension for Sphinx >= 1.6\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-cloudflare\n* certbot-dns-digitalocean\n* certbot-dns-google\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/62?closed=1\n\n## 0.28.0 - 2018-11-7\n\n### Added\n\n* `revoke` accepts `--cert-name`, and doesn't accept both `--cert-name` and `--cert-path`.\n* Use the ACMEv2 newNonce endpoint when a new nonce is needed, and newNonce is available in the directory.\n\n### Changed\n\n* Removed documentation mentions of `#letsencrypt` IRC on Freenode.\n* Write README to the base of (config-dir)/live directory\n* `--manual` will explicitly warn users that earlier challenges should remain in place when setting up subsequent challenges.\n* Warn when using deprecated acme.challenges.TLSSNI01\n* Log warning about TLS-SNI deprecation in Certbot\n* Stop preferring TLS-SNI in the Apache, Nginx, and standalone plugins\n* OVH DNS plugin now relies on Lexicon>=2.7.14 to support HTTP proxies\n* Default time the Linode plugin waits for DNS changes to propagate is now 1200 seconds.\n\n### Fixed\n\n* Match Nginx parser update in allowing variable names to start with `${`.\n* Fix ranking of vhosts in Nginx so that all port-matching vhosts come first\n* Correct OVH integration tests on machines without internet access.\n* Stop caching the results of ipv6_info in http01.py\n* Test fix for Route53 plugin to prevent boto3 making outgoing connections.\n* The grammar used by Augeas parser in Apache plugin was updated to fix various parsing errors.\n* The CloudXNS, DNSimple, DNS Made Easy, Gehirn, Linode, LuaDNS, NS1, OVH, and\n  Sakura Cloud DNS plugins are now compatible with Lexicon 3.0+.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-cloudxns\n* certbot-dns-dnsimple\n* certbot-dns-dnsmadeeasy\n* certbot-dns-gehirn\n* certbot-dns-linode\n* certbot-dns-luadns\n* certbot-dns-nsone\n* certbot-dns-ovh\n* certbot-dns-route53\n* certbot-dns-sakuracloud\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/59?closed=1\n\n## 0.27.1 - 2018-09-06\n\n### Fixed\n\n* Fixed parameter name in OpenSUSE overrides for default parameters in the\n  Apache plugin. Certbot on OpenSUSE works again.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* certbot-apache\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/60?closed=1\n\n## 0.27.0 - 2018-09-05\n\n### Added\n\n* The Apache plugin now accepts the parameter --apache-ctl which can be\n  used to configure the path to the Apache control script.\n\n### Changed\n\n* When using `acme.client.ClientV2` (or\n `acme.client.BackwardsCompatibleClientV2` with an ACME server that supports a\n newer version of the ACME protocol), an `acme.errors.ConflictError` will be\n raised if you try to create an ACME account with a key that has already been\n used. Previously, a JSON parsing error was raised in this scenario when using\n the library with Let's Encrypt's ACMEv2 endpoint.\n\n### Fixed\n\n* When Apache is not installed, Certbot's Apache plugin no longer prints\n  messages about being unable to find apachectl to the terminal when the plugin\n  is not selected.\n* If you're using the Apache plugin with the --apache-vhost-root flag set to a\n  directory containing a disabled virtual host for the domain you're requesting\n  a certificate for, the virtual host will now be temporarily enabled if\n  necessary to pass the HTTP challenge.\n* The documentation for the Certbot package can now be built using Sphinx 1.6+.\n* You can now call `query_registration` without having to first call\n  `new_account` on `acme.client.ClientV2` objects.\n* The requirement of `setuptools>=1.0` has been removed from `certbot-dns-ovh`.\n* Names in certbot-dns-sakuracloud's tests have been updated to refer to Sakura\n  Cloud rather than NS1 whose plugin certbot-dns-sakuracloud was based on.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackage with changes other than its version number was:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-ovh\n* certbot-dns-sakuracloud\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/57?closed=1\n\n## 0.26.1 - 2018-07-17\n\n### Fixed\n\n* Fix a bug that was triggered when users who had previously manually set `--server` to get ACMEv2 certs tried to renew ACMEv1 certs.\n\nDespite us having broken lockstep, we are continuing to release new versions of all Certbot components during releases for the time being, however, the only package with changes other than its version number was:\n\n* certbot\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/58?closed=1\n\n## 0.26.0 - 2018-07-11\n\n### Added\n\n* A new security enhancement which we're calling AutoHSTS has been added to\n  Certbot's Apache plugin. This enhancement configures your webserver to send a\n  HTTP Strict Transport Security header with a low max-age value that is slowly\n  increased over time. The max-age value is not increased to a large value\n  until you've successfully managed to renew your certificate. This enhancement\n  can be requested with the --auto-hsts flag.\n* New official DNS plugins have been created for Gehirn Infrastructure Service,\n  Linode, OVH, and Sakura Cloud. These plugins can be found on our Docker Hub\n  page at https://hub.docker.com/u/certbot and on PyPI.\n* The ability to reuse ACME accounts from Let's Encrypt's ACMEv1 endpoint on\n  Let's Encrypt's ACMEv2 endpoint has been added.\n* Certbot and its components now support Python 3.7.\n* Certbot's install subcommand now allows you to interactively choose which\n  certificate to install from the list of certificates managed by Certbot.\n* Certbot now accepts the flag `--no-autorenew` which causes any obtained\n  certificates to not be automatically renewed when it approaches expiration.\n* Support for parsing the TLS-ALPN-01 challenge has been added back to the acme\n  library.\n\n### Changed\n\n* Certbot's default ACME server has been changed to Let's Encrypt's ACMEv2\n  endpoint. By default, this server will now be used for both new certificate\n  lineages and renewals.\n* The Nginx plugin is no longer marked labeled as an \"Alpha\" version.\n* The `prepare` method of Certbot's plugins is no longer called before running\n  \"Updater\" enhancements that are run on every invocation of `certbot renew`.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with functional changes were:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-gehirn\n* certbot-dns-linode\n* certbot-dns-ovh\n* certbot-dns-sakuracloud\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/55?closed=1\n\n## 0.25.1 - 2018-06-13\n\n### Fixed\n\n* TLS-ALPN-01 support has been removed from our acme library. Using our current\n  dependencies, we are unable to provide a correct implementation of this\n  challenge so we decided to remove it from the library until we can provide\n  proper support.\n* Issues causing test failures when running the tests in the acme package with\n  pytest<3.0 has been resolved.\n* certbot-nginx now correctly depends on acme>=0.25.0.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with changes other than their version number were:\n\n* acme\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/56?closed=1\n\n## 0.25.0 - 2018-06-06\n\n### Added\n\n* Support for the ready status type was added to acme. Without this change,\n  Certbot and acme users will begin encountering errors when using Let's\n  Encrypt's ACMEv2 API starting on June 19th for the staging environment and\n  July 5th for production. See\n  https://community.letsencrypt.org/t/acmev2-order-ready-status/62866 for more\n  information.\n* Certbot now accepts the flag --reuse-key which will cause the same key to be\n  used in the certificate when the lineage is renewed rather than generating a\n  new key.\n* You can now add multiple email addresses to your ACME account with Certbot by\n  providing a comma separated list of emails to the --email flag.\n* Support for Let's Encrypt's upcoming TLS-ALPN-01 challenge was added to acme.\n  For more information, see\n  https://community.letsencrypt.org/t/tls-alpn-validation-method/63814/1.\n* acme now supports specifying the source address to bind to when sending\n  outgoing connections. You still cannot specify this address using Certbot.\n* If you run Certbot against Let's Encrypt's ACMEv2 staging server but don't\n  already have an account registered at that server URL, Certbot will\n  automatically reuse your staging account from Let's Encrypt's ACMEv1 endpoint\n  if it exists.\n* Interfaces were added to Certbot allowing plugins to be called at additional\n  points. The `GenericUpdater` interface allows plugins to perform actions\n  every time `certbot renew` is run, regardless of whether any certificates are\n  due for renewal, and the `RenewDeployer` interface allows plugins to perform\n  actions when a certificate is renewed. See `certbot.interfaces` for more\n  information.\n\n### Changed\n\n* When running Certbot with --dry-run and you don't already have a staging\n  account, the created account does not contain an email address even if one\n  was provided to avoid expiration emails from Let's Encrypt's staging server.\n* certbot-nginx does a better job of automatically detecting the location of\n  Nginx's configuration files when run on BSD based systems.\n* acme now requires and uses pytest when running tests with setuptools with\n  `python setup.py test`.\n* `certbot config_changes` no longer waits for user input before exiting.\n\n### Fixed\n\n* Misleading log output that caused users to think that Certbot's standalone\n  plugin failed to bind to a port when performing a challenge has been\n  corrected.\n* An issue where certbot-nginx would fail to enable HSTS if the server block\n  already had an `add_header` directive has been resolved.\n* certbot-nginx now does a better job detecting the server block to base the\n  configuration for TLS-SNI challenges on.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with functional changes were:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/54?closed=1\n\n## 0.24.0 - 2018-05-02\n\n### Added\n\n* certbot now has an enhance subcommand which allows you to configure security\n  enhancements like HTTP to HTTPS redirects, OCSP stapling, and HSTS without\n  reinstalling a certificate.\n* certbot-dns-rfc2136 now allows the user to specify the port to use to reach\n  the DNS server in its credentials file.\n* acme now parses the wildcard field included in authorizations so it can be\n  used by users of the library.\n\n### Changed\n\n* certbot-dns-route53 used to wait for each DNS update to propagate before\n  sending the next one, but now it sends all updates before waiting which\n  speeds up issuance for multiple domains dramatically.\n* Certbot's official Docker images are now based on Alpine Linux 3.7 rather\n  than 3.4 because 3.4 has reached its end-of-life.\n* We've doubled the time Certbot will spend polling authorizations before\n  timing out.\n* The level of the message logged when Certbot is being used with\n  non-standard paths warning that crontabs for renewal included in Certbot\n  packages from OS package managers may not work has been reduced. This stops\n  the message from being written to stderr every time `certbot renew` runs.\n\n### Fixed\n\n* certbot-auto now works with Python 3.6.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with changes other than their version number were:\n\n* acme\n* certbot\n* certbot-apache\n* certbot-dns-digitalocean (only style improvements to tests)\n* certbot-dns-rfc2136\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/52?closed=1\n\n## 0.23.0 - 2018-04-04\n\n### Added\n\n* Support for OpenResty was added to the Nginx plugin.\n\n### Changed\n\n* The timestamps in Certbot's logfiles now use the system's local time zone\n  rather than UTC.\n* Certbot's DNS plugins that use Lexicon now rely on Lexicon>=2.2.1 to be able\n  to create and delete multiple TXT records on a single domain.\n* certbot-dns-google's test suite now works without an internet connection.\n\n### Fixed\n\n* Removed a small window that if during which an error occurred, Certbot\n  wouldn't clean up performed challenges.\n* The parameters `default` and `ipv6only` are now removed from `listen`\n  directives when creating a new server block in the Nginx plugin.\n* `server_name` directives enclosed in quotation marks in Nginx are now properly\n  supported.\n* Resolved an issue preventing the Apache plugin from starting Apache when it's\n  not currently running on RHEL and Gentoo based systems.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with changes other than their version number were:\n\n* certbot\n* certbot-apache\n* certbot-dns-cloudxns\n* certbot-dns-dnsimple\n* certbot-dns-dnsmadeeasy\n* certbot-dns-google\n* certbot-dns-luadns\n* certbot-dns-nsone\n* certbot-dns-rfc2136\n* certbot-nginx\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/50?closed=1\n\n## 0.22.2 - 2018-03-19\n\n### Fixed\n\n* A type error introduced in 0.22.1 that would occur during challenge cleanup\n  when a Certbot plugin raises an exception while trying to complete the\n  challenge was fixed.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with changes other than their version number were:\n\n* certbot\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/53?closed=1\n\n## 0.22.1 - 2018-03-19\n\n### Changed\n\n* The ACME server used with Certbot's --dry-run and --staging flags is now\n  Let's Encrypt's ACMEv2 staging server which allows people to also test ACMEv2\n  features with these flags.\n\n### Fixed\n\n* The HTTP Content-Type header is now set to the correct value during\n  certificate revocation with new versions of the ACME protocol.\n* When using Certbot with Let's Encrypt's ACMEv2 server, it would add a blank\n  line to the top of chain.pem and between the certificates in fullchain.pem\n  for each lineage. These blank lines have been removed.\n* Resolved a bug that caused Certbot's --allow-subset-of-names flag not to\n  work.\n* Fixed a regression in acme.client.Client that caused the class to not work\n  when it was initialized without a ClientNetwork which is done by some of the\n  other projects using our ACME library.\n\nDespite us having broken lockstep, we are continuing to release new versions of\nall Certbot components during releases for the time being, however, the only\npackages with changes other than their version number were:\n\n* acme\n* certbot\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/51?closed=1\n\n## 0.22.0 - 2018-03-07\n\n### Added\n\n* Support for obtaining wildcard certificates and a newer version of the ACME\n  protocol such as the one implemented by Let's Encrypt's upcoming ACMEv2\n  endpoint was added to Certbot and its ACME library. Certbot still works with\n  older ACME versions and will automatically change the version of the protocol\n  used based on the version the ACME CA implements.\n* The Apache and Nginx plugins are now able to automatically install a wildcard\n  certificate to multiple virtual hosts that you select from your server\n  configuration.\n* The `certbot install` command now accepts the `--cert-name` flag for\n  selecting a certificate.\n* `acme.client.BackwardsCompatibleClientV2` was added to Certbot's ACME library\n  which automatically handles most of the differences between new and old ACME\n  versions. `acme.client.ClientV2` is also available for people who only want\n  to support one version of the protocol or want to handle the differences\n  between versions themselves.\n* certbot-auto now supports the flag --install-only which has the script\n  install Certbot and its dependencies and exit without invoking Certbot.\n* Support for issuing a single certificate for a wildcard and base domain was\n  added to our Google Cloud DNS plugin. To do this, we now require your API\n  credentials have additional permissions, however, your credentials will\n  already have these permissions unless you defined a custom role with fewer\n  permissions than the standard DNS administrator role provided by Google.\n  These permissions are also only needed for the case described above so it\n  will continue to work for existing users. For more information about the\n  permissions changes, see the documentation in the plugin.\n\n### Changed\n\n* We have broken lockstep between our ACME library, Certbot, and its plugins.\n  This means that the different components do not need to be the same version\n  to work together like they did previously. This makes packaging easier\n  because not every piece of Certbot needs to be repackaged to ship a change to\n  a subset of its components.\n* Support for Python 2.6 and Python 3.3 has been removed from ACME, Certbot,\n  Certbot's plugins, and certbot-auto. If you are using certbot-auto on a RHEL\n  6 based system, it will walk you through the process of installing Certbot\n  with Python 3 and refuse to upgrade to a newer version of Certbot until you\n  have done so.\n* Certbot's components now work with older versions of setuptools to simplify\n  packaging for EPEL 7.\n\n### Fixed\n\n* Issues caused by Certbot's Nginx plugin adding multiple ipv6only directives\n  has been resolved.\n* A problem where Certbot's Apache plugin would add redundant include\n  directives for the TLS configuration managed by Certbot has been fixed.\n* Certbot's webroot plugin now properly deletes any directories it creates.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/48?closed=1\n\n## 0.21.1 - 2018-01-25\n\n### Fixed\n\n* When creating an HTTP to HTTPS redirect in Nginx, we now ensure the Host\n  header of the request is set to an expected value before redirecting users to\n  the domain found in the header. The previous way Certbot configured Nginx\n  redirects was a potential security issue which you can read more about at\n  https://community.letsencrypt.org/t/security-issue-with-redirects-added-by-certbots-nginx-plugin/51493.\n* Fixed a problem where Certbot's Apache plugin could fail HTTP-01 challenges\n  if basic authentication is configured for the domain you request a\n  certificate for.\n* certbot-auto --no-bootstrap now properly tries to use Python 3.4 on RHEL 6\n  based systems rather than Python 2.6.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/49?closed=1\n\n## 0.21.0 - 2018-01-17\n\n### Added\n\n* Support for the HTTP-01 challenge type was added to our Apache and Nginx\n  plugins. For those not aware, Let's Encrypt disabled the TLS-SNI-01 challenge\n  type which was what was previously being used by our Apache and Nginx plugins\n  last week due to a security issue. For more information about Let's Encrypt's\n  change, click\n  [here](https://community.letsencrypt.org/t/2018-01-11-update-regarding-acme-tls-sni-and-shared-hosting-infrastructure/50188).\n  Our Apache and Nginx plugins will automatically switch to use HTTP-01 so no\n  changes need to be made to your Certbot configuration, however, you should\n  make sure your server is accessible on port 80 and isn't behind an external\n  proxy doing things like redirecting all traffic from HTTP to HTTPS. HTTP to\n  HTTPS redirects inside Apache and Nginx are fine.\n* IPv6 support was added to the Nginx plugin.\n* Support for automatically creating server blocks based on the default server\n  block was added to the Nginx plugin.\n* The flags --delete-after-revoke and --no-delete-after-revoke were added\n  allowing users to control whether the revoke subcommand also deletes the\n  certificates it is revoking.\n\n### Changed\n\n* We deprecated support for Python 2.6 and Python 3.3 in Certbot and its ACME\n  library. Support for these versions of Python will be removed in the next\n  major release of Certbot. If you are using certbot-auto on a RHEL 6 based\n  system, it will guide you through the process of installing Python 3.\n* We split our implementation of JOSE (Javascript Object Signing and\n  Encryption) out of our ACME library and into a separate package named josepy.\n  This package is available on [PyPI](https://pypi.python.org/pypi/josepy) and\n  on [GitHub](https://github.com/certbot/josepy).\n* We updated the ciphersuites used in Apache to the new [values recommended by\n  Mozilla](https://wiki.mozilla.org/Security/Server_Side_TLS#Intermediate_compatibility_.28default.29).\n  The major change here is adding ChaCha20 to the list of supported\n  ciphersuites.\n\n### Fixed\n\n* An issue with our Apache plugin on Gentoo due to differences in their\n  apache2ctl command have been resolved.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/47?closed=1\n\n## 0.20.0 - 2017-12-06\n\n### Added\n\n* Certbot's ACME library now recognizes URL fields in challenge objects in\n  preparation for Let's Encrypt's new ACME endpoint. The value is still\n  accessible in our ACME library through the name \"uri\".\n\n### Changed\n\n* The Apache plugin now parses some distro specific Apache configuration files\n  on non-Debian systems allowing it to get a clearer picture on the running\n  configuration. Internally, these changes were structured so that external\n  contributors can easily write patches to make the plugin work in new Apache\n  configurations.\n* Certbot better reports network failures by removing information about\n  connection retries from the error output.\n* An unnecessary question when using Certbot's webroot plugin interactively has\n  been removed.\n\n### Fixed\n\n* Certbot's NGINX plugin no longer sometimes incorrectly reports that it was\n  unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a\n  redirect for multiple domains.\n* Problems where the Apache plugin was failing to find directives and\n  duplicating existing directives on openSUSE have been resolved.\n* An issue running the test shipped with Certbot and some our DNS plugins with\n  older versions of mock have been resolved.\n* On some systems, users reported strangely interleaved output depending on\n  when stdout and stderr were flushed. This problem was resolved by having\n  Certbot regularly flush these streams.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/44?closed=1\n\n## 0.19.0 - 2017-10-04\n\n### Added\n\n* Certbot now has renewal hook directories where executable files can be placed\n  for Certbot to run with the renew subcommand. Pre-hooks, deploy-hooks, and\n  post-hooks can be specified in the renewal-hooks/pre, renewal-hooks/deploy,\n  and renewal-hooks/post directories respectively in Certbot's configuration\n  directory (which is /etc/letsencrypt by default). Certbot will automatically\n  create these directories when it is run if they do not already exist.\n* After revoking a certificate with the revoke subcommand, Certbot will offer\n  to delete the lineage associated with the certificate. When Certbot is run\n  with --non-interactive, it will automatically try to delete the associated\n  lineage.\n* When using Certbot's Google Cloud DNS plugin on Google Compute Engine, you no\n  longer have to provide a credential file to Certbot if you have configured\n  sufficient permissions for the instance which Certbot can automatically\n  obtain using Google's metadata service.\n\n### Changed\n\n* When deleting certificates interactively using the delete subcommand, Certbot\n  will now allow you to select multiple lineages to be deleted at once.\n* Certbot's Apache plugin no longer always parses Apache's sites-available on\n  Debian based systems and instead only parses virtual hosts included in your\n  Apache configuration. You can provide an additional directory for Certbot to\n  parse using the command line flag --apache-vhost-root.\n\n### Fixed\n\n* The plugins subcommand can now be run without root access.\n* certbot-auto now includes a timeout when updating itself so it no longer\n  hangs indefinitely when it is unable to connect to the external server.\n* An issue where Certbot's Apache plugin would sometimes fail to deploy a\n  certificate on Debian based systems if mod_ssl wasn't already enabled has\n  been resolved.\n* A bug in our Docker image where the certificates subcommand could not report\n  if certificates maintained by Certbot had been revoked has been fixed.\n* Certbot's RFC 2136 DNS plugin (for use with software like BIND) now properly\n  performs DNS challenges when the domain being verified contains a CNAME\n  record.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/43?closed=1\n\n## 0.18.2 - 2017-09-20\n\n### Fixed\n\n* An issue where Certbot's ACME module would raise an AttributeError trying to\n  create self-signed certificates when used with pyOpenSSL 17.3.0 has been\n  resolved. For Certbot users with this version of pyOpenSSL, this caused\n  Certbot to crash when performing a TLS SNI challenge or when the Nginx plugin\n  tried to create an SSL server block.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/46?closed=1\n\n## 0.18.1 - 2017-09-08\n\n### Fixed\n\n* If certbot-auto was running as an unprivileged user and it upgraded from\n  0.17.0 to 0.18.0, it would crash with a permissions error and would need to\n  be run again to successfully complete the upgrade. This has been fixed and\n  certbot-auto should upgrade cleanly to 0.18.1.\n* Certbot usually uses \"certbot-auto\" or \"letsencrypt-auto\" in error messages\n  and the User-Agent string instead of \"certbot\" when you are using one of\n  these wrapper scripts. Proper detection of this was broken with Certbot's new\n  installation path in /opt in 0.18.0 but this problem has been resolved.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/45?closed=1\n\n## 0.18.0 - 2017-09-06\n\n### Added\n\n* The Nginx plugin now configures Nginx to use 2048-bit Diffie-Hellman\n  parameters. Java 6 clients do not support Diffie-Hellman parameters larger\n  than 1024 bits, so if you need to support these clients you will need to\n  manually modify your Nginx configuration after using the Nginx installer.\n\n### Changed\n\n* certbot-auto now installs Certbot in directories under `/opt/eff.org`. If you\n  had an existing installation from certbot-auto, a symlink is created to the\n  new directory. You can configure certbot-auto to use a different path by\n  setting the environment variable VENV_PATH.\n* The Nginx plugin can now be selected in Certbot's interactive output.\n* Output verbosity of renewal failures when running with `--quiet` has been\n  reduced.\n* The default revocation reason shown in Certbot help output now is a human\n  readable string instead of a numerical code.\n* Plugin selection is now included in normal terminal output.\n\n### Fixed\n\n* A newer version of ConfigArgParse is now installed when using certbot-auto\n  causing values set to false in a Certbot INI configuration file to be handled\n  intuitively. Setting a boolean command line flag to false is equivalent to\n  not including it in the configuration file at all.\n* New naming conventions preventing certbot-auto from installing OS\n  dependencies on Fedora 26 have been resolved.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/42?closed=1\n\n## 0.17.0 - 2017-08-02\n\n### Added\n\n* Support in our nginx plugin for modifying SSL server blocks that do\n  not contain certificate or key directives.\n* A `--max-log-backups` flag to allow users to configure or even completely\n  disable Certbot's built in log rotation.\n* A `--user-agent-comment` flag to allow people who build tools around Certbot\n  to differentiate their user agent string by adding a comment to its default\n  value.\n\n### Changed\n\n* Due to some awesome work by\n  [cryptography project](https://github.com/pyca/cryptography), compilation can\n  now be avoided on most systems when using certbot-auto. This eliminates many\n  problems people have had in the past such as running out of memory, having\n  invalid headers/libraries, and changes to the OS packages on their system\n  after compilation breaking Certbot.\n* The `--renew-hook` flag has been hidden in favor of `--deploy-hook`. This new\n  flag works exactly the same way except it is always run when a certificate is\n  issued rather than just when it is renewed.\n* We have started printing deprecation warnings in certbot-auto for\n  experimentally supported systems with OS packages available.\n* A certificate lineage's name is included in error messages during renewal.\n\n### Fixed\n\n* Encoding errors that could occur when parsing error messages from the ACME\n  server containing Unicode have been resolved.\n* certbot-auto no longer prints misleading messages about there being a newer\n  pip version available when installation fails.\n* Certbot's ACME library now properly extracts domains from critical SAN\n  extensions.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.17.0+is%3Aclosed\n\n## 0.16.0 - 2017-07-05\n\n### Added\n\n* A plugin for performing DNS challenges using dynamic DNS updates as defined\n  in RFC 2316. This plugin is packaged separately from Certbot and is available\n  at https://pypi.python.org/pypi/certbot-dns-rfc2136. It supports Python 2.6,\n  2.7, and 3.3+. At this time, there isn't a good way to install this plugin\n  when using certbot-auto, but this should change in the near future.\n* Plugins for performing DNS challenges for the providers\n  [DNS Made Easy](https://pypi.python.org/pypi/certbot-dns-dnsmadeeasy) and\n  [LuaDNS](https://pypi.python.org/pypi/certbot-dns-luadns). These plugins are\n  packaged separately from Certbot and support Python 2.7 and 3.3+. Currently,\n  there isn't a good way to install these plugins when using certbot-auto,\n  but that should change soon.\n* Support for performing TLS-SNI-01 challenges when using the manual plugin.\n* Automatic detection of Arch Linux in the Apache plugin providing better\n  default settings for the plugin.\n\n### Changed\n\n* The text of the interactive question about whether a redirect from HTTP to\n  HTTPS should be added by Certbot has been rewritten to better explain the\n  choices to the user.\n* Simplified HTTP challenge instructions in the manual plugin.\n\n### Fixed\n\n* Problems performing a dry run when using the Nginx plugin have been fixed.\n* Resolved an issue where certbot-dns-digitalocean's test suite would sometimes\n  fail when ran using Python 3.\n* On some systems, previous versions of certbot-auto would error out with a\n  message about a missing hash for setuptools. This has been fixed.\n* A bug where Certbot would sometimes not print a space at the end of an\n  interactive prompt has been resolved.\n* Nonfatal tracebacks are no longer shown in rare cases where Certbot\n  encounters an exception trying to close its TCP connection with the ACME\n  server.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.16.0+is%3Aclosed\n\n## 0.15.0 - 2017-06-08\n\n### Added\n\n* Plugins for performing DNS challenges for popular providers. Like the Apache\n  and Nginx plugins, these plugins are packaged separately and not included in\n  Certbot by default. So far, we have plugins for\n  [Amazon Route 53](https://pypi.python.org/pypi/certbot-dns-route53),\n  [Cloudflare](https://pypi.python.org/pypi/certbot-dns-cloudflare),\n  [DigitalOcean](https://pypi.python.org/pypi/certbot-dns-digitalocean), and\n  [Google Cloud](https://pypi.python.org/pypi/certbot-dns-google) which all\n  work on Python 2.6, 2.7, and 3.3+. Additionally, we have plugins for\n  [CloudXNS](https://pypi.python.org/pypi/certbot-dns-cloudxns),\n  [DNSimple](https://pypi.python.org/pypi/certbot-dns-dnsimple),\n  [NS1](https://pypi.python.org/pypi/certbot-dns-nsone) which work on Python\n  2.7 and 3.3+ (and not 2.6). Currently, there isn't a good way to install\n  these plugins when using `certbot-auto`, but that should change soon.\n* IPv6 support in the standalone plugin. When performing a challenge, the\n  standalone plugin automatically handles listening for IPv4/IPv6 traffic based\n  on the configuration of your system.\n* A mechanism for keeping your Apache and Nginx SSL/TLS configuration up to\n  date. When the Apache or Nginx plugins are used, they place SSL/TLS\n  configuration options in the root of Certbot's config directory\n  (`/etc/letsencrypt` by default). Now when a new version of these plugins run\n  on your system, they will automatically update the file to the newest\n  version if it is unmodified. If you manually modified the file, Certbot will\n  display a warning giving you a path to the updated file which you can use as\n  a reference to manually update your modified copy.\n* `--http-01-address` and `--tls-sni-01-address` flags for controlling the\n  address Certbot listens on when using the standalone plugin.\n* The command `certbot certificates` that lists certificates managed by Certbot\n  now performs additional validity checks to notify you if your files have\n  become corrupted.\n\n### Changed\n\n* Messages custom hooks print to `stdout` are now displayed by Certbot when not\n  running in `--quiet` mode.\n* `jwk` and `alg` fields in JWS objects have been moved into the protected\n  header causing Certbot to more closely follow the latest version of the ACME\n  spec.\n\n### Fixed\n\n* Permissions on renewal configuration files are now properly preserved when\n  they are updated.\n* A bug causing Certbot to display strange defaults in its help output when\n  using Python <= 2.7.4 has been fixed.\n* Certbot now properly handles mixed case domain names found in custom CSRs.\n* A number of poorly worded prompts and error messages.\n\n### Removed\n\n* Support for OpenSSL 1.0.0 in `certbot-auto` has been removed as we now pin a\n  newer version of `cryptography` which dropped support for this version.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.15.0+is%3Aclosed\n\n## 0.14.2 - 2017-05-25\n\n### Fixed\n\n* Certbot 0.14.0 included a bug where Certbot would create a temporary log file\n(usually in /tmp) if the program exited during argument parsing. If a user\nprovided -h/--help/help, --version, or an invalid command line argument,\nCertbot would create this temporary log file. This was especially bothersome to\ncertbot-auto users as certbot-auto runs `certbot --version` internally to see\nif the script needs to upgrade causing it to create at least one of these files\non every run. This problem has been resolved.\n\nMore details about this change can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.2+is%3Aclosed\n\n## 0.14.1 - 2017-05-16\n\n### Fixed\n\n* Certbot now works with configargparse 0.12.0.\n* Issues with the Apache plugin and Augeas 1.7+ have been resolved.\n* A problem where the Nginx plugin would fail to install certificates on\nsystems that had the plugin's SSL/TLS options file from 7+ months ago has been\nfixed.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.1+is%3Aclosed\n\n## 0.14.0 - 2017-05-04\n\n### Added\n\n* Python 3.3+ support for all Certbot packages. `certbot-auto` still currently\nonly supports Python 2, but the `acme`, `certbot`, `certbot-apache`, and\n`certbot-nginx` packages on PyPI now fully support Python 2.6, 2.7, and 3.3+.\n* Certbot's Apache plugin now handles multiple virtual hosts per file.\n* Lockfiles to prevent multiple versions of Certbot running simultaneously.\n\n### Changed\n\n* When converting an HTTP virtual host to HTTPS in Apache, Certbot only copies\nthe virtual host rather than the entire contents of the file it's contained\nin.\n* The Nginx plugin now includes SSL/TLS directives in a separate file located\nin Certbot's configuration directory rather than copying the contents of the\nfile into every modified `server` block.\n\n### Fixed\n\n* Ensure logging is configured before parts of Certbot attempt to log any\nmessages.\n* Support for the `--quiet` flag in `certbot-auto`.\n* Reverted a change made in a previous release to make the `acme` and `certbot`\npackages always depend on `argparse`. This dependency is conditional again on\nthe user's Python version.\n* Small bugs in the Nginx plugin such as properly handling empty `server`\nblocks and setting `server_names_hash_bucket_size` during challenges.\n\nAs always, a more complete list of changes can be found on GitHub:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.14.0+is%3Aclosed\n\n## 0.13.0 - 2017-04-06\n\n### Added\n\n* `--debug-challenges` now pauses Certbot after setting up challenges for debugging.\n* The Nginx parser can now handle all valid directives in configuration files.\n* Nginx ciphersuites have changed to Mozilla Intermediate.\n* `certbot-auto --no-bootstrap` provides the option to not install OS dependencies.\n\n### Fixed\n\n* `--register-unsafely-without-email` now respects `--quiet`.\n* Hyphenated renewal parameters are now saved in renewal config files.\n* `--dry-run` no longer persists keys and csrs.\n* Certbot no longer hangs when trying to start Nginx in Arch Linux.\n* Apache rewrite rules no longer double-encode characters.\n\nA full list of changes is available on GitHub:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.13.0%20is%3Aclosed%20\n\n## 0.12.0 - 2017-03-02\n\n### Added\n\n* Certbot now allows non-camelcase Apache VirtualHost names.\n* Certbot now allows more log messages to be silenced.\n\n### Fixed\n\n* Fixed a regression around using `--cert-name` when getting new certificates\n\nMore information about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.12.0\n\n## 0.11.1 - 2017-02-01\n\n### Fixed\n\n* Resolved a problem where Certbot would crash while parsing command line\narguments in some cases.\n* Fixed a typo.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/pulls?q=is%3Apr%20milestone%3A0.11.1%20is%3Aclosed\n\n## 0.11.0 - 2017-02-01\n\n### Added\n\n* When using the standalone plugin while running Certbot interactively\nand a required port is bound by another process, Certbot will give you\nthe option to retry to grab the port rather than immediately exiting.\n* You are now able to deactivate your account with the Let's Encrypt\nserver using the `unregister` subcommand.\n* When revoking a certificate using the `revoke` subcommand, you now\nhave the option to provide the reason the certificate is being revoked\nto Let's Encrypt with `--reason`.\n\n### Changed\n\n* Providing `--quiet` to `certbot-auto` now silences package manager output.\n\n### Removed\n\n* Removed the optional `dnspython` dependency in our `acme` package.\nNow the library does not support client side verification of the DNS\nchallenge.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.11.0+is%3Aclosed\n\n## 0.10.2 - 2017-01-25\n\n### Added\n\n* If Certbot receives a request with a `badNonce` error, it now\nautomatically retries the request. Since nonces from Let's Encrypt expire,\nthis helps people performing the DNS challenge with the `manual` plugin\nwho may have to wait an extended period of time for their DNS changes to\npropagate.\n\n### Fixed\n\n* Certbot now saves the `--preferred-challenges` values for renewal. Previously\nthese values were discarded causing a different challenge type to be used when\nrenewing certs in some cases.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.2+is%3Aclosed\n\n## 0.10.1 - 2017-01-13\n\n### Fixed\n\n* Resolve problems where when asking Certbot to update a certificate at\nan existing path to include different domain names, the old names would\ncontinue to be used.\n* Fix issues successfully running our unit test suite on some systems.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.1+is%3Aclosed\n\n## 0.10.0 - 2017-01-11\n\n## Added\n\n* Added the ability to customize and automatically complete DNS and HTTP\ndomain validation challenges with the manual plugin. The flags\n`--manual-auth-hook` and `--manual-cleanup-hook` can now be provided\nwhen using the manual plugin to execute commands provided by the user to\nperform and clean up challenges provided by the CA. This is best used in\ncomplicated setups where the DNS challenge must be used or Certbot's\nexisting plugins cannot be used to perform HTTP challenges. For more\ninformation on how this works, see `certbot --help manual`.\n* Added a `--cert-name` flag for specifying the name to use for the\ncertificate in Certbot's configuration directory. Using this flag in\ncombination with `-d/--domains`, a user can easily request a new\ncertificate with different domains and save it with the name provided by\n`--cert-name`. Additionally, `--cert-name` can be used to select a\ncertificate with the `certonly` and `run` subcommands so a full list of\ndomains in the certificate does not have to be provided.\n* Added subcommand `certificates` for listing the certificates managed by\nCertbot and their properties.\n* Added the `delete` subcommand for removing certificates managed by Certbot\nfrom the configuration directory.\n* Certbot now supports requesting internationalized domain names (IDNs).\n* Hooks provided to Certbot are now saved to be reused during renewal.\nIf you run Certbot with `--pre-hook`, `--renew-hook`, or `--post-hook`\nflags when obtaining a certificate, the provided commands will\nautomatically be saved and executed again when renewing the certificate.\nA pre-hook and/or post-hook can also be given to the `certbot renew`\ncommand either on the command line or in a [configuration\nfile](https://certbot.eff.org/docs/using.html#configuration-file) to run\nan additional command before/after any certificate is renewed. Hooks\nwill only be run if a certificate is renewed.\n* Support Busybox in certbot-auto.\n\n### Changed\n\n* Recategorized `-h/--help` output to improve documentation and\ndiscoverability.\n\n### Removed\n\n* Removed the ncurses interface. This change solves problems people\nwere having on many systems, reduces the number of Certbot\ndependencies, and simplifies our code. Certbot's only interface now is\nthe text interface which was available by providing `-t/--text` to\nearlier versions of Certbot.\n\n### Fixed\n\n* Many small bug fixes.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.10.0is%3Aclosed\n\n## 0.9.3 - 2016-10-13\n\n### Added\n\n* The Apache plugin uses information about your OS to help determine the\nlayout of your Apache configuration directory. We added a patch to\nensure this code behaves the same way when testing on different systems\nas the tests were failing in some cases.\n\n### Changed\n\n* Certbot adopted more conservative behavior about reporting a needed port as\nunavailable when using the standalone plugin.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/27?closed=1\n\n## 0.9.2 - 2016-10-12\n\n### Added\n\n* Certbot stopped requiring that all possibly required ports are available when\nusing the standalone plugin. It now only verifies that the ports are available\nwhen they are necessary.\n\n### Fixed\n\n* Certbot now verifies that our optional dependencies version matches what is\nrequired by Certbot.\n* Certnot now properly copies the `ssl on;` directives as necessary when\nperforming domain validation in the Nginx plugin.\n* Fixed problem where symlinks were becoming files when they were\npackaged, causing errors during testing and OS packaging.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/26?closed=1\n\n## 0.9.1 - 2016-10-06\n\n### Fixed\n\n* Fixed a bug that was introduced in version 0.9.0 where the command\nline flag -q/--quiet wasn't respected in some cases.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/milestone/25?closed=1\n\n## 0.9.0 - 2016-10-05\n\n### Added\n\n* Added an alpha version of the Nginx plugin. This plugin fully automates the\nprocess of obtaining and installing certificates with Nginx.\nAdditionally, it is able to automatically configure security\nenhancements such as an HTTP to HTTPS redirect and OCSP stapling. To use\nthis plugin, you must have the `certbot-nginx` package installed (which\nis installed automatically when using `certbot-auto`) and provide\n`--nginx` on the command line. This plugin is still in its early stages\nso we recommend you use it with some caution and make sure you have a\nbackup of your Nginx configuration.\n* Added support for the `DNS` challenge in the `acme` library and `DNS` in\nCertbot's `manual` plugin. This allows you to create DNS records to\nprove to Let's Encrypt you control the requested domain name. To use\nthis feature, include `--manual --preferred-challenges dns` on the\ncommand line.\n* Certbot now helps with enabling Extra Packages for Enterprise Linux (EPEL) on\nCentOS 6 when using `certbot-auto`. To use `certbot-auto` on CentOS 6,\nthe EPEL repository has to be enabled. `certbot-auto` will now prompt\nusers asking them if they would like the script to enable this for them\nautomatically. This is done without prompting users when using\n`letsencrypt-auto` or if `-n/--non-interactive/--noninteractive` is\nincluded on the command line.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.9.0+is%3Aclosed\n\n## 0.8.1 - 2016-06-14\n\n### Added\n\n* Certbot now preserves a certificate's common name when using `renew`.\n* Certbot now saves webroot values for renewal when they are entered interactively.\n* Certbot now gracefully reports that the Apache plugin isn't usable when Augeas is not installed.\n* Added experimental support for Mageia has been added to `certbot-auto`.\n\n### Fixed\n\n* Fixed problems with an invalid user-agent string on OS X.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.8.1+\n\n## 0.8.0 - 2016-06-02\n\n### Added\n\n* Added the `register` subcommand which can be used to register an account\nwith the Let's Encrypt CA.\n* You can now run `certbot register --update-registration` to\nchange the e-mail address associated with your registration.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue+milestone%3A0.8.0+\n\n## 0.7.0 - 2016-05-27\n\n### Added\n\n* Added `--must-staple` to request certificates from Let's Encrypt\nwith the OCSP must staple extension.\n* Certbot now automatically configures OSCP stapling for Apache.\n* Certbot now allows requesting certificates for domains found in the common name\nof a custom CSR.\n\n### Fixed\n\n* Fixed a number of miscellaneous bugs\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=milestone%3A0.7.0+is%3Aissue\n\n## 0.6.0 - 2016-05-12\n\n### Added\n\n* Versioned the datetime dependency in setup.py.\n\n### Changed\n\n* Renamed the client from `letsencrypt` to `certbot`.\n\n### Fixed\n\n* Fixed a small json deserialization error.\n* Certbot now preserves domain order in generated CSRs.\n* Fixed some minor bugs.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/certbot/certbot/issues?q=is%3Aissue%20milestone%3A0.6.0%20is%3Aclosed%20\n\n## 0.5.0 - 2016-04-05\n\n### Added\n\n* Added the ability to use the webroot plugin interactively.\n* Added the flags --pre-hook, --post-hook, and --renew-hook which can be used with\nthe renew subcommand to register shell commands to run in response to\nrenewal events. Pre-hook commands will be run before any certs are\nrenewed, post-hook commands will be run after any certs are renewed,\nand renew-hook commands will be run after each cert is renewed. If no\ncerts are due for renewal, no command is run.\n* Added a -q/--quiet flag which silences all output except errors.\n* Added an --allow-subset-of-domains flag which can be used with the renew\ncommand to prevent renewal failures for a subset of the requested\ndomains from causing the client to exit.\n\n### Changed\n\n* Certbot now uses renewal configuration files. In /etc/letsencrypt/renewal\nby default, these files can be used to control what parameters are\nused when renewing a specific certificate.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=milestone%3A0.5.0+is%3Aissue\n\n## 0.4.2 - 2016-03-03\n\n### Fixed\n\n* Resolved problems encountered when compiling letsencrypt\nagainst the new OpenSSL release.\n* Fixed problems encountered when using `letsencrypt renew` with configuration files\nfrom the private beta.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.4.2\n\n## 0.4.1 - 2016-02-29\n\n### Fixed\n\n* Fixed Apache parsing errors encountered with some configurations.\n* Fixed Werkzeug dependency problems encountered on some Red Hat systems.\n* Fixed bootstrapping failures when using letsencrypt-auto with --no-self-upgrade.\n* Fixed problems with parsing renewal config files from private beta.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=is:issue+milestone:0.4.1\n\n## 0.4.0 - 2016-02-10\n\n### Added\n\n* Added the verb/subcommand `renew` which can be used to renew your existing\ncertificates as they approach expiration. Running `letsencrypt renew`\nwill examine all existing certificate lineages and determine if any are\nless than 30 days from expiration. If so, the client will use the\nsettings provided when you previously obtained the certificate to renew\nit. The subcommand finishes by printing a summary of which renewals were\nsuccessful, failed, or not yet due.\n* Added a `--dry-run` flag to help with testing configuration\nwithout affecting production rate limits. Currently supported by the\n`renew` and `certonly` subcommands, providing `--dry-run` on the command\nline will obtain certificates from the staging server without saving the\nresulting certificates to disk.\n* Added major improvements to letsencrypt-auto. This script\nhas been rewritten to include full support for Python 2.6, the ability\nfor letsencrypt-auto to update itself, and improvements to the\nstability, security, and performance of the script.\n* Added support for Apache 2.2 to the Apache plugin.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.4.0\n\n## 0.3.0 - 2016-01-27\n\n### Added\n\n* Added a non-interactive mode which can be enabled by including `-n` or\n`--non-interactive` on the command line. This can be used to guarantee\nthe client will not prompt when run automatically using cron/systemd.\n* Added preparation for the new letsencrypt-auto script. Over the past\ncouple months, we've been working on increasing the reliability and\nsecurity of letsencrypt-auto. A number of changes landed in this\nrelease to prepare for the new version of this script.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.3.0\n\n## 0.2.0 - 2016-01-14\n\n### Added\n\n* Added Apache plugin support for non-Debian based systems. Support has been\nadded for modern Red Hat based systems such as Fedora 23, Red Hat 7,\nand CentOS 7 running Apache 2.4. In theory, this plugin should be\nable to be configured to run on any Unix-like OS running Apache 2.4.\n* Relaxed PyOpenSSL version requirements. This adds support for systems\nwith PyOpenSSL versions 0.13 or 0.14.\n* Improved error messages from the client.\n\n### Fixed\n\n* Resolved issues with the Apache plugin enabling an HTTP to HTTPS\nredirect on some systems.\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=is%3Aissue+milestone%3A0.2.0\n\n## 0.1.1 - 2015-12-15\n\n### Added\n\n* Added a check that avoids attempting to issue for unqualified domain names like\n\"localhost\".\n\n### Fixed\n\n* Fixed a confusing UI path that caused some users to repeatedly renew\ntheir certs while experimenting with the client, in some cases hitting\nissuance rate limits.\n* Fixed numerous Apache configuration parser problems\n* Fixed --webroot permission handling for non-root users\n\nMore details about these changes can be found on our GitHub repo:\nhttps://github.com/letsencrypt/letsencrypt/issues?q=milestone%3A0.1.1\n"
  },
  {
    "path": "certbot/LICENSE.txt",
    "content": "Certbot ACME Client\nCopyright (c) Electronic Frontier Foundation and others\nLicensed Apache Version 2.0\n\nThe nginx plugin incorporates code from nginxparser\nCopyright (c) 2014 Fatih Erikli\nLicensed MIT\n\n\nText of Apache License\n======================\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n\nText of MIT License\n===================\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "certbot/MANIFEST.in",
    "content": "include README.rst\ninclude CHANGELOG.md\ninclude LICENSE.txt\nrecursive-include docs *\nrecursive-include examples *\nrecursive-include src/certbot/tests/testdata *\ninclude src/certbot/ssl-dhparams.pem\ninclude src/certbot/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot/README.rst",
    "content": ".. This file contains a series of comments that are used to include sections of this README in other files. Do not modify these comments unless you know what you are doing. tag:intro-begin\n\n|build-status|\n\n.. |build-status| image:: https://img.shields.io/azure-devops/build/certbot/ba534f81-a483-4b9b-9b4e-a60bec8fee72/5/main\n   :target: https://dev.azure.com/certbot/certbot/_build?definitionId=5\n   :alt: Azure Pipelines CI status\n\n.. image:: https://raw.githubusercontent.com/EFForg/design/master/logos/certbot/eff-certbot-lockup.png\n  :width: 200\n  :alt: EFF Certbot Logo\n\nCertbot is part of EFF’s effort to encrypt the entire Internet. Secure communication over the Web relies on HTTPS, which requires the use of a digital certificate that lets browsers verify the identity of web servers (e.g., is that really google.com?). Web servers obtain their certificates from trusted third parties called certificate authorities (CAs). Certbot is an easy-to-use client that fetches a certificate from Let’s Encrypt—an open certificate authority launched by the EFF, Mozilla, and others—and deploys it to a web server.\n\nAnyone who has gone through the trouble of setting up a secure website knows what a hassle getting and maintaining a certificate is. Certbot and Let’s Encrypt can automate away the pain and let you turn on and manage HTTPS with simple commands. Using Certbot and Let's Encrypt is free.\n\n.. _installation:\n\nGetting Started\n---------------\nThe best way to get started is to use our `interactive guide <https://certbot.eff.org>`_. It generates instructions based on your configuration settings. In most cases, you’ll need `root or administrator access <https://certbot.eff.org/faq/#does-certbot-require-root-administrator-privileges>`_ to your web server to run Certbot.\n\nCertbot is meant to be run directly on your web server on the command line, not on your personal computer. If you’re using a hosted service and don’t have direct access to your web server, you might not be able to use Certbot. Check with your hosting provider for documentation about uploading certificates or using certificates issued by Let’s Encrypt.\n\nContributing\n------------\n\nIf you'd like to contribute to this project please read `Developer Guide\n<https://certbot.eff.org/docs/contributing.html>`_.\n\nThis project is governed by `EFF's Public Projects Code of Conduct <https://www.eff.org/pages/eppcode>`_.\n\nLinks\n=====\n\n.. Do not modify this comment unless you know what you're doing. tag:links-begin\n\nDocumentation: https://certbot.eff.org/docs\n\nSoftware project: https://github.com/certbot/certbot\n\nChangelog: https://github.com/certbot/certbot/blob/main/certbot/CHANGELOG.md\n\nFor Contributors: https://certbot.eff.org/docs/contributing.html\n\nFor Users: https://certbot.eff.org/docs/using.html\n\nMain Website: https://certbot.eff.org\n\nLet's Encrypt Website: https://letsencrypt.org\n\nCommunity: https://community.letsencrypt.org\n\nACME spec: `RFC 8555 <https://tools.ietf.org/html/rfc8555>`_\n\nACME working area in github (archived): https://github.com/ietf-wg-acme/acme\n\n.. Do not modify this comment unless you know what you're doing. tag:links-end\n\n.. Do not modify this comment unless you know what you're doing. tag:intro-end\n\n.. Do not modify this comment unless you know what you're doing. tag:features-begin\n\nCurrent Features\n=====================\n\n* Supports multiple web servers:\n\n  - Apache 2.4+\n  - nginx/0.8.48+\n  - webroot (adds files to webroot directories in order to prove control of\n    domains and obtain certificates)\n  - standalone (runs its own simple webserver to prove you control a domain)\n  - other server software via `third party plugins <https://certbot.eff.org/docs/using.html#third-party-plugins>`_\n\n* The private key is generated locally on your system.\n* Can talk to the Let's Encrypt CA or optionally to other ACME\n  compliant services.\n* Can get domain-validated (DV) certificates.\n* Can revoke certificates.\n* Supports ECDSA (default) and RSA certificate private keys.\n* Can optionally install a http -> https redirect, so your site effectively\n  runs https only.\n* Fully automated.\n* Configuration changes are logged and can be reverted.\n\n.. Do not modify this comment unless you know what you're doing. tag:features-end\n\nThanks\n------\n\nWe appreciate the donation of credits to help us test and develop Certbot from:\n\n.. image:: https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg\n  :width: 201\n  :alt: DigitalOcean Logo\n  :target: https://www.digitalocean.com/\n"
  },
  {
    "path": "certbot/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot/docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = _build\n\n# User-friendly check for sphinx-build\nifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)\n$(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/)\nendif\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .\n\n.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext\n\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\nclean:\n\trm -rf $(BUILDDIR)/*\n\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/LetsEncrypt.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/LetsEncrypt.qhc\"\n\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/LetsEncrypt\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/LetsEncrypt\"\n\t@echo \"# devhelp\"\n\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n"
  },
  {
    "path": "certbot/docs/_static/.gitignore",
    "content": ""
  },
  {
    "path": "certbot/docs/_templates/footer.html",
    "content": "<footer>\n  {% if (theme_prev_next_buttons_location == 'bottom' or theme_prev_next_buttons_location == 'both') and (next or prev) %}\n    <div class=\"rst-footer-buttons\" role=\"navigation\" aria-label=\"footer navigation\">\n      {% if next %}\n        <a href=\"{{ next.link|e }}\" class=\"btn btn-neutral float-right\" title=\"{{ next.title|striptags|e }}\" accesskey=\"n\" rel=\"next\">{{ _('Next') }} <span class=\"fa fa-arrow-circle-right\"></span></a>\n      {% endif %}\n      {% if prev %}\n        <a href=\"{{ prev.link|e }}\" class=\"btn btn-neutral\" title=\"{{ prev.title|striptags|e }}\" accesskey=\"p\" rel=\"prev\"><span class=\"fa fa-arrow-circle-left\"></span> {{ _('Previous') }}</a>\n      {% endif %}\n    </div>\n  {% endif %}\n\n  <hr/>\n\n  <div role=\"contentinfo\">\n    <p>\n    <span class=\"copyright\">\n    &copy; Copyright 2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at <a href=\"https://eff.org/cb-license\">https://eff.org/cb-license</a>.\n    </span>\n    <br>\n    <br>\n    <span class=\"status\">\n        <a href=\"https://letsencrypt.status.io/\">Let's Encrypt Status</a>\n    </span>\n\n    {%- if build_id and build_url %}\n      {% trans build_url=build_url, build_id=build_id %}\n        <span class=\"build\">\n          Build\n          <a href=\"{{ build_url }}\">{{ build_id }}</a>.\n        </span>\n      {% endtrans %}\n    {%- elif commit %}\n      {% trans commit=commit %}\n        <span class=\"commit\">\n          Revision <code>{{ commit }}</code>.\n        </span>\n      {% endtrans %}\n    {%- elif last_updated %}\n      {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}\n    {%- endif %}\n\n    </p>\n  </div>\n\n  {%- if show_sphinx %}\n  {% trans %}Built with <a href=\"https://www.sphinx-doc.org/\">Sphinx</a> using a <a href=\"https://github.com/snide/sphinx_rtd_theme\">theme</a> provided by <a href=\"https://readthedocs.org\">Read the Docs</a>{% endtrans %}.\n  {%- endif %}\n\n  {%- block extrafooter %} {% endblock %}\n\n</footer>\n"
  },
  {
    "path": "certbot/docs/api/certbot.achallenges.rst",
    "content": "certbot.achallenges module\n==========================\n\n.. automodule:: certbot.achallenges\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.compat.filesystem.rst",
    "content": "certbot.compat.filesystem module\n================================\n\n.. automodule:: certbot.compat.filesystem\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.compat.misc.rst",
    "content": "certbot.compat.misc module\n==========================\n\n.. automodule:: certbot.compat.misc\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.compat.os.rst",
    "content": "certbot.compat.os module\n========================\n\n.. automodule:: certbot.compat.os\n    :members: chmod, umask, chown, open, mkdir, makedirs, rename, replace, access, stat, fstat\n"
  },
  {
    "path": "certbot/docs/api/certbot.compat.rst",
    "content": "certbot.compat package\n======================\n\n.. automodule:: certbot.compat\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nSubmodules\n----------\n\n.. toctree::\n\n   certbot.compat.filesystem\n   certbot.compat.misc\n   certbot.compat.os\n\n"
  },
  {
    "path": "certbot/docs/api/certbot.crypto_util.rst",
    "content": "certbot.crypto\\_util module\n===========================\n\n.. automodule:: certbot.crypto_util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.display.ops.rst",
    "content": "certbot.display.ops module\n==========================\n\n.. automodule:: certbot.display.ops\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.display.rst",
    "content": "certbot.display package\n=======================\n\n.. automodule:: certbot.display\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nSubmodules\n----------\n\n.. toctree::\n\n   certbot.display.ops\n   certbot.display.util\n\n"
  },
  {
    "path": "certbot/docs/api/certbot.display.util.rst",
    "content": "certbot.display.util module\n===========================\n\n.. automodule:: certbot.display.util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.errors.rst",
    "content": "certbot.errors module\n=====================\n\n.. automodule:: certbot.errors\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.interfaces.rst",
    "content": "certbot.interfaces module\n=========================\n\n.. automodule:: certbot.interfaces\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.main.rst",
    "content": "certbot.main module\n===================\n\n.. automodule:: certbot.main\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.common.rst",
    "content": "certbot.plugins.common module\n=============================\n\n.. automodule:: certbot.plugins.common\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.dns_common.rst",
    "content": "certbot.plugins.dns\\_common module\n==================================\n\n.. automodule:: certbot.plugins.dns_common\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.dns_common_lexicon.rst",
    "content": "certbot.plugins.dns\\_common\\_lexicon module\n===========================================\n\n.. automodule:: certbot.plugins.dns_common_lexicon\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.dns_test_common.rst",
    "content": "certbot.plugins.dns\\_test\\_common module\n========================================\n\n.. automodule:: certbot.plugins.dns_test_common\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.dns_test_common_lexicon.rst",
    "content": "certbot.plugins.dns\\_test\\_common\\_lexicon module\n=================================================\n\n.. automodule:: certbot.plugins.dns_test_common_lexicon\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.enhancements.rst",
    "content": "certbot.plugins.enhancements module\n===================================\n\n.. automodule:: certbot.plugins.enhancements\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.rst",
    "content": "certbot.plugins package\n=======================\n\n.. automodule:: certbot.plugins\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nSubmodules\n----------\n\n.. toctree::\n\n   certbot.plugins.common\n   certbot.plugins.dns_common\n   certbot.plugins.dns_common_lexicon\n   certbot.plugins.dns_test_common\n   certbot.plugins.dns_test_common_lexicon\n   certbot.plugins.enhancements\n   certbot.plugins.storage\n   certbot.plugins.util\n\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.storage.rst",
    "content": "certbot.plugins.storage module\n==============================\n\n.. automodule:: certbot.plugins.storage\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.plugins.util.rst",
    "content": "certbot.plugins.util module\n===========================\n\n.. automodule:: certbot.plugins.util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.reverter.rst",
    "content": "certbot.reverter module\n=======================\n\n.. automodule:: certbot.reverter\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.rst",
    "content": "certbot package\n===============\n\n.. automodule:: certbot\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nSubpackages\n-----------\n\n.. toctree::\n\n    certbot.compat\n    certbot.display\n    certbot.plugins\n    certbot.tests\n\nSubmodules\n----------\n\n.. toctree::\n\n   certbot.achallenges\n   certbot.crypto_util\n   certbot.errors\n   certbot.interfaces\n   certbot.main\n   certbot.reverter\n   certbot.util\n\n"
  },
  {
    "path": "certbot/docs/api/certbot.tests.acme_util.rst",
    "content": "certbot.tests.acme\\_util module\n===============================\n\n.. automodule:: certbot.tests.acme_util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.tests.rst",
    "content": "certbot.tests package\n=====================\n\n.. automodule:: certbot.tests\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nSubmodules\n----------\n\n.. toctree::\n\n   certbot.tests.acme_util\n   certbot.tests.util\n\n"
  },
  {
    "path": "certbot/docs/api/certbot.tests.util.rst",
    "content": "certbot.tests.util module\n=========================\n\n.. automodule:: certbot.tests.util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api/certbot.util.rst",
    "content": "certbot.util module\n===================\n\n.. automodule:: certbot.util\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "certbot/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\n.. toctree::\n   :maxdepth: 4\n\n   api/certbot\n"
  },
  {
    "path": "certbot/docs/challenges.rst",
    "content": "Challenges\n==========\n\nTo receive a certificate from Let's Encrypt certificate authority (CA), you must pass a *challenge* to\nprove you control each of the domain names that will be listed in the certificate. A challenge is one of\na list of specified tasks that only someone who controls the domain should be able to accomplish, such as:\n\n* Posting a specified file in a specified location on a web site (the HTTP-01 challenge)\n* Posting a specified DNS record in the domain name system (the DNS-01 challenge)\n\nIt’s possible to complete each type of challenge *automatically* (Certbot directly makes the necessary\nchanges itself, or runs another program that does so), or *manually* (Certbot tells you to make a\ncertain change, and you edit a configuration file of some kind in order to accomplish it). Certbot's\ndesign favors performing challenges automatically, and this is the normal case for most users of Certbot.\n\nSome plugins offer an *authenticator*, meaning that they can satisfy challenges:\n\n* Apache plugin: (HTTP-01) Tries to edit your Apache configuration files to temporarily serve files to\n  satisfy challenges from the certificate authority. Use the Apache plugin when you're running Certbot on a\n  web server with Apache listening on port 80.\n* Nginx plugin: (HTTP-01) Tries to edit your nginx configuration files to temporarily serve files to\n  satisfy challenges from the certificate authority. Use the nginx plugin when you're running Certbot on a\n  web server with nginx listening on port 80.\n* Webroot plugin: (HTTP-01) Tries to place a file where it can be served over HTTP on port 80 by a\n  web server running on your system. Use the Webroot plugin when you're running Certbot on\n  a web server with any server application listening on port 80 serving files from a folder on disk in response.\n* Standalone plugin: (HTTP-01) Tries to run a temporary web server listening on HTTP on port 80. Use the\n  Standalone plugin if no existing program is listening to this port.\n* Manual plugin: (DNS-01 or HTTP-01) Either tells you what changes to make to your configuration or updates\n  your DNS records using an external script (for DNS-01) or your webroot (for HTTP-01). Use the Manual\n  plugin if you have the technical knowledge to make configuration changes yourself when asked to do so,\n  and are prepared to repeat these steps every time the certificate needs to be renewed.\n\nTips for Challenges\n-------------------\nGeneral tips:\n\n* Run Certbot on your web server, not on your laptop or another server. It’s usually the easiest way to get a certificate.\n* Use a tool like the DNSchecker at dnsstuff.com to check your DNS records to make sure\n  there are no serious errors. A DNS error can prevent a certificate authority from\n  issuing a certificate, even if it does not prevent your site from loading in a browser.\n* If you are using Apache or NGINX plugins, make sure the configuration of your Apache or NGINX server is correct.\n\nHTTP-01 Challenge\n~~~~~~~~~~~~~~~~~\n\n* Make sure the domain name exists and is already pointed to the public IP address of the server where\n  you’re requesting the certificate.\n* Make sure port 80 is open, publicly reachable from the Internet, and not blocked by a router or firewall.\n* When using the Webroot plugin or the manual plugin, make sure the the webroot directory exists and that you\n  specify it properly. If you set the webroot directory for example.com to `/var/www/example.com`\n  then a file placed in `/var/www/example.com/.well-known/acme-challenge/testfile` should appear on\n  your web site at `http://example.com/.well-known/acme-challenge/testfile` (A redirection to HTTPS\n  is OK here and should not stop the challenge from working.)\n* In some web server configurations, all pages are dynamically generated by some kind of framework,\n  usually using a database backend. In this case, there might not be a particular directory\n  from which the web server can serve filesdirectly. Using the Webroot plugin in this case\n  requires making a change to your web server configuration first.\n* Make sure your web server serves files properly from the directory where the challenge\n  file is placed (e. g. `/.well-known/acme-challenge`) to the expected location on the\n  website without adding a header or footer.\n* When using the Standalone plugin, make sure another program is not already listening to port 80 on the server.\n* When using the Webroot plugin, make sure there is a web server listening on port 80.\n\nDNS-01 Challenge\n~~~~~~~~~~~~~~~~\n\n* When using the manual plugin, make sure your DNS records are correctly updated;\n  you must be able to make appropriate changes to your DNS zone in order to pass the challenge.\n\n"
  },
  {
    "path": "certbot/docs/ciphers.rst",
    "content": "..\n  Sphinx complains that this file isn't included in any toctree, however, we\n  currently link to it in the section about installing Certbot through Docker.\n  Setting :orphan: below suppresses this warning. See\n  https://www.sphinx-doc.org/en/master/usage/restructuredtext/field-lists.html#special-metadata-fields.\n\n:orphan:\n\n============\nCiphersuites\n============\n\n.. contents:: Table of Contents\n   :local:\n\n\n.. _ciphersuites:\n\nIntroduction\n============\n\nAutoupdates\n-----------\n\nWithin certain limits, TLS server software can choose what kind of\ncryptography to use when a client connects. These choices can affect\nsecurity, compatibility, and performance in complex ways. Most of\nthese options are independent of a particular certificate. Certbot\ntries to provide defaults that we think are most useful to our users.\n\nAs described below, Certbot will default to modifying\nserver software's cryptographic settings to keep these up-to-date with\nwhat we think are appropriate defaults when new versions of the Certbot\nare installed (for example, by an operating system package manager).\n\nWhen this feature is implemented, this document will be updated\nto describe how to disable these automatic changes.\n\n\nCryptographic choices\n---------------------\n\nSoftware that uses cryptography must inevitably make choices about what\nkind of cryptography to use and how. These choices entail assumptions\nabout how well particular cryptographic mechanisms resist attack, and what\ntrade-offs are available and appropriate. The choices are constrained\nby compatibility issues (in order to interoperate with other software,\nan implementation must agree to use cryptographic mechanisms that the\nother side also supports) and protocol issues (cryptographic mechanisms\nmust be specified in protocols and there must be a way to agree to use\nthem in a particular context).\n\nThe best choices for a particular application may change over time in\nresponse to new research, new standardization events, changes in computer\nhardware, and changes in the prevalence of legacy software. Much important\nresearch on cryptanalysis and cryptographic vulnerabilities is unpublished\nbecause many researchers have been working in the interest of improving\nsome entities' communications security while weakening, or failing to\nimprove, others' security. But important information that improves our\nunderstanding of the state of the art is published regularly.\n\nWhen enabling TLS support in a compatible web server (which is a separate\nstep from obtaining a certificate), Certbot has the ability to\nupdate that web server's TLS configuration. Again, this is *different\nfrom the cryptographic particulars of the certificate itself*; the\ncertificate as of the initial release will be RSA-signed using one of\nLet's Encrypt's 2048-bit RSA keys, and will describe the subscriber's\nRSA public key (\"subject public key\") of at least 2048 bits, which is\nused for key establishment.\n\nNote that the subscriber's RSA public key can be used in a wide variety\nof key establishment methods, most of which do not use RSA directly\nfor key exchange, but only for authenticating the server!  For example,\nin DHE and ECDHE key exchanges, the subject public key is just used to\nsign other parameters for authentication. You do not have to \"use RSA\"\nfor other purposes just because you're using an RSA key for authentication.\n\nThe certificate doesn't specify other cryptographic or ciphersuite\nparticulars; for example, it doesn't say whether or not parties should\nuse a particular symmetric algorithm like 3DES, or what cipher modes\nthey should use. All of these details are negotiated between client\nand server independent of the content of the ciphersuite. The\nLet's Encrypt project hopes to provide useful defaults that reflect\ngood security choices with respect to the publicly-known state of the\nart. However, the Let's Encrypt certificate authority does *not*\ndictate end-users' security policy, and any site is welcome to change\nits preferences in accordance with its own policy or its administrators'\npreferences, and use different cryptographic mechanisms or parameters,\nor a different priority order, than the defaults provided by Certbot.\n\nIf you don't use Certbot to configure your server directly, because the\nclient doesn't integrate with your server software or because you chose\nnot to use this integration, then the cryptographic defaults haven't been\nmodified, and the cryptography chosen by the server will still be whatever\nthe default for your software was.  For example, if you obtain a\ncertificate using *standalone* mode and then manually install it in an IMAP\nor LDAP server, your cryptographic settings will not be modified by the\nclient in any way.\n\n\nSources of defaults\n-------------------\n\nInitially, Certbot will configure users' servers to use the cryptographic\ndefaults recommended by the Mozilla project. These settings are well-reasoned\nrecommendations that carefully consider client software compatibility. They\nare described at\n\nhttps://wiki.mozilla.org/Security/Server_Side_TLS\n\nand the version implemented by Certbot will be the\nversion that was most current as of the release date of each client\nversion. Mozilla offers three separate sets of cryptographic options,\nwhich trade off security and compatibility differently. These are\nreferred to as the \"Modern\", \"Intermediate\", and \"Old\" configurations\n(in order from most secure to least secure, and least-backwards compatible\nto most-backwards compatible). The client will follow the Mozilla defaults\nfor the *Intermediate* configuration by default, at least with regards to\nciphersuites and TLS versions. Mozilla's web site describes which client\nsoftware will be compatible with each configuration. You can also use\nthe Qualys SSL Labs site to test your server and see whether it\nwill be compatible with particular software versions.\n\nThe Let's Encrypt project expects to follow the Mozilla recommendations\nin the future as those recommendations are updated. (For example, some\nusers have proposed prioritizing a new ciphersuite known as ``0xcc13``\nwhich uses the ChaCha and Poly1305 algorithms, and which is already\nimplemented by the Chrome browser.  Mozilla has delayed recommending\n``0xcc13`` over compatibility and standardization concerns, but is likely\nto recommend it in the future once these concerns have been addressed. At\nthat point, Certbot would likely follow the Mozilla recommendations and favor\nthe use of this ciphersuite as well.)\n\nThe Let's Encrypt project may deviate from the Mozilla recommendations\nin the future if good cause is shown and we believe our users'\npriorities would be well-served by doing so. In general, please address\nrelevant proposals for changing priorities to the Mozilla security\nteam first, before asking the Certbot developers to change\nCertbot's priorities. The Mozilla security team is likely to have more\nresources and expertise to bring to bear on evaluating reasons why its\nrecommendations should be updated.\n\nThe Let's Encrypt project will entertain proposals to create a *very*\nsmall number of alternative configurations (apart from Modern,\nIntermediate, and Old) that there's reason to believe would be widely\nused by sysadmins; this would usually be a preferable course to modifying\nan existing configuration. For example, if many sysadmins want their\nservers configured to track a different expert recommendation, Certbot\ncould add an option to do so.\n\n\nResources for recommendations\n-----------------------------\n\nIn the course of considering how to handle this issue, we received\nrecommendations with sources of expert guidance on ciphersuites and other\ncryptographic parameters. We're grateful to everyone who contributed\nsuggestions. The recommendations we received are available under Feedback_.\n\nCertbot users are welcome to review these authorities to\nbetter inform their own cryptographic parameter choices. We also\nwelcome suggestions of other resources to add to this list. Please keep\nin mind that different recommendations may reflect different priorities\nor evaluations of trade-offs, especially related to compatibility!\n\nFeedback\n========\nWe receive lots of feedback on the type of ciphersuites that Let's Encrypt supports and list some collated feedback below. This section aims to track suggestions and references that people have offered or identified to improve the ciphersuites that Let's Encrypt enables when configuring TLS on servers.\n\nBecause of the Chatham House Rule applicable to some of the discussions, people are *not* individually credited for their suggestions, but most suggestions here were made or found by other people, and I thank them for their contributions.\n\nSome people provided rationale information mostly having to do with compatibility of particular user-agents (especially UAs that don't support ECC, or that don't support DH groups > 1024 bits).  Some ciphersuite configurations have been chosen to try to increase compatibility with older UAs while allowing newer UAs to negotiate stronger crypto.  For example, some configurations forego forward secrecy entirely for connections from old UAs, like by offering ECDHE and RSA key exchange, but no DHE at all.  (There are UAs that can fail the negotiation completely if a DHE ciphersuite with prime > 1024 bits is offered.)\n\nReferences\n----------\n\nRFC 7575\n~~~~~~~~\n\nIETF has published a BCP document, RFC 7525, \"Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)\"\n\nhttps://datatracker.ietf.org/doc/rfc7525/\n\nBetterCrypto.org\n~~~~~~~~~~~~~~~~\n\nBetterCrypto.org, a collaboration of mostly European IT security experts, has published a draft paper, \"Applied Crypto Hardening\"\n\nhttps://bettercrypto.org/\n\nRFC 7919\n~~~~~~~~\n\nIETF has published a document, RFC 7919, \"Negotiated Discrete Log Diffie-Hellman Ephemeral Parameters for TLS\".\nIt advocates using *standardized* DH groups in all cases, not individually-chosen ones (mostly because of the Triple\nHandshake attack which can involve maliciously choosing invalid DH groups).  The RFC provides a list of recommended\ngroups, with primes beginning at 2048 bits and going up from there.  It also has a new protocol mechanism for agreeing\nto use these groups, with the possibility of backwards compatibility (and use of weaker DH groups) for older clients\nand servers that don't know about this mechanism.\n\nhttps://datatracker.ietf.org/doc/html/rfc7919\n\nMozilla\n~~~~~~~\n\nMozilla's general server configuration guidance is available at https://wiki.mozilla.org/Security/Server_Side_TLS\n\nMozilla has also produced a configuration generator: https://ssl-config.mozilla.org\n\nDutch National Cyber Security Centre\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nThe Dutch National Cyber Security Centre has published guidance on \"ICT-beveiligingsrichtlijnen voor Transport Layer Security (TLS)\" (\"IT Security Guidelines for Transport Layer Security (TLS)\").  These are available only in Dutch at\n\nhttps://web.archive.org/web/20190516085116/https://www.ncsc.nl/actueel/whitepapers/ict-beveiligingsrichtlijnen-voor-transport-layer-security-tls.html\n\nI have access to an English-language summary of the recommendations.\n\nKeylength.com\n~~~~~~~~~~~~~\n\nDamien Giry collects recommendations by academic researchers and standards organizations about keylengths for particular cryptoperiods, years, or security levels.  The keylength recommendations of the various sources are summarized in a chart.  This site has been updated over time and includes expert guidance from eight sources published between 2000 and 2017.\n\nhttps://www.keylength.com/\n\nNIST\n~~~~\nNIST published its \"NIST Special Publication 800-52 Revision 2: Guidelines for the Selection, Configuration, and Use of Transport Layer Security (TLS) Implementations\"\n\nhttps://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf\n\nand its \"NIST Special Publication 800-57: Recommendation for Key Management – Part 1: General (Revision 5)\"\n\nhttps://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf\n\nENISA\n~~~~~\n\nENISA published its \"Algorithms, Key Sizes and Parameters Report - 2013\"\n\nhttps://www.enisa.europa.eu/activities/identity-and-trust/library/deliverables/algorithms-key-sizes-and-parameters-report\n\nWeakDH/Logjam\n-------------\n\nThe WeakDH/Logjam research has thrown into question the safety of some existing practice using DH ciphersuites, especially the use of standardized groups with a prime ≤ 1024 bits.  The authors provided detailed guidance, including ciphersuite lists, at\n\nhttps://weakdh.org/sysadmin.html\n\nThese lists may have been derived from Mozilla's recommendations.\nOne of the authors clarified his view of the priorities for various changes as a result of the research at\n\nhttps://web.archive.org/web/20150526022820/https://www.ietf.org/mail-archive/web/tls/current/msg16496.html\n\nIn particular, he supports ECDHE and also supports the use of the standardized groups in the FF-DHE Internet-Draft mentioned above (which isn't clear from the group's original recommendations).\n\nParticular sites' opinions or configurations\n--------------------------------------------\n\nAmazon ELB\n~~~~~~~~~~\n\nAmazon ELB explains its current ciphersuite choices at\n\nhttps://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/elb-security-policy-table.html\n\nU.S. Government 18F\n~~~~~~~~~~~~~~~~~~~\n\nThe 18F site (https://18f.gsa.gov/) is using\n\n::\n\n    ssl_ciphers 'kEECDH+ECDSA+AES128 kEECDH+ECDSA+AES256 kEECDH+AES128 kEECDH+AES256 kEDH+AES128 kEDH+AES256 DES-CBC3-SHA +SHA !aNULL !eNULL !LOW !MD5 !EXP !DSS !PSK !SRP !kECDH !CAMELLIA !RC4 !SEED';\n\nDuraconf\n~~~~~~~~\n\nThe Duraconf project collects particular configuration files, with an apparent focus on avoiding the use of obsolete symmetric ciphers and hash functions, and favoring forward secrecy while not requiring it.\n\nhttps://github.com/ioerror/duraconf\n\nSite scanning or rating tools\n-----------------------------\n\nQualys SSL Labs\n~~~~~~~~~~~~~~~\n\nQualys offers the best-known TLS security scanner, maintained by Ivan Ristić.\n\nhttps://www.ssllabs.com/\n\nDutch NCSC\n~~~~~~~~~~\n\nThe Dutch NCSC, mentioned above, has also made available its own site security scanner which indicates how well sites comply with the recommendations.\n\nhttps://en.internet.nl/\n\nJava compatibility issue\n------------------------\n\nA lot of backward-compatibility concerns have to do with Java hard-coding DHE primes to a 1024-bit limit, accepting DHE ciphersuites in negotiation, and then aborting the connection entirely if a prime > 1024 bits is presented.  The simple summary is that servers offering a Java-compatible DHE ciphersuite in preference to other Java-compatible ciphersuites, and then presenting a DH group with a prime > 1024 bits, will be completely incompatible with clients running some versions of Java.  (This may also be the case with very old MSIE versions...?)  There are various strategies for dealing with this, and maybe we can document the options here.\n"
  },
  {
    "path": "certbot/docs/cli-help.txt",
    "content": "usage: \n  certbot [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...\n\nCertbot can obtain and install HTTPS/TLS/SSL certificates.  By default,\nit will attempt to use a webserver both for obtaining and installing the\ncertificate. The most common SUBCOMMANDS and flags are:\n\nobtain, install, and renew certificates:\n    (default) run   Obtain & install a certificate in your current webserver\n    certonly        Obtain or renew a certificate, but do not install it\n    renew           Renew all previously obtained certificates that are near expiry\n    enhance         Add security enhancements to your existing configuration\n   -d DOMAINS       Comma-separated list of domains to obtain a certificate for\n\n  --apache          Use the Apache plugin for authentication & installation\n  --standalone      Run a standalone webserver for authentication\n  --nginx           Use the Nginx plugin for authentication & installation\n  --webroot         Place files in a server's webroot folder for authentication\n  --manual          Obtain certificates interactively, or using shell script hooks\n\n   -n               Run non-interactively\n  --test-cert       Obtain a test certificate from a staging server\n  --dry-run         Test \"renew\" or \"certonly\" without saving any certificates to disk\n\nmanage certificates:\n    certificates    Display information about certificates you have from Certbot\n    revoke          Revoke a certificate (supply --cert-name or --cert-path)\n    delete          Delete a certificate (supply --cert-name)\n    reconfigure     Update a certificate's configuration (supply --cert-name)\n\nmanage your account:\n    register        Create an ACME account\n    unregister      Deactivate an ACME account\n    update_account  Update an ACME account\n    show_account    Display account details\n  --agree-tos       Agree to the ACME server's Subscriber Agreement\n   -m EMAIL         Email address for important account notifications\n\noptions:\n  -h, --help            show this help message and exit\n  -c CONFIG_FILE, --config CONFIG_FILE\n                        path to config file (default: /etc/letsencrypt/cli.ini\n                        and ~/.config/letsencrypt/cli.ini)\n  -v, --verbose         This flag can be used multiple times to incrementally\n                        increase the verbosity of output, e.g. -vvv. (default:\n                        0)\n  --max-log-backups MAX_LOG_BACKUPS\n                        Specifies the maximum number of backup logs that\n                        should be kept by Certbot's built in log rotation.\n                        Setting this flag to 0 disables log rotation entirely,\n                        causing Certbot to always append to the same log file.\n                        (default: 1000)\n  -n, --non-interactive, --noninteractive\n                        Run without ever asking for user input. This may\n                        require additional command line flags; the client will\n                        try to explain which ones are required if it finds one\n                        missing (default: False)\n  --force-interactive   Force Certbot to be interactive even if it detects\n                        it's not being run in a terminal. This flag cannot be\n                        used with the renew subcommand. (default: False)\n  -d DOMAIN, --domains DOMAIN, --domain DOMAIN\n                        Domain names to include. For multiple domains you can\n                        use multiple -d flags or enter a comma separated list\n                        of domains as a parameter. All domains will be\n                        included as Subject Alternative Names on the\n                        certificate. The first domain will be used as the\n                        certificate name, unless otherwise specified or if you\n                        already have a certificate with the same name. In the\n                        case of a name conflict, a number like -0001 will be\n                        appended to the certificate name. (default: Ask)\n  --ip-address IP_ADDRESSES\n                        IP addresses to include. For multiple IP addresses you\n                        can use multiple --ip-address flags. All IP addresses\n                        will be included as Subject Alternative Names on the\n                        certificate. (default: [])\n  --eab-kid EAB_KID     Key Identifier for External Account Binding (default:\n                        None)\n  --eab-hmac-key EAB_HMAC_KEY\n                        HMAC key for External Account Binding (default: None)\n  --eab-hmac-alg EAB_HMAC_ALG\n                        HMAC algorithm for External Account Binding (default:\n                        HS256)\n  --cert-name CERTNAME  Certificate name to apply. This name is used by\n                        Certbot for housekeeping and in file paths; it doesn't\n                        affect the content of the certificate itself.\n                        Certificate name cannot contain filepath separators\n                        (i.e. '/' or '\\', depending on the platform). To see\n                        certificate names, run 'certbot certificates'. When\n                        creating a new certificate, specifies the new\n                        certificate's name. (default: the first provided\n                        domain or the name of an existing certificate on your\n                        system for the same domains)\n  --dry-run             Perform a test run against the Let's Encrypt staging\n                        server, obtaining test (invalid) certificates but not\n                        saving them to disk. This can only be used with the\n                        'certonly' and 'renew' subcommands. It may trigger\n                        webserver reloads to temporarily modify & roll back\n                        configuration files. --pre-hook and --post-hook\n                        commands run by default. --deploy-hook commands do not\n                        run, unless enabled by --run-deploy-hooks. The test\n                        server may be overridden with --server. (default:\n                        False)\n  --debug-challenges    After setting up challenges, wait for user input\n                        before submitting to CA. When used in combination with\n                        the `-v` option, the challenge URLs or FQDNs and their\n                        expected return values are shown. (default: False)\n  --required-profile REQUIRED_PROFILE\n                        Request the given profile name from the ACME server.\n                        If the ACME server returns an error, issuance (or\n                        renewal) will fail. For long-term reliability, setting\n                        preferred_profile instead may be preferable because it\n                        allows fallback to a default. Use this setting when\n                        renewal failure is preferable to fallback. (default:\n                        None)\n  --preferred-profile PREFERRED_PROFILE\n                        Request the given profile name from the ACME server,\n                        or fallback to default. If the given profile name\n                        exists in the ACME directory, use it to request a a\n                        certificate. Otherwise, fall back to requesting a\n                        certificate without a profile (which means the CA will\n                        use its default profile). This allows renewals to\n                        succeed even if the CA deprecates and removes a given\n                        profile. (default: None)\n  --preferred-chain PREFERRED_CHAIN\n                        Set the preferred certificate chain. If the CA offers\n                        multiple certificate chains, prefer the chain whose\n                        topmost certificate was issued from this Subject\n                        Common Name. If no match, the default offered chain\n                        will be used. (default: None)\n  --preferred-challenges PREF_CHALLS\n                        A sorted, comma delimited list of the preferred\n                        challenge to use during authorization with the most\n                        preferred challenge listed first (Eg, \"dns\" or\n                        \"http,dns\"). Not all plugins support all challenges.\n                        See https://certbot.eff.org/docs/using.html#plugins\n                        for details. ACME Challenges are versioned, but if you\n                        pick \"http\" rather than \"http-01\", Certbot will select\n                        the latest version automatically. (default: [])\n  --issuance-timeout ISSUANCE_TIMEOUT\n                        This option specifies how long (in seconds) Certbot\n                        will wait for the server to issue a certificate.\n                        (default: 90)\n  --user-agent USER_AGENT\n                        Set a custom user agent string for the client. User\n                        agent strings allow the CA to collect high level\n                        statistics about success rates by OS, plugin and use\n                        case, and to know when to deprecate support for past\n                        Python versions and flags. If you wish to hide this\n                        information from the Let's Encrypt server, set this to\n                        \"\". (default: CertbotACMEClient/5.4.0 (certbot;\n                        OS_NAME OS_VERSION) Authenticator/XXX Installer/YYY\n                        (SUBCOMMAND; flags: FLAGS) Py/major.minor.patchlevel).\n                        The flags encoded in the user agent are: --duplicate,\n                        --force-renew, --allow-subset-of-names, -n, and\n                        whether any hooks are set.\n  --user-agent-comment USER_AGENT_COMMENT\n                        Add a comment to the default user agent string. May be\n                        used when repackaging Certbot or calling it from\n                        another tool to allow additional statistical data to\n                        be collected. Ignored if --user-agent is set.\n                        (Example: Foo-Wrapper/1.0) (default: None)\n\nautomation:\n  Flags for automating execution & other tweaks\n\n  --keep-until-expiring, --keep, --reinstall\n                        If the requested certificate matches an existing\n                        certificate, always keep the existing one until it is\n                        due for renewal (for the 'run' subcommand this means\n                        reinstall the existing certificate). (default: Ask)\n  --expand              If an existing certificate is a strict subset of the\n                        requested names, always expand and replace it with the\n                        additional names. (default: Ask)\n  --version             show program's version number and exit\n  --force-renewal, --renew-by-default\n                        If a certificate already exists for the requested\n                        domains, renew it now, regardless of whether it is\n                        near expiry. (Often --keep-until-expiring is more\n                        appropriate). Also implies --expand. (default: False)\n  --renew-with-new-domains\n                        If a certificate already exists for the requested\n                        certificate name but does not match the requested\n                        domains, renew it now, regardless of whether it is\n                        near expiry. (default: False)\n  --reuse-key           When renewing, use the same private key as the\n                        existing certificate. (default: False)\n  --no-reuse-key        When renewing, do not use the same private key as the\n                        existing certificate. Not reusing private keys is the\n                        default behavior of Certbot. This option may be used\n                        to unset --reuse-key on an existing certificate.\n                        (default: False)\n  --new-key             When renewing or replacing a certificate, generate a\n                        new private key, even if --reuse-key is set on the\n                        existing certificate. Combining --new-key and --reuse-\n                        key will result in the private key being replaced and\n                        then reused in future renewals. (default: False)\n  --allow-subset-of-names\n                        When performing domain validation, do not consider it\n                        a failure if authorizations can not be obtained for a\n                        strict subset of the requested domains. This may be\n                        useful for allowing renewals for multiple domains to\n                        succeed even if some domains no longer point at this\n                        system. This option cannot be used with --csr.\n                        (default: False)\n  --agree-tos           Agree to the ACME Subscriber Agreement (default: Ask)\n  --duplicate           Allow making a certificate lineage that duplicates an\n                        existing one (both can be renewed in parallel)\n                        (default: False)\n  -q, --quiet           Silence all output except errors. Useful for\n                        automation via cron. Implies --non-interactive.\n                        (default: False)\n\nsecurity:\n  Security parameters & server settings\n\n  --rsa-key-size N      Size of the RSA key. (default: 2048)\n  --key-type {rsa,ecdsa}\n                        Type of generated private key. Only *ONE* per\n                        invocation can be provided at this time. (default:\n                        ecdsa)\n  --elliptic-curve N    The SECG elliptic curve name to use. Please see RFC\n                        8446 for supported values. (default: secp256r1)\n  --must-staple         Adds the OCSP Must-Staple extension to the\n                        certificate. Autoconfigures OCSP Stapling for\n                        supported setups (Apache version >= 2.3.3 ). (default:\n                        False)\n  --redirect            Automatically redirect all HTTP traffic to HTTPS for\n                        the newly authenticated vhost. (default: redirect\n                        enabled for install and run, disabled for enhance)\n  --no-redirect         Do not automatically redirect all HTTP traffic to\n                        HTTPS for the newly authenticated vhost. (default:\n                        redirect enabled for install and run, disabled for\n                        enhance)\n  --hsts                Add the Strict-Transport-Security header to every HTTP\n                        response. Forcing browser to always use SSL for the\n                        domain. Defends against SSL Stripping. (default:\n                        False)\n  --uir                 Add the \"Content-Security-Policy: upgrade-insecure-\n                        requests\" header to every HTTP response. Forcing the\n                        browser to use https:// for every http:// resource.\n                        (default: False)\n  --staple-ocsp         Enables OCSP Stapling. A valid OCSP response is\n                        stapled to the certificate that the server offers\n                        during TLS. (default: False)\n  --strict-permissions  Require that all configuration files are owned by the\n                        current user; only needed if your config is somewhere\n                        unsafe like /tmp/ (default: False)\n  --auto-hsts           Gradually increasing max-age value for HTTP Strict\n                        Transport Security security header (default: False)\n\ntesting:\n  The following flags are meant for testing and integration purposes only.\n\n  --run-deploy-hooks    When performing a test run using `--dry-run` or\n                        `reconfigure`, run any applicable deploy hooks. This\n                        includes hooks set on the command line, saved in the\n                        certificate's renewal configuration file, or present\n                        in the renewal-hooks directory. To exclude directory\n                        hooks, use --no-directory-hooks. The hook(s) will only\n                        be run if the dry run succeeds, and will use the\n                        current active certificate, not the temporary test\n                        certificate acquired during the dry run. This flag is\n                        recommended when modifying the deploy hook using\n                        `reconfigure`. (default: False)\n  --test-cert, --staging\n                        Use the Let's Encrypt staging server to obtain or\n                        revoke test (invalid) certificates; equivalent to\n                        --server https://acme-\n                        staging-v02.api.letsencrypt.org/directory (default:\n                        False)\n  --debug               Show tracebacks in case of errors (default: False)\n  --no-verify-ssl       Disable verification of the ACME server's certificate.\n                        The root certificates trusted by Certbot can be\n                        overridden by setting the REQUESTS_CA_BUNDLE\n                        environment variable. (default: False)\n  --http-01-port HTTP01_PORT\n                        Port used in the http-01 challenge. This only affects\n                        the port Certbot listens on. A conforming ACME server\n                        will still attempt to connect on port 80. (default:\n                        80)\n  --http-01-address HTTP01_ADDRESS\n                        The address the server listens to during http-01\n                        challenge. (default: )\n  --https-port HTTPS_PORT\n                        Port used to serve HTTPS. This affects which port\n                        Nginx will listen on after a LE certificate is\n                        installed. (default: 443)\n  --break-my-certs      Be willing to replace or renew valid certificates with\n                        invalid (testing/staging) certificates (default:\n                        False)\n\npaths:\n  Flags for changing execution paths & servers\n\n  --cert-path CERT_PATH\n                        Path to where certificate is saved (with certonly\n                        --csr), installed from, or revoked (default: None)\n  --key-path KEY_PATH   Path to private key for certificate installation or\n                        revocation (if account key is missing) (default: None)\n  --fullchain-path FULLCHAIN_PATH\n                        Accompanying path to a full certificate chain\n                        (certificate plus chain). (default: None)\n  --chain-path CHAIN_PATH\n                        Accompanying path to a certificate chain. (default:\n                        None)\n  --config-dir CONFIG_DIR\n                        Configuration directory. (default: /etc/letsencrypt)\n  --work-dir WORK_DIR   Working directory. (default: /var/lib/letsencrypt)\n  --logs-dir LOGS_DIR   Logs directory. (default: /var/log/letsencrypt)\n  --server SERVER       ACME Directory Resource URI. (default:\n                        https://acme-v02.api.letsencrypt.org/directory)\n\nmanage:\n  Various subcommands and flags are available for managing your\n  certificates:\n\n  certificates          List certificates managed by Certbot\n  delete                Clean up all files related to a certificate\n  renew                 Renew all certificates (or one specified with --cert-\n                        name)\n  revoke                Revoke a certificate specified with --cert-path or\n                        --cert-name\n  reconfigure           Update renewal configuration for a certificate\n                        specified by --cert-name\n\nrun:\n  Options for obtaining & installing certificates\n\ncertonly:\n  Options for modifying how a certificate is obtained\n\n  --deploy-hook DEPLOY_HOOK\n                        Command to be run in a shell once for each\n                        successfully issued certificate, including on\n                        subsequent renewals. Unless --disable-hook-validation\n                        is used, the command’s first word must be the absolute\n                        pathname of an executable or one found via the PATH\n                        environment variable. For this command, the shell\n                        variable $RENEWED_LINEAGE will point to the config\n                        live subdirectory (for example,\n                        \"/etc/letsencrypt/live/example.com\") containing the\n                        new certificates and keys; the shell variable\n                        $RENEWED_DOMAINS will contain a space-delimited list\n                        of renewed certificate domains (for example,\n                        \"example.com www.example.com\") (default: None)\n  --csr CSR             Path to a Certificate Signing Request (CSR) in DER or\n                        PEM format. Currently --csr only works with the\n                        'certonly' subcommand. (default: None)\n\nrenew:\n  The 'renew' subcommand will attempt to renew any certificates previously\n  obtained if they are close to expiry, and print a summary of the results.\n  By default, 'renew' will reuse the plugins and options used to obtain or\n  most recently renew each certificate. You can test whether future renewals\n  will succeed with `--dry-run`. Individual certificates can be renewed with\n  the `--cert-name` option. Hooks are available to run commands before and\n  after renewal; see https://certbot.eff.org/docs/using.html#renewal for\n  more information on these.\n\n  --pre-hook PRE_HOOK   Command to be run in a shell before obtaining any\n                        certificates. Unless --disable-hook-validation is\n                        used, the command’s first word must be the absolute\n                        pathname of an executable or one found via the PATH\n                        environment variable. Intended primarily for renewal,\n                        where it can be used to temporarily shut down a\n                        webserver that might conflict with the standalone\n                        plugin. This will only be called if a certificate is\n                        actually to be obtained/renewed. When renewing several\n                        certificates that have identical pre-hooks, only the\n                        first will be executed. (default: None)\n  --post-hook POST_HOOK\n                        Command to be run in a shell after attempting to\n                        obtain/renew certificates. Unless --disable-hook-\n                        validation is used, the command’s first word must be\n                        the absolute pathname of an executable or one found\n                        via the PATH environment variable. Can be used to\n                        deploy renewed certificates, or to restart any servers\n                        that were stopped by --pre-hook. This is only run if\n                        an attempt was made to obtain/renew a certificate. If\n                        multiple renewed certificates have identical post-\n                        hooks, only one will be run. (default: None)\n  --disable-hook-validation\n                        Ordinarily the commands specified for --pre-\n                        hook/--post-hook/--deploy-hook will be checked for\n                        validity, to see if the programs being run are in the\n                        $PATH, so that mistakes can be caught early, even when\n                        the hooks aren't being run just yet. The validation is\n                        rather simplistic and fails if you use more advanced\n                        shell constructs, so you can use this switch to\n                        disable it. (default: False)\n  --no-directory-hooks  Disable running executables found in Certbot's hook\n                        directories. (default: False)\n  --disable-renew-updates\n                        Disable automatic updates to your server configuration\n                        that would otherwise be done by the selected installer\n                        plugin, and triggered when the user executes \"certbot\n                        renew\", regardless of if the certificate is renewed.\n                        This setting does not apply to important TLS\n                        configuration updates. (default: False)\n  --no-autorenew        Disable auto renewal of certificates. (default: False)\n\ncertificates:\n  List certificates managed by Certbot\n\ndelete:\n  Options for deleting a certificate\n\nrevoke:\n  Options for revocation of certificates\n\n  --reason {unspecified,keycompromise,affiliationchanged,superseded,cessationofoperation}\n                        Specify reason for revoking certificate. (default:\n                        unspecified)\n  --delete-after-revoke\n                        Delete certificates after revoking them, along with\n                        all previous and later versions of those certificates.\n                        (default: Ask)\n  --no-delete-after-revoke\n                        Do not delete certificates after revoking them. This\n                        option should be used with caution because the 'renew'\n                        subcommand will attempt to renew undeleted revoked\n                        certificates. (default: Ask)\n\nregister:\n  Options for account registration\n\n  -m EMAIL, --email EMAIL\n                        Email used for registration and recovery contact. Use\n                        comma to register multiple emails, ex:\n                        u1@example.com,u2@example.com. (default: Ask).\n  --eff-email           Share your e-mail address with EFF (default: Ask)\n  --no-eff-email        Don't share your e-mail address with EFF (default:\n                        Ask)\n\nupdate_account:\n  Options for account modification\n\nunregister:\n  Options for account deactivation.\n\n  --account ACCOUNT_ID  Account ID to use (default: None)\n\ninstall:\n  Options for modifying how a certificate is deployed\n\nrollback:\n  Options for rolling back server configuration changes\n\n  --checkpoints N       Revert configuration N number of checkpoints.\n                        (default: 1)\n\nplugins:\n  Options for the \"plugins\" subcommand\n\n  --init                Initialize plugins. (default: False)\n  --prepare             Initialize and prepare plugins. (default: False)\n  --authenticators      Limit to authenticator plugins only. (default: None)\n  --installers          Limit to installer plugins only. (default: None)\n\nenhance:\n  Helps to harden the TLS configuration by adding security enhancements to\n  already existing configuration.\n\nshow_account:\n  Options useful for the \"show_account\" subcommand:\n\nreconfigure:\n  Common options that may be updated with the \"reconfigure\" subcommand:\n\nplugins:\n  Plugin Selection: Certbot client supports an extensible plugins\n  architecture. See 'certbot plugins' for a list of all installed plugins\n  and their names. You can force a particular plugin by setting options\n  provided below. Running --help <plugin_name> will list flags specific to\n  that plugin.\n\n  --configurator CONFIGURATOR\n                        Name of the plugin that is both an authenticator and\n                        an installer. Should not be used together with\n                        --authenticator or --installer. (default: Ask)\n  -a AUTHENTICATOR, --authenticator AUTHENTICATOR\n                        Authenticator plugin name. (default: None)\n  -i INSTALLER, --installer INSTALLER\n                        Installer plugin name (also used to find domains).\n                        (default: None)\n  --apache              Obtain and install certificates using Apache (default:\n                        False)\n  --nginx               Obtain and install certificates using Nginx (default:\n                        False)\n  --standalone          Obtain certificates using a \"standalone\" webserver.\n                        (default: False)\n  --manual              Provide laborious manual instructions for obtaining a\n                        certificate (default: False)\n  --webroot             Obtain certificates by placing files in a webroot\n                        directory. (default: False)\n  --dns-cloudflare      Obtain certificates using a DNS TXT record (if you are\n                        using Cloudflare for DNS). (default: False)\n  --dns-digitalocean    Obtain certificates using a DNS TXT record (if you are\n                        using DigitalOcean for DNS). (default: False)\n  --dns-dnsimple        Obtain certificates using a DNS TXT record (if you are\n                        using DNSimple for DNS). (default: False)\n  --dns-dnsmadeeasy     Obtain certificates using a DNS TXT record (if you are\n                        using DNS Made Easy for DNS). (default: False)\n  --dns-gehirn          Obtain certificates using a DNS TXT record (if you are\n                        using Gehirn Infrastructure Service for DNS).\n                        (default: False)\n  --dns-google          Obtain certificates using a DNS TXT record (if you are\n                        using Google Cloud DNS). (default: False)\n  --dns-linode          Obtain certificates using a DNS TXT record (if you are\n                        using Linode for DNS). (default: False)\n  --dns-luadns          Obtain certificates using a DNS TXT record (if you are\n                        using LuaDNS for DNS). (default: False)\n  --dns-nsone           Obtain certificates using a DNS TXT record (if you are\n                        using NS1 for DNS). (default: False)\n  --dns-ovh             Obtain certificates using a DNS TXT record (if you are\n                        using OVH for DNS). (default: False)\n  --dns-rfc2136         Obtain certificates using a DNS TXT record (if you are\n                        using BIND for DNS). (default: False)\n  --dns-route53         Obtain certificates using a DNS TXT record (if you are\n                        using Route53 for DNS). (default: False)\n  --dns-sakuracloud     Obtain certificates using a DNS TXT record (if you are\n                        using Sakura Cloud for DNS). (default: False)\n\napache:\n  Apache Web Server plugin (Please note that the default values of the\n  Apache plugin options change depending on the operating system Certbot is\n  run on.)\n\n  --apache-enmod APACHE_ENMOD\n                        Path to the Apache 'a2enmod' binary (default: None)\n  --apache-dismod APACHE_DISMOD\n                        Path to the Apache 'a2dismod' binary (default: None)\n  --apache-le-vhost-ext APACHE_LE_VHOST_EXT\n                        SSL vhost configuration extension (default: -le-\n                        ssl.conf)\n  --apache-server-root APACHE_SERVER_ROOT\n                        Apache server root directory (default: /etc/apache2)\n  --apache-vhost-root APACHE_VHOST_ROOT\n                        Apache server VirtualHost configuration root (default:\n                        None)\n  --apache-logs-root APACHE_LOGS_ROOT\n                        Apache server logs directory (default:\n                        /var/log/apache2)\n  --apache-challenge-location APACHE_CHALLENGE_LOCATION\n                        Directory path for challenge configuration (default:\n                        /etc/apache2)\n  --apache-handle-modules APACHE_HANDLE_MODULES\n                        Let installer handle enabling required modules for you\n                        (Only Ubuntu/Debian currently) (default: False)\n  --apache-handle-sites APACHE_HANDLE_SITES\n                        Let installer handle enabling sites for you (Only\n                        Ubuntu/Debian currently) (default: False)\n  --apache-ctl APACHE_CTL\n                        Full path to Apache control script (default:\n                        apache2ctl)\n  --apache-bin APACHE_BIN\n                        Full path to apache2/httpd binary (default: None)\n\ndns-cloudflare:\n  Obtain certificates using a DNS TXT record (if you are using Cloudflare\n  for DNS).\n\n  --dns-cloudflare-propagation-seconds DNS_CLOUDFLARE_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 10)\n  --dns-cloudflare-credentials DNS_CLOUDFLARE_CREDENTIALS\n                        Cloudflare credentials INI file. (default: None)\n\ndns-digitalocean:\n  Obtain certificates using a DNS TXT record (if you are using DigitalOcean\n  for DNS).\n\n  --dns-digitalocean-propagation-seconds DNS_DIGITALOCEAN_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 10)\n  --dns-digitalocean-credentials DNS_DIGITALOCEAN_CREDENTIALS\n                        DigitalOcean credentials INI file. (default: None)\n\ndns-dnsimple:\n  Obtain certificates using a DNS TXT record (if you are using DNSimple for\n  DNS).\n\n  --dns-dnsimple-propagation-seconds DNS_DNSIMPLE_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 30)\n  --dns-dnsimple-credentials DNS_DNSIMPLE_CREDENTIALS\n                        DNSimple credentials INI file. (default: None)\n\ndns-dnsmadeeasy:\n  Obtain certificates using a DNS TXT record (if you are using DNS Made Easy\n  for DNS).\n\n  --dns-dnsmadeeasy-propagation-seconds DNS_DNSMADEEASY_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 60)\n  --dns-dnsmadeeasy-credentials DNS_DNSMADEEASY_CREDENTIALS\n                        DNS Made Easy credentials INI file. (default: None)\n\ndns-gehirn:\n  Obtain certificates using a DNS TXT record (if you are using Gehirn\n  Infrastructure Service for DNS).\n\n  --dns-gehirn-propagation-seconds DNS_GEHIRN_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 30)\n  --dns-gehirn-credentials DNS_GEHIRN_CREDENTIALS\n                        Gehirn Infrastructure Service credentials file.\n                        (default: None)\n\ndns-google:\n  Obtain certificates using a DNS TXT record (if you are using Google Cloud\n  DNS for DNS).\n\n  --dns-google-propagation-seconds DNS_GOOGLE_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 60)\n  --dns-google-credentials DNS_GOOGLE_CREDENTIALS\n                        Path to Google Cloud DNS service account JSON file to\n                        use instead of relying on Application Default\n                        Credentials (ADC). (See https://cloud.google.com/docs/\n                        authentication/application-default-credentials for\n                        information about ADC, https://developers.google.com/i\n                        dentity/protocols/OAuth2ServiceAccount#creatinganaccou\n                        nt for information about creating a service account,\n                        and https://cloud.google.com/dns/access-\n                        control#permissions_and_roles for information about\n                        the permissions required to modify Cloud DNS records.)\n                        (default: None)\n  --dns-google-project DNS_GOOGLE_PROJECT\n                        The ID of the Google Cloud project that the Google\n                        Cloud DNS managed zone(s) reside in. This will be\n                        determined automatically if not specified. (default:\n                        None)\n\ndns-linode:\n  Obtain certificates using a DNS TXT record (if you are using Linode for\n  DNS).\n\n  --dns-linode-propagation-seconds DNS_LINODE_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 120)\n  --dns-linode-credentials DNS_LINODE_CREDENTIALS\n                        Linode credentials INI file. (default: None)\n\ndns-luadns:\n  Obtain certificates using a DNS TXT record (if you are using LuaDNS for\n  DNS).\n\n  --dns-luadns-propagation-seconds DNS_LUADNS_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 30)\n  --dns-luadns-credentials DNS_LUADNS_CREDENTIALS\n                        LuaDNS credentials INI file. (default: None)\n\ndns-nsone:\n  Obtain certificates using a DNS TXT record (if you are using NS1 for DNS).\n\n  --dns-nsone-propagation-seconds DNS_NSONE_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 30)\n  --dns-nsone-credentials DNS_NSONE_CREDENTIALS\n                        NS1 credentials file. (default: None)\n\ndns-ovh:\n  Obtain certificates using a DNS TXT record (if you are using OVH for DNS).\n\n  --dns-ovh-propagation-seconds DNS_OVH_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 120)\n  --dns-ovh-credentials DNS_OVH_CREDENTIALS\n                        OVH credentials INI file. (default: None)\n\ndns-rfc2136:\n  Obtain certificates using a DNS TXT record (if you are using BIND for\n  DNS).\n\n  --dns-rfc2136-propagation-seconds DNS_RFC2136_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 60)\n  --dns-rfc2136-credentials DNS_RFC2136_CREDENTIALS\n                        RFC 2136 credentials INI file. (default: None)\n\ndns-route53:\n  Obtain certificates using a DNS TXT record (if you are using AWS Route53\n  for DNS).\n\ndns-sakuracloud:\n  Obtain certificates using a DNS TXT record (if you are using Sakura Cloud\n  for DNS).\n\n  --dns-sakuracloud-propagation-seconds DNS_SAKURACLOUD_PROPAGATION_SECONDS\n                        The number of seconds to wait for DNS to propagate\n                        before asking the ACME server to verify the DNS\n                        record. (default: 90)\n  --dns-sakuracloud-credentials DNS_SAKURACLOUD_CREDENTIALS\n                        Sakura Cloud credentials file. (default: None)\n\nmanual:\n  Authenticate through manual configuration or custom shell scripts. When\n  using shell scripts, an authenticator script must be provided. The\n  environment variables available to this script depend on the type of\n  challenge. $CERTBOT_IDENTIFIER will always contain the domain or IP\n  address being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION\n  is the validation string, and $CERTBOT_TOKEN is the filename of the\n  resource requested when performing an HTTP-01 challenge. An additional\n  cleanup script can also be provided and can use the additional variable\n  $CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth\n  script. For both authenticator and cleanup script, on HTTP-01 and DNS-01\n  challenges, $CERTBOT_REMAINING_CHALLENGES will be equal to the number of\n  challenges that remain after the current one, and $CERTBOT_ALL_IDENTIFIERS\n  contains a comma-separated list of all identifiers that are challenged for\n  the current certificate.\n\n  --manual-auth-hook MANUAL_AUTH_HOOK\n                        Path or command to execute for the authentication\n                        script (default: None)\n  --manual-cleanup-hook MANUAL_CLEANUP_HOOK\n                        Path or command to execute for the cleanup script\n                        (default: None)\n\nnginx:\n  Nginx Web Server plugin\n\n  --nginx-server-root NGINX_SERVER_ROOT\n                        Nginx server root directory. (default: /etc/nginx or\n                        /usr/local/etc/nginx)\n  --nginx-ctl NGINX_CTL\n                        Path to the 'nginx' binary, used for 'configtest' and\n                        retrieving nginx version number. (default: nginx)\n  --nginx-sleep-seconds NGINX_SLEEP_SECONDS\n                        Number of seconds to wait for nginx configuration\n                        changes to apply when reloading. (default: 1)\n\nnull:\n  Null Installer\n\nstandalone:\n  Runs an HTTP server locally which serves the necessary validation files\n  under the /.well-known/acme-challenge/ request path. Suitable if there is\n  no HTTP server already running. HTTP challenge only (wildcards not\n  supported).\n\nwebroot:\n  Saves the necessary validation files to a .well-known/acme-challenge/\n  directory within the nominated webroot path. A separate HTTP server must\n  be running and serving files from the webroot path. HTTP challenge only\n  (wildcards not supported).\n\n  --webroot-path WEBROOT_PATH, -w WEBROOT_PATH\n                        public_html / webroot path. This can be specified\n                        multiple times to handle different identifiers; each\n                        identifier will have the webroot path that preceded\n                        it. For instance: `-w /var/www/example -d example.com\n                        -d www.example.com -w /var/www/thing -d thing.net -d\n                        m.thing.net` (default: Ask)\n  --webroot-map WEBROOT_MAP\n                        JSON dictionary mapping identifiers to webroot paths;\n                        this implies -d or --ip-address for each entry. You\n                        may need to escape this from your shell. E.g.:\n                        --webroot-map '{\"eg1.is,m.eg1.is\":\"/www/eg1/\",\n                        \"eg2.is\":\"/www/eg2\"}' This option is merged with, but\n                        takes precedence over, -w / -d entries. At present, if\n                        you put webroot-map in a config file, it needs to be\n                        on a single line, like: webroot-map =\n                        {\"example.com\":\"/var/www\"}. (default: {})\n"
  },
  {
    "path": "certbot/docs/compatibility.rst",
    "content": "=======================\nBackwards Compatibility\n=======================\n\nAll Certbot components including `acme <https://acme-python.readthedocs.io/>`_,\nCertbot, and :ref:`non-third party plugins <plugins>` follow `Semantic\nVersioning <https://semver.org/>`_ both for its Python :doc:`API <api>` and for the\napplication itself. This means that we will not change behavior in a backwards\nincompatible way except in a new major version of the project.\n\n.. note:: None of this applies to the behavior of Certbot distribution\n    mechanisms such as :ref:`our snaps <snap-install>` or OS packages whose\n    behavior may change at any time. Semantic versioning only applies to the\n    common Certbot components that are installed by various distribution\n    methods.\n\nFor Certbot as an application, the command line interface and non-interactive\nbehavior can be considered stable with two exceptions. The first is that no\naspects of Certbot's console or log output should be considered stable and it\nmay change at any time. The second is that Certbot's behavior should only be\nconsidered stable with certain files but not all. Files with which users should\nexpect Certbot to maintain its current behavior with are:\n\n* ``/etc/letsencrypt/live/$domain/{cert,chain,fullchain,privkey}.pem``, where\n  ``$domain`` is the certificate name (see :ref:`where-certs`\n  for more details)\n* :ref:`CLI configuration files <config-file>`\n* Hook directories in ``/etc/letsencrypt/renewal-hooks``\n\nCertbot's behavior with other files may change at any point.\n\nAnother area where Certbot should not be considered stable is its behavior when\nnot run in non-interactive mode which also may change at any point.\n\nIn general, if we're making a change that we expect will break some users, we\nwill bump the major version and will have warned about it in a prior release\nwhen possible. For our Python API, we will issue warnings using Python's\nwarning module. For application level changes, we will print and log warning\nmessages.\n"
  },
  {
    "path": "certbot/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Certbot documentation build configuration file, created by\n# sphinx-quickstart on Sun Nov 23 20:35:21 2014.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\nimport codecs\nimport os\nimport re\nimport sys\n\nimport sphinx\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n# read version number (and other metadata) from package init\ninit_fn = os.path.join(here, '..', 'src', 'certbot', '__init__.py')\nwith codecs.open(init_fn, encoding='utf8') as fd:\n    meta = dict(re.findall(r\"\"\"__([a-z]+)__ = '([^']+)\"\"\", fd.read()))\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\nsys.path.insert(0, os.path.abspath(os.path.join(here, '..')))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\nneeds_sphinx = '1.2'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    'sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme',\n]\n\nif sphinx.version_info >= (1, 6):\n    extensions.append('sphinx.ext.imgconverter')\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix of source filenames.\nsource_suffix = '.rst'\n\n# The encoding of source files.\n#source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'Certbot'\n# this is now overridden by the footer.html template\ncopyright = u'2014-2018 - The Certbot software and documentation are licensed under the Apache 2.0 license as described at https://eff.org/cb-license.'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = '.'.join(meta['version'].split('.')[:2])\n# The full version, including alpha/beta/rc tags.\nrelease = meta['version']\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#today = ''\n# Else, today_fmt is used as the format for a strftime call.\n#today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\nexclude_patterns = [\n    '_build',\n    'challenges.rst',\n]\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\ndefault_role = 'py:obj'\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# A list of ignored prefixes for module index sorting.\n#modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n#keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\nsuppress_warnings = ['image.nonlocal_uri']\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n#html_theme_path = []\n\n# The name for this set of Sphinx documents.  If None, it defaults to\n# \"<project> v<release> documentation\".\n#html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#html_logo = None\n\n# The name of an image file (within the static path) to use as favicon of the\n# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = ['_static']\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#html_extra_path = []\n\n# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,\n# using the given strftime format.\n#html_last_updated_fmt = '%b %d, %Y'\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#html_additional_pages = {}\n\n# If false, no module index is generated.\n#html_domain_indices = True\n\n# If false, no index is generated.\n#html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n#html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'\n#html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# Now only 'ja' uses this config value\n#html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'Certbotdoc'\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #'preamble': '',\n\n    # Latex figure (float) alignment\n    #'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    ('index', 'Certbot.tex', u'Certbot Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#latex_use_parts = False\n\n# If true, show page references after internal links.\n#latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#latex_show_urls = False\n\n# Documents to append as an appendix to all manuals.\n#latex_appendices = []\n\n# If false, no module index is generated.\n#latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    ('index', 'certbot', u'Certbot Documentation',\n     [project], 7),\n    ('man/certbot', 'certbot', u\"Automatically configure HTTPS using Let's Encrypt\",\n     [project], 1),\n]\n\n# If true, show URL addresses after external links.\n#man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    ('index', 'Certbot', u'Certbot Documentation',\n     u'Certbot Project', 'Certbot', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n# Documents to append as an appendix to all manuals.\n#texinfo_appendices = []\n\n# If false, no module index is generated.\n#texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#texinfo_no_detailmenu = False\n\n\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n}\n"
  },
  {
    "path": "certbot/docs/contributing.rst",
    "content": "===============\nDeveloper Guide\n===============\n\n.. contents:: Table of Contents\n   :local:\n\n\n.. _getting_started:\n\nGetting Started\n===============\n\nCertbot has the same :ref:`system requirements <system_requirements>` when set\nup for development.  While the section below will help you install Certbot and\nits dependencies, Certbot needs to be run on a UNIX-like OS so if you're using\nWindows, you'll need to set up a (virtual) machine running an OS such as Linux\nand continue with these instructions on that UNIX-like OS.\n\nIf you're using macOS, it is recommended to first check out the `macOS\nsuggestions`_ section before continuing with the installation instructions\nbelow.\n\n.. _local copy:\n\nRunning a local copy of the client\n----------------------------------\n\nRunning the client in developer mode from your local tree is a little different\nthan running Certbot as a user. To get set up, clone our git repository by\nrunning:\n\n.. code-block:: shell\n\n   git clone https://github.com/certbot/certbot\n\nIf you're running on a UNIX-like OS, you can run the following commands to\ninstall dependencies and set up a virtual environment where you can run\nCertbot.\n\nInstall and configure the OS system dependencies required to run Certbot.\n\n.. code-block:: shell\n\n   # For APT-based distributions (e.g. Debian, Ubuntu ...)\n   sudo apt update\n   sudo apt install python3-dev python3-venv libaugeas-dev gcc\n   # For RPM-based distributions (e.g. Fedora, CentOS ...)\n   # NB1: old distributions will use yum instead of dnf\n   # NB2: RHEL-based distributions use python3X instead of python3 (e.g. python38)\n   sudo dnf install python3 python3-devel augeas-devel gcc\n   # For macOS installations with Homebrew already installed and configured\n   # NB: CFLAGS are needed to compile and link to Augeas installed through\n   #     Homebrew and some of our developer scripts expect GNU coreutils be first in\n   #     your PATH. The commands below set this up for bash and zsh, but your\n   #     instructions may be slightly different if you use an alternate shell.\n   brew install augeas coreutils gnu-sed\n   BREW_PREFIX=$(brew --prefix)\n   RC_LINES=\"export CFLAGS=\\\"\\$CFLAGS -I$BREW_PREFIX/include -L$BREW_PREFIX/lib\\\"\\n\"\n   RC_LINES+=\"export PATH=\\\"$BREW_PREFIX/opt/coreutils/libexec/gnubin:\"\n   RC_LINES+=\"$BREW_PREFIX/opt/gnu-sed/libexec/gnubin:\\$PATH\\\"\\n\"\n   printf \"$RC_LINES\" >> ~/.bashrc  # for bash\n   printf \"$RC_LINES\" >> ~/.zshrc  # for zsh\n\n.. note:: If you have trouble creating the virtual environment below, you may\n   need to install additional dependencies. See the `cryptography project's\n   site`_ for more information.\n\n.. _`cryptography project's site`: https://cryptography.io/en/latest/installation.html#building-cryptography-on-linux\n\nSet up the Python virtual environment that will host your Certbot local instance.\n\n.. code-block:: shell\n\n   cd certbot\n   python3 tools/venv.py\n\n.. note:: You may need to repeat this when\n  Certbot's dependencies change or when a new plugin is introduced.\n\nYou can now run the copy of Certbot from git either by executing\n``venv/bin/certbot``, or by activating the virtual environment. You can do the\nlatter by running:\n\n.. code-block:: shell\n\n   source venv/bin/activate\n\nAfter running this command, ``certbot`` and development tools like ``ipdb3``,\n``ipython``, ``pytest``, and ``tox`` are available in the shell where you ran\nthe command. These tools are installed in the virtual environment and are kept\nseparate from your global Python installation. This works by setting\nenvironment variables so the right executables are found and Python can pull in\nthe versions of various packages needed by Certbot.  More information can be\nfound in the `virtualenv docs`_.\n\n.. _`virtualenv docs`: https://virtualenv.pypa.io\n\nFind issues to work on\n----------------------\n\nYou can find the open issues in the `github issue tracker`_. If you're starting\nwork on something, post a comment to let others know and seek feedback on your\nplan where appropriate.\n\nOnce you've got a working branch, you can open a pull request.  All changes in\nyour pull request must have thorough unit test coverage, pass our\ntests, and be compliant with the :ref:`coding style <coding-style>`.\n\n.. _github issue tracker: https://github.com/certbot/certbot/issues\n\n.. _testing:\n\nTesting\n-------\n\nYou can test your code in several ways:\n\n- running the `automated unit`_ tests,\n- running the `automated integration`_ tests\n- running an *ad hoc* `manual integration`_ test\n\n.. _automated unit:\n\nRunning automated unit tests\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nTo run all unittests, mypy, and lint:\n\n.. code-block:: shell\n\n    tox\n\nIf you're working on a specific test and would like to run just that one:\n\n.. code-block:: shell\n\n    pytest acme/src/acme/_internal/tests/messages_test.py # Use the test file you're working on\n\nTo run a specific test case within a file:\n\n.. code-block:: shell\n\n    pytest acme/src/acme/_internal/tests/messages_test.py -k test_to_partial_json\n\nFor debugging, we recommend putting\n``import ipdb; ipdb.set_trace()`` statements inside the source code, which will require\nadding the `-s` flag to `pytest` invocations.\n\n.. warning:: The full test suite may attempt to modify your system's Apache\n  config if your user has sudo permissions, so it should not be run on a\n  production Apache server.\n\n.. _automated integration:\n\nRunning automated integration tests\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nGenerally it is sufficient to open a pull request and let Github and Azure Pipelines run\nintegration tests for you. However, you may want to run them locally before submitting\nyour pull request. You need Docker installed and working.\n\nThe tox environment `integration` will setup `Pebble`_, the Let's Encrypt ACME CA server\nfor integration testing, then launch the Certbot integration tests.\n\nWith a user allowed to access your local Docker daemon, run:\n\n.. code-block:: shell\n\n  tox run -e integration\n\nTests will be run using pytest. A test report and a code coverage report will be\ndisplayed at the end of the integration tests execution.\n\n.. _Pebble: https://github.com/letsencrypt/pebble\n\n.. _manual integration:\n\nRunning manual integration tests\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nYou can also manually execute Certbot against a local instance of the `Pebble`_ ACME server.\nThis is useful to verify that the modifications done to the code makes Certbot behave as expected.\n\nTo do so you need:\n\n- Docker installed, and a user with access to the Docker client,\n- an available `local copy`_ of Certbot.\n\nThe virtual environment set up with `python3 tools/venv.py` contains two CLI tools\nthat can be used once the virtual environment is activated:\n\n.. code-block:: shell\n\n    run_acme_server\n\n- Starts a local instance of Pebble and runs in the foreground printing its logs.\n- Press CTRL+C to stop this instance.\n- This instance is configured to validate challenges against certbot executed locally.\n\n.. note:: Some options are available to tweak the local ACME server. You can execute\n    ``run_acme_server --help`` to see the inline help of the ``run_acme_server`` tool.\n\n.. code-block:: shell\n\n    certbot_test [ARGS...]\n\n- Execute certbot with the provided arguments and other arguments useful for testing purposes,\n  such as: verbose output, full tracebacks in case Certbot crashes, *etc.*\n- Execution is preconfigured to interact with the Pebble CA started with ``run_acme_server``.\n- Any arguments can be passed as they would be to Certbot (eg. ``certbot_test certonly -d test.example.com``).\n\nHere is a typical workflow to verify that Certbot successfully issued a certificate\nusing an HTTP-01 challenge on a machine with Python 3:\n\n.. code-block:: shell\n\n    python3 tools/venv.py\n    source venv/bin/activate\n    run_acme_server &\n    certbot_test certonly --standalone -d test.example.com\n    # To stop Pebble, launch `fg` to get back the background job, then press CTRL+C\n\nRunning tests in CI\n~~~~~~~~~~~~~~~~~~~\n\nCertbot uses Azure Pipelines to run continuous integration tests. If you are using our\nAzure setup, a branch whose name starts with `test-` will run all tests on that branch.\n\nCode components and layout\n==========================\n\nThe following components of the Certbot repository are distributed to users:\n\nacme\n  contains all protocol specific code\ncertbot\n  main client code\ncertbot-apache and certbot-nginx\n  client code to configure specific web servers\ncertbot-dns-*\n  client code to configure DNS providers\n\nPlugin-architecture\n-------------------\n\nCertbot has a plugin architecture to facilitate support for\ndifferent webservers, other TLS servers, and operating systems.\nThe interfaces available for plugins to implement are defined in\n`interfaces.py`_ and `plugins/common.py`_.\n\nThe main two plugin interfaces are `~certbot.interfaces.Authenticator`, which\nimplements various ways of proving domain control to a certificate authority,\nand `~certbot.interfaces.Installer`, which configures a server to use a\ncertificate once it is issued. Some plugins, like the built-in Apache and Nginx\nplugins, implement both interfaces and perform both tasks. Others, like the\nbuilt-in Standalone authenticator, implement just one interface.\n\n.. _interfaces.py: https://github.com/certbot/certbot/blob/main/certbot/src/certbot/interfaces.py\n.. _plugins/common.py: https://github.com/certbot/certbot/blob/main/certbot/src/certbot/plugins/common.py#L45\n\n\nAuthenticators\n--------------\n\nAuthenticators are plugins that prove control of a domain name by solving a\nchallenge provided by the ACME server. ACME currently defines two types of\nchallenges: HTTP and DNS, represented by classes in `acme.challenges`.\nAn authenticator plugin should implement support for at least one challenge type.\n\nAn Authenticator indicates which challenges it supports by implementing\n`get_chall_pref(identifier)` to return a sorted list of challenge types in\npreference order.\n\nAn Authenticator must also implement `perform(achalls)`, which \"performs\" a list\nof challenges by, for instance, provisioning a file on an HTTP server, or\nsetting a TXT record in DNS. Once all challenges have succeeded or failed,\nCertbot will call the plugin's `cleanup(achalls)` method to remove any files or\nDNS records that were needed only during authentication.\n\nInstaller\n---------\n\nInstallers plugins exist to actually setup the certificate in a server,\npossibly tweak the security configuration to make it more correct and secure\n(Fix some mixed content problems, turn on HSTS, redirect to HTTPS, etc).\nInstaller plugins tell the main client about their abilities to do the latter\nvia the :meth:`~.Installer.supported_enhancements` call. We currently\nhave two Installers in the tree, the `~.ApacheConfigurator`. and the\n`~.NginxConfigurator`.  External projects have made some progress toward\nsupport for IIS, Icecast and Plesk.\n\nInstallers and Authenticators will oftentimes be the same class/object\n(because for instance both tasks can be performed by a webserver like nginx)\nthough this is not always the case (the standalone plugin is an authenticator\nthat listens on port 80, but it cannot install certificates; a postfix plugin\nwould be an installer but not an authenticator).\n\nInstallers and Authenticators are kept separate because\nit should be possible to use the `~.StandaloneAuthenticator` (it sets\nup its own Python server to perform challenges) with a program that\ncannot solve challenges itself (Such as MTA installers).\n\n\nInstaller Development\n---------------------\n\nThere are a few existing classes that may be beneficial while\ndeveloping a new `~certbot.interfaces.Installer`.\nInstallers aimed to reconfigure UNIX servers may use Augeas for\nconfiguration parsing and can inherit from `~.AugeasConfigurator` class\nto handle much of the interface. Installers that are unable to use\nAugeas may still find the `~.Reverter` class helpful in handling\nconfiguration checkpoints and rollback.\n\n\n.. _dev-plugin:\n\nWriting your own plugin\n-----------------------\n\n.. note:: The Certbot team is not currently accepting any new plugins\n    because we want to rethink our approach to the challenge and resolve some\n    issues like `#6464 <https://github.com/certbot/certbot/issues/6464>`_,\n    `#6503 <https://github.com/certbot/certbot/issues/6503>`_, and `#6504\n    <https://github.com/certbot/certbot/issues/6504>`_ first.\n\n    In the meantime, you're welcome to release it as a third-party plugin. See\n    `certbot-dns-ispconfig <https://github.com/m42e/certbot-dns-ispconfig>`_\n    for one example of that.\n\nCertbot client supports dynamic discovery of plugins through the\n`importlib.metadata entry points`_ using the `certbot.plugins` group.\nThis way you can, for example, create a custom implementation of\n`~certbot.interfaces.Authenticator` or the\n`~certbot.interfaces.Installer` without having to merge it\nwith the core upstream source code. An example is provided in\n``examples/plugins/`` directory.\n\nWhile developing, you can install your plugin into a Certbot development\nvirtualenv like this:\n\n.. code-block:: shell\n\n  . venv/bin/activate\n  pip install -e examples/plugins/\n  certbot_test plugins\n\nYour plugin should show up in the output of the last command. If not,\nit was not installed properly.\n\nOnce you've finished your plugin and published it, you can have your\nusers install it system-wide with `pip install`. Note that this will\nonly work for users who have Certbot installed from OS packages or via\npip.\n\n.. _`importlib.metadata entry points`:\n    https://importlib-metadata.readthedocs.io/en/latest/using.html#entry-points\n\nWriting your own plugin snap\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you'd like your plugin to be used alongside the Certbot snap, you\nwill also have to publish your plugin as a snap. Plugin snaps are\nregular confined snaps, but normally do not provide any \"apps\"\nthemselves. Plugin snaps export loadable Python modules to the Certbot\nsnap.\n\nWhen the Certbot snap runs, it will use its version of Python and prefer\nPython modules contained in its own snap over modules contained in\nexternal snaps. This means that your snap doesn't have to contain things\nlike an extra copy of Python, Certbot, or their dependencies, but also\nthat if you need a different version of a dependency than is already\ninstalled in the Certbot snap, the Certbot snap will have to be updated.\n\nCertbot plugin snaps expose their Python modules to the Certbot snap via a\n`snap content interface`_ where ``certbot-1`` is the value for the ``content``\nattribute. The Certbot snap only uses this to find the names of connected\nplugin snaps and it expects to find the Python modules to be loaded under\n``lib/python3.12/site-packages/`` in the plugin snap. This location is the\ndefault when using the ``core24`` `base snap`_ and the `python snapcraft\nplugin`_.\n\nThe Certbot snap also provides a separate content interface which\nyou can use to get metadata about the Certbot snap using the ``content``\nidentifier ``metadata-1``.\n\nThe script used to generate the snapcraft.yaml files for our own externally\nsnapped plugins can be found at\nhttps://github.com/certbot/certbot/blob/main/tools/snap/generate_dnsplugins_snapcraft.sh.\n\nFor more information on building externally snapped plugins, see the section on\n:ref:`Building snaps`.\n\nOnce you have created your own snap, if you have the snap file locally,\nit can be installed for use with Certbot by running:\n\n.. code-block:: shell\n\n    snap install --classic certbot\n    snap set certbot trust-plugin-with-root=ok\n    snap install --dangerous your-snap-filename.snap\n    sudo snap connect certbot:plugin your-snap-name\n    sudo /snap/bin/certbot plugins\n\nIf everything worked, the last command should list your plugin in the\nlist of plugins found by Certbot. Once your snap is published to the\nsnap store, it will be installable through the name of the snap on the\nsnap store without the ``--dangerous`` flag. If you are also using\nCertbot's metadata interface, you can run ``sudo snap connect\nyour-snap-name:your-plug-name-for-metadata certbot:certbot-metadata`` to\nconnect your snap to it.\n\n.. _`snap content interface`:\n    https://snapcraft.io/docs/content-interface\n.. _`base snap`:\n    https://snapcraft.io/docs/base-snaps\n.. _`python snapcraft plugin`:\n    https://snapcraft.io/docs/python-plugin\n\n.. _coding-style:\n\nCoding style\n============\n\nPlease:\n\n1. **Be consistent with the rest of the code**.\n\n2. Read `PEP 8 - Style Guide for Python Code`_.\n\n3. Follow the `Google Python Style Guide`_, with the exception that we\n   use `Sphinx-style`_ documentation::\n\n        def foo(arg):\n            \"\"\"Short description.\n\n            :param int arg: Some number.\n\n            :returns: Argument\n            :rtype: int\n\n            \"\"\"\n            return arg\n\n4. Remember to use ``pylint``.\n\n5. You may consider installing a plugin for `editorconfig`_ in\n   your editor to prevent some linting warnings.\n\n6. Please avoid `unittest.assertTrue` or `unittest.assertFalse` when\n   possible, and use `assertEqual` or more specific assert. They give\n   better messages when it's failing, and are generally more correct.\n\n.. _Google Python Style Guide:\n  https://google.github.io/styleguide/pyguide.html\n.. _Sphinx-style: https://www.sphinx-doc.org/\n.. _PEP 8 - Style Guide for Python Code:\n  https://www.python.org/dev/peps/pep-0008\n.. _editorconfig: https://editorconfig.org/\n\nUse ``certbot.compat.os`` instead of ``os``\n===========================================\n\nPython's standard library ``os`` module lacks full support for several Windows\nsecurity features about file permissions (eg. DACLs). However several files\nhandled by Certbot (eg. private keys) need strongly restricted access\non both Linux and Windows.\n\nTo help with this, the ``certbot.compat.os`` module wraps the standard\n``os`` module, and forbids usage of methods that lack support for these Windows\nsecurity features.\n\nAs a developer, when working on Certbot or its plugins, you must use ``certbot.compat.os``\nin every place you would need ``os`` (eg. ``from certbot.compat import os`` instead of\n``import os``). Otherwise the tests will fail when your PR is submitted.\n\n.. _type annotations:\n\nMypy type annotations\n=====================\n\nCertbot uses the `mypy`_ static type checker. Python 3 natively supports official type annotations,\nwhich can then be tested for consistency using mypy. Mypy does some type checks even without type\nannotations; we can find bugs in Certbot even without a fully annotated codebase.\n\nZulip wrote a `great guide`_ to using mypy. It’s useful, but you don’t have to read the whole thing\nto start contributing to Certbot.\n\nTo run mypy on Certbot, use ``tox run -e mypy`` on a machine that has Python 3 installed.\n\nAlso note that OpenSSL, which we rely on, has type definitions for crypto but not SSL. We use both.\nThose imports should look like this:\n\n.. code-block:: python\n\n  from OpenSSL import crypto\n  from OpenSSL import SSL\n\n.. _mypy: https://mypy.readthedocs.io\n.. _added in comments: https://mypy.readthedocs.io/en/latest/cheat_sheet.html\n.. _great guide: https://blog.zulip.org/2016/10/13/static-types-in-python-oh-mypy/\n\nSubmitting a pull request\n=========================\n\nSteps:\n\n0. We recommend you talk with us in a GitHub issue or :ref:`Mattermost <ask for\n   help>` before writing a pull request to ensure the changes you're making is\n   something we have the time and interest to review.\n1. Write your code! When doing this, you should add :ref:`mypy type annotations\n   <type annotations>` for any functions you add or modify. You can check that\n   you've done this correctly by running ``tox run -e mypy`` on a machine that has\n   Python 3 installed.\n2. Make sure your environment is set up properly and that you're in your\n   virtualenv. You can do this by following the instructions in the\n   :ref:`Getting Started <getting_started>` section.\n3. Run ``tox run -e lint`` to check for pylint errors. Fix any errors.\n4. Run ``tox --skip-missing-interpreters`` to run all the tests we recommend\n   developers run locally. The ``--skip-missing-interpreters`` argument ignores\n   missing versions of Python needed for running the tests. Fix any errors.\n5. If any documentation should be added or updated as part of the changes you\n   have made, please include the documentation changes in your PR.\n6. Submit the PR. Once your PR is open, please do not force push to the branch\n   containing your pull request to squash or amend commits. We use `squash\n   merges <https://github.com/blog/2141-squash-your-commits>`_ on PRs and\n   rewriting commits makes changes harder to track between reviews.\n7. Did your tests pass on Azure Pipelines? If they didn't, fix any errors.\n\n.. _ask for help:\n\nAsking for help\n===============\n\nIf you have any questions while working on a Certbot issue, don't hesitate to\nask for help! You can do this in the Certbot channel in EFF's Mattermost\ninstance for its open source projects as described below.\n\nYou can get involved with several of EFF's software projects such as Certbot at\nthe `EFF Open Source Contributor Chat Platform\n<https://opensource.eff.org/signup_user_complete/?id=6iqur37ucfrctfswrs14iscobw>`_.\nBy signing up for the EFF Open Source Contributor Chat Platform, you consent to\nshare your personal information with the Electronic Frontier Foundation, which\nis the operator and data controller for this platform. The channels will be\navailable both to EFF, and to other users of EFFOSCCP, who may use or disclose\ninformation in these channels outside of EFFOSCCP. EFF will use your\ninformation, according to the `Privacy Policy <https://www.eff.org/policy>`_,\nto further the mission of EFF, including hosting and moderating the discussions\non this platform.\n\nUse of EFFOSCCP is subject to the `EFF Code of Conduct\n<https://www.eff.org/pages/eppcode>`_. When investigating an alleged Code of\nConduct violation, EFF may review discussion channels or direct messages.\n\n.. _Building snaps:\n\nBuilding the Certbot and DNS plugin snaps\n=========================================\n\nInstructions for how to manually build and run the Certbot snap and the externally\nsnapped DNS plugins that the Certbot project supplies are located in the README\nfile at https://github.com/certbot/certbot/tree/main/tools/snap.\n\nUpdating the documentation\n==========================\n\nMany of the packages in the Certbot repository have documentation in a\n``docs/`` directory. This directory is located under the top level directory\nfor the package. For instance, Certbot's documentation is under\n``certbot/docs``.\n\nTo build the documentation of a package, make sure you have followed the\ninstructions to set up a `local copy`_ of Certbot including activating the\nvirtual environment. After that, ``cd`` to the docs directory you want to build\nand run the command:\n\n.. code-block:: shell\n\n   make clean html\n\nThis would generate the HTML documentation in ``_build/html`` in your current\n``docs/`` directory.\n\nCertbot's dependencies\n======================\n\nWe attempt to pin all of Certbot's dependencies whenever we can for reliability\nand consistency. Some of the places we have Certbot's dependencies pinned\ninclude our snaps, Docker images, CI, and our development environments.\n\nIn most cases, the file where dependency versions are specified is\n``tools/requirements.txt``. The one exception to this is our \"oldest\" tests\nwhere ``tools/oldest_constraints.txt`` is used instead. The purpose of the\n\"oldest\" tests is to ensure Certbot continues to work with the oldest versions\nof our dependencies which we claim to support. The oldest versions of the\ndependencies we support should also be declared in our setup.py files to\ncommunicate this information to our users.\n\nThe choices of whether Certbot's dependencies are pinned and what file is used\nif they are should be automatically handled for you most of the time by\nCertbot's tooling. The way it works though is ``tools/pip_install.py`` (which\nmany of our other tools build on) checks for the presence of environment\nvariables. If ``CERTBOT_OLDEST`` is set to 1, ``tools/oldest_constraints.txt``\nwill be used as constraints for ``pip``, otherwise, ``tools/requirements.txt``\nis used as constraints.\n\nUpdating dependency versions\n----------------------------\n\n``tools/requirements.txt`` and ``tools/oldest_constraints.txt`` can be updated\nusing ``tools/pinning/current/repin.sh`` and ``tools/pinning/oldest/repin.sh``\nrespectively. This works by using ``poetry`` to generate pinnings based on a\nPoetry project defined by the ``pyproject.toml`` file in the same directory as\nthe script. In many cases, you can just run the script to generate updated\ndependencies, however, if you need to pin back packages or unpin packages that\nwere previously restricted to an older version, you will need to modify the\n``pyproject.toml`` file. The syntax used by this file is described at\nhttps://python-poetry.org/docs/pyproject/ and how dependencies are specified in\nthis file is further described at\nhttps://python-poetry.org/docs/dependency-specification/.\n\nIf you want to learn more about the design used here, see\n``tools/pinning/DESIGN.md`` in the Certbot repo.\n\nChoosing dependency versions\n----------------------------\n\nWhen choosing dependency versions, we should choose whatever minimum versions\nsimplify development of Certbot and our own distribution methods such as snaps,\npip, and docker. Since these approaches have full access to PyPI, it's OK if\nthe required packages declared in ``setup.py`` are quite new.\n\nIf this approach to development creates significant trouble for some of our users, we\ncan revisit this decision and weigh their trouble against the difficulties\ninvolved in maintaining support for a wider range of package versions. When\ndoing this, we should also be sure to consider the feasibility of users getting\naccess to these newer packages on their system rather than changing our own\napproach here. Their OS distribution may be able to package it, especially in\nan alternate repository and/or for a different version of Python to help avoid\nconflicts with other packages on their system.\n\nmacOS suggestions\n=================\n\nIf you're developing on macOS, before :ref:`setting up your Certbot development\nenvironment <local copy>`, it is recommended you perform the following steps.\nNone of this is required, but it is the approach used by all/most of the\ncurrent Certbot developers on macOS as of writing this:\n\n0. Install `Homebrew <https://brew.sh/>`_. It is the most popular package\n   manager on macOS by a wide margin and works well enough.\n1. Install `pyenv <https://github.com/pyenv/pyenv>`_, ideally through Homebrew\n   by running ``brew install pyenv``. Using Homebrew's Python for Certbot\n   development is annoying because it regularly updates and every time it does\n   it breaks your virtual environments. Using Python from ``pyenv`` avoids this\n   problem and gives you easy access to all versions of Python.\n2. If you're using ``pyenv``, make sure you've set up your shell for it by\n   following instructions like\n   https://github.com/pyenv/pyenv?tab=readme-ov-file#set-up-your-shell-environment-for-pyenv.\n"
  },
  {
    "path": "certbot/docs/index.rst",
    "content": "Welcome to the Certbot documentation!\n==================================================\n\n.. toctree::\n   :maxdepth: 2\n\n   intro\n   what\n   install\n   using\n   contributing\n   packaging\n   compatibility\n   resources\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot/docs/install.rst",
    "content": "=====================\nGet Certbot\n=====================\n\n.. contents:: Table of Contents\n   :local:\n\n\n.. _system_requirements:\n\nSystem Requirements\n-------------------\n- Linux, macOS, BSD and Windows\n- Recommended root access on Linux/BSD/Required Administrator access on Windows\n- Port 80 Open\n\n.. Note:: Certbot is most useful when run with root privileges, because it is then able to automatically configure TLS/SSL for Apache and nginx. \\\n   \n   *Certbot is meant to be run directly on a web server*, normally by a system administrator. In most cases, running Certbot on your personal computer is not a useful option. The instructions below relate to installing and running Certbot on a server.\n\nInstallation\n------------\n\nUnless you have very specific requirements, we kindly suggest that you use the installation instructions for your system found at https://certbot.eff.org/instructions.\n\n.. _snap-install:\n\nSnap (Recommended)\n------------------\nOur instructions are the same across all systems that use Snap. You can find instructions for installing Certbot through Snap can be found at https://certbot.eff.org/instructions by selecting your server software and then choosing \"snapd\" in the \"System\" dropdown menu.\n\nMost modern Linux distributions (basically any that use systemd) can install Certbot packaged as a snap. Snaps are available for x86_64, ARMv7 and ARMv8 architectures. The Certbot snap provides an easy way to ensure you have the latest version of Certbot with features like automated certificate renewal preconfigured.\n\nIf you unable to use snaps, you can use an alternate method for installing ``certbot``.\n\n\n.. _docker-user:\n\nAlternative 1: Docker\n---------------------\n\nDocker_ is an amazingly simple and quick way to obtain a\ncertificate. However, this mode of operation is unable to install\ncertificates or configure your webserver, because our installer\nplugins cannot reach your webserver from inside the Docker container.\n\nMost users should use the instructions at certbot.eff.org_. You should only use Docker if you are sure you know what you are doing and have a good reason to do so.\n\nYou should definitely read the :ref:`where-certs` section, in order to\nknow how to manage the certificates\nmanually. `Our ciphersuites page <ciphers.html>`__\nprovides some information about recommended ciphersuites. If none of\nthese make much sense to you, you should definitely use the installation method\nrecommended for your system at certbot.eff.org_, which enables you to use\ninstaller plugins that cover both of those hard topics.\n\nIf you're still not convinced and have decided to use this method, from\nthe server that the domain you're requesting a certificate for resolves\nto, `install Docker`_, then issue a command like the one found below. If\nyou are using Certbot with the :ref:`Standalone` plugin, you will need\nto make the port it uses accessible from outside of the container by\nincluding something like ``-p 80:80`` or ``-p 443:443`` on the command\nline before ``certbot/certbot``.\n\n.. code-block:: shell\n\n   sudo docker run -it --rm --name certbot \\\n               -v \"/etc/letsencrypt:/etc/letsencrypt\" \\\n               -v \"/var/lib/letsencrypt:/var/lib/letsencrypt\" \\\n               certbot/certbot certonly\n\nRunning Certbot with the ``certonly`` command will obtain a certificate and place it in the directory\n``/etc/letsencrypt/live`` on your system. Because Certonly cannot install the certificate from\nwithin Docker, you must install the certificate manually according to the procedure\nrecommended by the provider of your webserver.\n\nThere are also Docker images for each of Certbot's DNS plugins available\nat https://hub.docker.com/u/certbot which automate doing domain\nvalidation over DNS for popular providers. To use one, just replace\n``certbot/certbot`` in the command above with the name of the image you\nwant to use. For example, to use Certbot's plugin for Amazon Route 53,\nyou'd use ``certbot/dns-route53``. You may also need to add flags to\nCertbot and/or mount additional directories to provide access to your\nDNS API credentials as specified in the :ref:`DNS plugin documentation\n<dns_plugins>`.\n\nFor more information about the layout\nof the ``/etc/letsencrypt`` directory, see :ref:`where-certs`.\n\n.. _Docker: https://docker.com\n.. _`install Docker`: https://docs.docker.com/engine/installation/\n.. _certbot.eff.org: https://certbot.eff.org/instructions\n\n\n.. _pip:\n\nAlternative 2: Pip\n------------------\n\nInstalling Certbot through pip is only supported on a best effort basis and\nwhen using a virtual environment. Instructions for installing Certbot through\npip can be found at https://certbot.eff.org/instructions by selecting your\nserver software and then choosing \"pip\" in the \"System\" dropdown menu.\n\n\n.. _third-party:\n\nAlternative 3: Third Party Distributions\n----------------------------------------\n\nThird party distributions exist for other specific needs. They often are maintained\nby these parties outside of Certbot and tend to rapidly fall out of date on LTS-style distributions.\n\n\n.. _certbot-auto:\n\nCertbot-Auto [Deprecated]\n-------------------------\n.. toctree::\n   :hidden:\n\nWe used to have a shell script named ``certbot-auto`` to help people install\nCertbot on UNIX operating systems, however, this script is no longer supported.\n\nPlease remove ``certbot-auto``. To do so, you need to do three things:\n\n1. If you added a cron job or systemd timer to automatically run certbot-auto to renew your certificates, you should delete it. If you did this by following our instructions, you can delete the entry added to `/etc/crontab` by running a command like `sudo sed -i '/certbot-auto/d' /etc/crontab`.\n2. Delete the certbot-auto script. If you placed it in `/usr/local/bin`` like we recommended, you can delete it by running `sudo rm /usr/local/bin/certbot-auto`.\n3. Delete the Certbot installation created by certbot-auto by running `sudo rm -rf /opt/eff.org`.\n"
  },
  {
    "path": "certbot/docs/intro.rst",
    "content": "=====================\nIntroduction\n=====================\n\n.. note::\n    To get started quickly, use the `interactive installation guide <https://certbot.eff.org>`_.\n\n.. include:: ../README.rst\n    :start-after: tag:intro-begin\n    :end-before: tag:intro-end\n"
  },
  {
    "path": "certbot/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset BUILDDIR=_build\r\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .\r\nset I18NSPHINXOPTS=%SPHINXOPTS% .\r\nif NOT \"%PAPER%\" == \"\" (\r\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\r\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\nif \"%1\" == \"help\" (\r\n\t:help\r\n\techo.Please use `make ^<target^>` where ^<target^> is one of\r\n\techo.  html       to make standalone HTML files\r\n\techo.  dirhtml    to make HTML files named index.html in directories\r\n\techo.  singlehtml to make a single large HTML file\r\n\techo.  pickle     to make pickle files\r\n\techo.  json       to make JSON files\r\n\techo.  htmlhelp   to make HTML files and a HTML help project\r\n\techo.  qthelp     to make HTML files and a qthelp project\r\n\techo.  devhelp    to make HTML files and a Devhelp project\r\n\techo.  epub       to make an epub\r\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\r\n\techo.  text       to make text files\r\n\techo.  man        to make manual pages\r\n\techo.  texinfo    to make Texinfo files\r\n\techo.  gettext    to make PO message catalogs\r\n\techo.  changes    to make an overview over all changed/added/deprecated items\r\n\techo.  xml        to make Docutils-native XML files\r\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\r\n\techo.  linkcheck  to check all external links for integrity\r\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\r\n\techo.  coverage   to run coverage check of the documentation if enabled\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"clean\" (\r\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\r\n\tdel /q /s %BUILDDIR%\\*\r\n\tgoto end\r\n)\r\n\r\n\r\nREM Check if sphinx-build is available and fallback to Python version if any\r\n%SPHINXBUILD% 2> nul\r\nif errorlevel 9009 goto sphinx_python\r\ngoto sphinx_ok\r\n\r\n:sphinx_python\r\n\r\nset SPHINXBUILD=python -m sphinx.__init__\r\n%SPHINXBUILD% 2> nul\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n:sphinx_ok\r\n\r\n\r\nif \"%1\" == \"html\" (\r\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/html.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"dirhtml\" (\r\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"singlehtml\" (\r\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pickle\" (\r\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the pickle files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"json\" (\r\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can process the JSON files.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"htmlhelp\" (\r\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run HTML Help Workshop with the ^\r\n.hhp project file in %BUILDDIR%/htmlhelp.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"qthelp\" (\r\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\r\n.qhcp project file in %BUILDDIR%/qthelp, like this:\r\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\LetsEncrypt.qhcp\r\n\techo.To view the help file:\r\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\LetsEncrypt.ghc\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"devhelp\" (\r\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"epub\" (\r\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The epub file is in %BUILDDIR%/epub.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latex\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished; the LaTeX files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdf\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"latexpdfja\" (\r\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\r\n\tcd %BUILDDIR%/latex\r\n\tmake all-pdf-ja\r\n\tcd %~dp0\r\n\techo.\r\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"text\" (\r\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The text files are in %BUILDDIR%/text.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"man\" (\r\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The manual pages are in %BUILDDIR%/man.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"texinfo\" (\r\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"gettext\" (\r\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The message catalogs are in %BUILDDIR%/locale.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"changes\" (\r\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.The overview file is in %BUILDDIR%/changes.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"linkcheck\" (\r\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Link check complete; look for any errors in the above output ^\r\nor in %BUILDDIR%/linkcheck/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"doctest\" (\r\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of doctests in the sources finished, look at the ^\r\nresults in %BUILDDIR%/doctest/output.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"coverage\" (\r\n\t%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Testing of coverage in the sources finished, look at the ^\r\nresults in %BUILDDIR%/coverage/python.txt.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"xml\" (\r\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The XML files are in %BUILDDIR%/xml.\r\n\tgoto end\r\n)\r\n\r\nif \"%1\" == \"pseudoxml\" (\r\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\r\n\tif errorlevel 1 exit /b 1\r\n\techo.\r\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.\r\n\tgoto end\r\n)\r\n\r\n:end\r\n"
  },
  {
    "path": "certbot/docs/man/certbot.rst",
    "content": ":orphan:\n\n=======\ncertbot\n=======\n\nSynopsis\n========\nThe objective of Certbot, Let's Encrypt, and the ACME (Automated Certificate Management\nEnvironment) protocol is to make it possible to set up an HTTPS server and have it automatically\nobtain a browser-trusted certificate, without any human intervention. This is accomplished by\nrunning a certificate management agent on the web server.\n\nThis agent is used to:\n\n- Automatically prove to the Let's Encrypt CA that you control the website\n- Obtain a browser-trusted certificate and set it up on your web server\n- Keep track of when your certificate is going to expire, and renew it\n- Help you revoke the certificate if that ever becomes necessary.\n\nOptions\n=======\n.. literalinclude:: ../cli-help.txt"
  },
  {
    "path": "certbot/docs/packaging.rst",
    "content": "===============\nPackaging Guide\n===============\n\nReleases\n========\n\nWe release packages and upload them to PyPI (wheels and source tarballs).\n\n- https://pypi.org/project/acme/\n- https://pypi.org/project/certbot/\n- https://pypi.org/project/certbot-apache/\n- https://pypi.org/project/certbot-nginx/\n- https://pypi.org/project/certbot-dns-cloudflare/\n- https://pypi.org/project/certbot-dns-digitalocean/\n- https://pypi.org/project/certbot-dns-dnsimple/\n- https://pypi.org/project/certbot-dns-dnsmadeeasy/\n- https://pypi.org/project/certbot-dns-google/\n- https://pypi.org/project/certbot-dns-linode/\n- https://pypi.org/project/certbot-dns-luadns/\n- https://pypi.org/project/certbot-dns-nsone/\n- https://pypi.org/project/certbot-dns-ovh/\n- https://pypi.org/project/certbot-dns-rfc2136/\n- https://pypi.org/project/certbot-dns-route53/\n\nThe following scripts are used in the process:\n\n- https://github.com/certbot/certbot/blob/main/tools/release.sh\n\nWe use git tags to identify releases, using `Semantic Versioning`_. For\nexample: `v0.11.1`.\n\n.. _`Semantic Versioning`: https://semver.org/\n\nSince version 1.21.0, our packages are cryptographically signed by one of four\nPGP keys:\n\n- ``BF6BCFC89E90747B9A680FD7B6029E8500F7DB16``\n- ``86379B4F0AF371B50CD9E5FF3402831161D1D280``\n- ``20F201346BF8F3F455A73F9A780CC99432A28621``\n- ``F2871B4152AE13C49519111F447BF683AA3B26C3```\n\nThese keys can be found on major key servers.\n\nReleases before 1.21.0 were signed by the PGP key\n``A2CFB51FA275A7286234E7B24D17C995CD9775F2`` which can still be found on major\nkey servers.\n\nNotes for package maintainers\n=============================\n\n0. Please use our tagged releases, not ``main``!\n\n1. Do not package ``certbot-compatibility-test`` as it's only used internally.\n\n2. To run tests on our packages, you should use pytest by running the command ``python -m pytest``. Running ``pytest`` directly may not work because PYTHONPATH is not handled the same way and local modules may not be found by the test runner.\n\n3. If you'd like to include automated renewal in your package:\n\n  - ``certbot renew -q`` should be added to crontab or systemd timer.\n  - A random per-machine time offset should be included to avoid having a large number of your clients hit Let's Encrypt's servers simultaneously.\n  - ``--preconfigured-renewal`` should be included on the CLI or in ``cli.ini`` for all invocations of Certbot, so that it can adjust its interactive output regarding automated renewal (Certbot >= 1.9.0).\n\n4. ``jws`` is an internal script for ``acme`` module and it doesn't have to be packaged - it's mostly for debugging: you can use it as ``echo foo | jws sign | jws verify``.\n\n5. Do get in touch with us. We are happy to make any changes that will make packaging easier. If you need to apply some patches don't do it downstream - make a PR here.\n"
  },
  {
    "path": "certbot/docs/resources.rst",
    "content": "=====================\nResources\n=====================\n\n.. include:: ../README.rst\n    :start-after: tag:links-begin\n    :end-before: tag:links-end\n"
  },
  {
    "path": "certbot/docs/using.rst",
    "content": "==========\nUser Guide\n==========\n\n.. contents:: Table of Contents\n   :local:\n\nCertbot Commands\n================\n\nCertbot uses a number of different commands (also referred\nto as \"subcommands\") to request specific actions such as\nobtaining, renewing, or revoking certificates. The most important\nand commonly-used commands will be discussed throughout this\ndocument; an exhaustive list also appears near the end of the document.\n\nThe ``certbot`` script on your web server might be named ``letsencrypt`` if your system uses an older package. Throughout the docs, whenever you see ``certbot``, swap in the correct name as needed.\n\n.. _plugins:\n\nGetting certificates (and choosing plugins)\n===========================================\n\nCertbot helps you achieve two tasks:\n\n1. Obtaining a certificate: automatically performing the required authentication steps to prove that you control the domain(s),\n   saving the certificate to ``/etc/letsencrypt/live/`` and renewing it on a regular schedule.\n2. Optionally, installing that certificate to supported web servers (like Apache or nginx) and other kinds of servers. This is\n   done by automatically modifying the configuration of your server in order to use the certificate.\n\nTo obtain a certificate and also install it, use the ``certbot run`` command (or ``certbot``, which is the same).\n\nTo just obtain the certificate without installing it anywhere, the ``certbot certonly`` (\"certificate only\") command can be used.\n\nSome example ways to use Certbot::\n\n    # Obtain and install a certificate:\n    certbot\n\n    # Obtain a certificate but don't install it:\n    certbot certonly\n\n    # You may specify multiple domains with -d and obtain and\n    # install different certificates by running Certbot multiple times:\n    certbot certonly -d example.com -d www.example.com\n    certbot certonly -d app.example.com -d api.example.com\n\nTo perform these tasks, Certbot will ask you to choose from a selection of authenticator and installer plugins. The appropriate\nchoice of plugins will depend on what kind of server software you are running and plan to use your certificates with.\n\n**Authenticators** are plugins which automatically perform the required steps to prove that you control the domain names you're trying\nto request a certificate for. An authenticator is always required to obtain a certificate.\n\n**Installers** are plugins which can automatically modify your web server's configuration to serve your website over HTTPS, using the\ncertificates obtained by Certbot. An installer is only required if you want Certbot to install the certificate to your web server.\n\nSome plugins are both authenticators and installers and it is possible to specify a distinct combination_ of authenticator and plugin.\n\n=========== ==== ==== =============================================================== =============================\nPlugin      Auth Inst Notes                                                           Challenge types (and port)\n=========== ==== ==== =============================================================== =============================\napache_     Y    Y    | Automates obtaining and installing a certificate with Apache. http-01_ (80)\nnginx_      Y    Y    | Automates obtaining and installing a certificate with Nginx.  http-01_ (80)\nwebroot_    Y    N    | Obtains a certificate by writing to the webroot directory of  http-01_ (80)\n                      | an already running webserver.\nstandalone_ Y    N    | Uses a \"standalone\" webserver to obtain a certificate.        http-01_ (80)\n                      | Requires port 80 to be available. This is useful on\n                      | systems with no webserver, or when direct integration with\n                      | the local webserver is not supported or not desired.\n|dns_plugs| Y    N    | This category of plugins automates obtaining a certificate by dns-01_ (53)\n                      | modifying DNS records to prove you have control over a\n                      | domain. Doing domain validation in this way is\n                      | the only way to obtain wildcard certificates from Let's\n                      | Encrypt.\nmanual_     Y    N    | Obtain a certificate by manually following instructions to    http-01_ (80) or\n                      | perform domain validation yourself. Certificates created this dns-01_ (53)\n                      | way do not support autorenewal.\n                      | Autorenewal may be enabled by providing an authentication\n                      | hook script to automate the domain validation steps.\n=========== ==== ==== =============================================================== =============================\n\n.. |dns_plugs| replace:: :ref:`DNS plugins <dns_plugins>`\n\nUnder the hood, plugins use one of several ACME protocol challenges_ to\nprove you control a domain. The options are http-01_ (which uses port 80)\nand dns-01_ (requiring configuration of a DNS server on\nport 53, though that's often not the same machine as your webserver). A few\nplugins support more than one challenge type, in which case you can choose one\nwith ``--preferred-challenges``.\n\nThere are also many third-party-plugins_ available. Below we describe in more detail\nthe circumstances in which each plugin can be used, and how to use it.\n\n.. _challenges: https://datatracker.ietf.org/doc/html/rfc8555#section-8\n.. _http-01: https://datatracker.ietf.org/doc/html/rfc8555#section-8.3\n.. _dns-01: https://datatracker.ietf.org/doc/html/rfc8555#section-8.4\n\nApache\n------\n\nThe Apache plugin currently `supports\n<https://github.com/certbot/certbot/blob/main/certbot-apache/src/certbot_apache/_internal/entrypoint.py>`_\nmodern OSes based on Debian, Fedora, SUSE, Gentoo, CentOS and Darwin.\nThis automates both obtaining *and* installing certificates on an Apache\nwebserver. To specify this plugin on the command line, simply include\n``--apache``.\n\nWebroot\n-------\n\nIf you're running a local webserver for which you have the ability\nto modify the content being served, and you'd prefer not to stop the\nwebserver during the certificate issuance process, you can use the webroot\nplugin to obtain a certificate by including ``certonly`` and ``--webroot`` on\nthe command line. In addition, you'll need to specify ``--webroot-path``\nor ``-w`` with the top-level directory (\"web root\") containing the files\nserved by your webserver. For example, ``--webroot-path /var/www/html``\nor ``--webroot-path /usr/share/nginx/html`` are two common webroot paths.\n\nIf you're getting a certificate for many domains at once, the plugin\nneeds to know where each domain's files are served from, which could\npotentially be a separate directory for each domain. When requesting a\ncertificate for multiple domains, each domain will use the most recently\nspecified ``--webroot-path``. So, for instance,\n\n::\n\n    certbot certonly --webroot -w /var/www/example -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net\n\nwould obtain a single certificate for all of those names, using the\n``/var/www/example`` webroot directory for the first two, and\n``/var/www/other`` for the second two.\n\nThe webroot plugin works by creating a temporary file for each of your requested\ndomains in ``${webroot-path}/.well-known/acme-challenge``. Then the Let's Encrypt\nvalidation server makes HTTP requests to validate that the DNS for each\nrequested domain resolves to the server running certbot. An example request\nmade to your web server would look like:\n\n::\n\n    66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] \"GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1\" 200 87 \"-\" \"Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)\"\n\nNote that to use the webroot plugin, your server must be configured to serve\nfiles from hidden directories. If ``/.well-known`` is treated specially by\nyour webserver configuration, you might need to modify the configuration\nto ensure that files inside ``/.well-known/acme-challenge`` are served by\nthe webserver.\n\nUnder Windows, Certbot will generate a ``web.config`` file, if one does not already exist,\nin ``/.well-known/acme-challenge`` in order to let IIS serve the challenge files even if they\ndo not have an extension.\n\nNginx\n-----\n\nThe Nginx plugin should work for most configurations. We recommend backing up\nNginx configurations before using it (though you can also revert changes to\nconfigurations with ``certbot --nginx rollback``). You can use it by providing\nthe ``--nginx`` flag on the commandline.\n\n::\n\n   certbot --nginx\n\n.. _standalone:\n\nStandalone\n----------\n\nUse standalone mode to obtain a certificate if you don't want to use (or don't currently have)\nexisting server software. The standalone plugin does not rely on any other server\nsoftware running on the machine where you obtain the certificate.\n\nTo obtain a certificate using a \"standalone\" webserver, you can use the\nstandalone plugin by including ``certonly`` and ``--standalone``\non the command line. This plugin needs to bind to port 80 in\norder to perform domain validation, so you may need to stop your\nexisting webserver.\n\nIt must still be possible for your machine to accept inbound connections from\nthe Internet on the specified port using each requested domain name.\n\nBy default, Certbot first attempts to bind to the port for all interfaces using\nIPv6 and then bind to that port using IPv4; Certbot continues so long as at\nleast one bind succeeds. On most Linux systems, IPv4 traffic will be routed to\nthe bound IPv6 port and the failure during the second bind is expected.\n\nUse ``--<challenge-type>-address`` to explicitly tell Certbot which interface\n(and protocol) to bind.\n\n.. _dns_plugins:\n\nDNS Plugins\n-----------\n\nIf you'd like to obtain a wildcard certificate from Let's Encrypt or run\n``certbot`` on a machine other than your target webserver, you can use one of\nCertbot's DNS plugins.\n\nThese plugins are not included in a default Certbot installation and must be\ninstalled separately. They are available in many OS package managers, as Docker\nimages, and as snaps. Visit https://certbot.eff.org to learn the best way to\nuse the DNS plugins on your system.\n\nOnce installed, you can find documentation on how to use each plugin at:\n\n* `certbot-dns-cloudflare <https://certbot-dns-cloudflare.readthedocs.io>`_\n* `certbot-dns-digitalocean <https://certbot-dns-digitalocean.readthedocs.io>`_\n* `certbot-dns-dnsimple <https://certbot-dns-dnsimple.readthedocs.io>`_\n* `certbot-dns-dnsmadeeasy <https://certbot-dns-dnsmadeeasy.readthedocs.io>`_\n* `certbot-dns-gehirn <https://certbot-dns-gehirn.readthedocs.io>`_\n* `certbot-dns-google <https://certbot-dns-google.readthedocs.io>`_\n* `certbot-dns-linode <https://certbot-dns-linode.readthedocs.io>`_\n* `certbot-dns-luadns <https://certbot-dns-luadns.readthedocs.io>`_\n* `certbot-dns-nsone <https://certbot-dns-nsone.readthedocs.io>`_\n* `certbot-dns-ovh <https://certbot-dns-ovh.readthedocs.io>`_\n* `certbot-dns-rfc2136 <https://certbot-dns-rfc2136.readthedocs.io>`_\n* `certbot-dns-route53 <https://certbot-dns-route53.readthedocs.io>`_\n* `certbot-dns-sakuracloud <https://certbot-dns-sakuracloud.readthedocs.io>`_\n\nManual\n------\n\nIf you'd like to obtain a certificate running ``certbot`` on a machine\nother than your target webserver or perform the steps for domain\nvalidation yourself, you can use the manual plugin. While hidden from\nthe UI, you can use the plugin to obtain a certificate by specifying\n``certonly`` and ``--manual`` on the command line. This requires you\nto copy and paste commands into another terminal session, which may\nbe on a different computer.\n\nThe manual plugin can use either the ``http`` or the ``dns`` challenge. You can use the ``--preferred-challenges`` option\nto choose the challenge of your preference.\n\nThe ``http`` challenge will ask you to place a file with a specific name and\nspecific content in the ``/.well-known/acme-challenge/`` directory directly\nin the top-level directory (“web root”) containing the files served by your\nwebserver. In essence it's the same as the webroot_ plugin, but not automated.\n\nWhen using the ``dns`` challenge, ``certbot`` will ask you to place a TXT DNS\nrecord with specific contents under the domain name consisting of the hostname\nfor which you want a certificate issued, prepended by ``_acme-challenge``.\n\nFor example, for the domain ``example.com``, a zone file entry would look like:\n\n::\n\n        _acme-challenge.example.com. 300 IN TXT \"gfj9Xq...Rg85nM\"\n\n.. _manual-renewal:\n\n**Renewal with the manual plugin**\n\nCertificates created using ``--manual`` **do not** support automatic renewal unless\ncombined with an `authentication hook script <#hooks>`_  via ``--manual-auth-hook``\nto automatically set up the required HTTP and/or TXT challenges.\n\nIf you can use one of the other plugins_ which support autorenewal to create\nyour certificate, doing so is highly recommended.\n\nTo manually renew a certificate using ``--manual`` without hooks, repeat the same\n``certbot --manual`` command you used to create the certificate originally. As this\nwill require you to copy and paste new HTTP files or DNS TXT records, the command\ncannot be automated with a cron job.\n\n.. _combination:\n\nCombining plugins\n-----------------\n\nSometimes you may want to specify a combination of distinct authenticator and\ninstaller plugins. To do so, specify the authenticator plugin with\n``--authenticator`` or ``-a`` and the installer plugin with ``--installer`` or\n``-i``.\n\nFor instance, you could create a certificate using the webroot_ plugin\nfor authentication and the apache_ plugin for installation.\n\n::\n\n    certbot run -a webroot -i apache -w /var/www/html -d example.com\n\nOr you could create a certificate using the manual_ plugin for authentication\nand the nginx_ plugin for installation. (Note that this certificate cannot\nbe renewed automatically.)\n\n::\n\n    certbot run -a manual -i nginx -d example.com\n\n.. _third-party-plugins:\n\nThird-party plugins\n-------------------\n\nThere are also a number of third-party plugins for the client, provided by\nother developers. Many are beta/experimental, but some are already in\nwidespread use:\n\n======================= ==== ==== =================================================================\nPlugin                  Auth Inst Notes\n======================= ==== ==== =================================================================\nhaproxy_                Y    Y    Integration with the HAProxy load balancer\ns3front_                Y    Y    Integration with Amazon CloudFront distribution of S3 buckets\ngandi_                  Y    N    Obtain certificates via the Gandi LiveDNS API\nvarnish_                Y    N    Obtain certificates via a Varnish server\nexternal-auth_          Y    Y    A plugin for convenient scripting\npritunl_                N    Y    Install certificates in pritunl distributed OpenVPN servers\nproxmox_                N    Y    Install certificates in Proxmox Virtualization servers\ndns-standalone_         Y    N    Obtain certificates via an integrated DNS server\ndns-ispconfig_          Y    N    DNS Authentication using ISPConfig as DNS server\ndns-cloudns_            Y    N    DNS Authentication using ClouDNS API\ndns-clouddns_           Y    N    DNS Authentication using CloudDNS API\ndns-lightsail_          Y    N    DNS Authentication using Amazon Lightsail DNS API\ndns-inwx_               Y    Y    DNS Authentication for INWX through the XML API\ndns-azure_              Y    N    DNS Authentication using Azure DNS\ndns-godaddy_            Y    N    DNS Authentication using Godaddy DNS\ndns-yandexcloud_        Y    N    DNS Authentication using Yandex Cloud DNS\ndns-bunny_              Y    N    DNS Authentication using BunnyDNS\nnjalla_                 Y    N    DNS Authentication for njalla\nDuckDNS_                Y    N    DNS Authentication for DuckDNS\nPorkbun_                Y    N    DNS Authentication for Porkbun\nInfomaniak_             Y    N    DNS Authentication using Infomaniak Domains API\ndns-multi_              Y    N    DNS authentication of 100+ providers using go-acme/lego\ndns-dnsmanager_         Y    N    DNS Authentication for dnsmanager.io\nstandalone-nfq_         Y    N    HTTP Authentication that works with any webserver (Linux only)\ndns-solidserver_        Y    N    DNS Authentication using SOLIDserver (EfficientIP)\ndns-stackit_            Y    N    DNS Authentication using STACKIT DNS\ndns-ionos_              Y    N    DNS Authentication using IONOS Cloud DNS\ndns-mijn-host_          Y    N    DNS Authentication using mijn.host DNS\nnginx-unit_             Y    Y    Automates obtaining and installing a certificate with Nginx Unit\ndns-cdmon_              Y    N    DNS Authentication using cdmon's API\ndns-synergy-wholesale_  Y    N    DNS Authentication using Synergy Wholesale DNS\npkcs12_                 N    Y    Install certificates as PKCS#12 archives\ndns-hetzner-cloud_      Y    N    DNS Authentication for Hetzner Cloud DNS\ndns-czechia_            Y    N    DNS Authentication for czechia.com\ndns-eurodns_            Y    N    DNS Authentication for EuroDNS\n======================= ==== ==== =================================================================\n\n.. _haproxy: https://github.com/greenhost/certbot-haproxy\n.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front\n.. _gandi: https://github.com/obynio/certbot-plugin-gandi\n.. _varnish: https://git.sesse.net/?p=letsencrypt-varnish-plugin\n.. _pritunl: https://github.com/kharkevich/letsencrypt-pritunl\n.. _proxmox: https://github.com/kharkevich/letsencrypt-proxmox\n.. _external-auth: https://github.com/EnigmaBridge/certbot-external-auth\n.. _dns-standalone: https://github.com/siilike/certbot-dns-standalone\n.. _dns-ispconfig: https://github.com/m42e/certbot-dns-ispconfig\n.. _dns-cloudns: https://github.com/inventage/certbot-dns-cloudns\n.. _dns-clouddns: https://github.com/vshosting/certbot-dns-clouddns\n.. _dns-lightsail: https://github.com/noi/certbot-dns-lightsail\n.. _dns-inwx: https://github.com/oGGy990/certbot-dns-inwx/\n.. _dns-azure: https://github.com/terricain/certbot-dns-azure\n.. _dns-godaddy: https://github.com/miigotu/certbot-dns-godaddy\n.. _dns-yandexcloud: https://github.com/PykupeJIbc/certbot-dns-yandexcloud\n.. _dns-bunny: https://github.com/mwt/certbot-dns-bunny\n.. _njalla: https://github.com/chaptergy/certbot-dns-njalla\n.. _DuckDNS: https://github.com/infinityofspace/certbot_dns_duckdns\n.. _Porkbun: https://github.com/infinityofspace/certbot_dns_porkbun\n.. _Infomaniak: https://github.com/Infomaniak/certbot-dns-infomaniak\n.. _dns-multi: https://github.com/alexzorin/certbot-dns-multi\n.. _dns-dnsmanager: https://github.com/stayallive/certbot-dns-dnsmanager\n.. _standalone-nfq: https://github.com/alexzorin/certbot-standalone-nfq\n.. _dns-solidserver: https://gitlab.com/charlyhong/certbot-dns-solidserver\n.. _dns-stackit: https://github.com/stackitcloud/certbot-dns-stackit\n.. _dns-ionos: https://github.com/ionos-cloud/certbot-dns-ionos-cloud\n.. _dns-mijn-host: https://github.com/mijnhost/certbot-dns-mijn-host\n.. _nginx-unit: https://github.com/kea/certbot-nginx-unit\n.. _dns-cdmon: https://github.com/rascazzione/certbot-dns-cdmon\n.. _dns-synergy-wholesale: https://github.com/ALameLlama/certbot-dns-synergy-wholesale\n.. _pkcs12: https://github.com/nasa-gcn/certbot-pkcs12\n.. _dns-hetzner-cloud: https://github.com/rolschewsky/certbot-dns-hetzner-cloud\n.. _dns-czechia: https://github.com/CZECHIA-COM/certbot-dns-czechia\n.. _dns-eurodns: https://pypi.org/project/certbot-dns-eurodns/\n\nIf you're interested, you can also :ref:`write your own plugin <dev-plugin>`.\n\n.. _managing-certs:\n\nManaging certificates\n=====================\n\nTo view a list of the certificates Certbot knows about, run\nthe ``certificates`` subcommand:\n\n``certbot certificates``\n\nThis returns information in the following format::\n\n  Found the following certificates:\n    Certificate Name: example.com\n      Domains: example.com, www.example.com\n      Expiry Date: 2017-02-19 19:53:00+00:00 (VALID: 30 days)\n      Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem\n      Key Type: RSA\n      Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem\n\n``Certificate Name`` shows the name of the certificate. Pass this name\nusing the ``--cert-name`` flag to specify a particular certificate for the ``run``,\n``certonly``, ``certificates``, ``renew``, and ``delete`` commands. The certificate\nname cannot contain filepath separators (i.e. '/' or '\\\\', depending on the platform).\nExample::\n\n  certbot certonly --cert-name example.com\n\n.. _updating_certs:\n\nRe-creating and Updating Existing Certificates\n----------------------------------------------\n\nYou can use ``certonly`` or ``run`` subcommands to request\nthe creation of a single new certificate even if you already have an\nexisting certificate with some of the same domain names.\n\nIf a certificate is requested with ``run`` or ``certonly`` specifying a\ncertificate name that already exists, Certbot updates\nthe existing certificate. Otherwise a new certificate\nis created and assigned the specified name.\n\nThe ``--force-renewal``, ``--duplicate``, and ``--expand`` options\ncontrol Certbot's behavior when re-creating\na certificate with the same name as an existing certificate.\nIf you don't specify a requested behavior, Certbot may ask you what you intended.\n\n``--force-renewal`` tells Certbot to request a new certificate\nwith the same domains as an existing certificate. Each domain\nmust be explicitly specified via ``-d``. If successful, this certificate\nis saved alongside the earlier one and symbolic links (the \"``live``\"\nreference) will be updated to point to the new certificate. This is a\nvalid method of renewing a specific individual\ncertificate.\n\n``--duplicate`` tells Certbot to create a separate, unrelated certificate\nwith the same domains as an existing certificate. This certificate is\nsaved completely separately from the prior one. Most users will not\nneed to issue this command in normal circumstances.\n\n``--expand`` tells Certbot to update an existing certificate with a new\ncertificate that contains all of the old domains and one or more additional\nnew domains. With the ``--expand`` option, use the ``-d`` option to specify\nall existing domains and one or more new domains.\n\nExample:\n\n.. code-block:: none\n\n  certbot --expand -d existing.com,example.com,newdomain.com\n\nIf you prefer, you can specify the domains individually like this:\n\n.. code-block:: none\n\n  certbot --expand -d existing.com -d example.com -d newdomain.com\n\nConsider using ``--cert-name`` instead of ``--expand``, as it gives more control\nover which certificate is modified and it lets you remove domains as well as adding them.\n\n``--allow-subset-of-names`` tells Certbot to continue with certificate generation if\nonly some of the specified domain authorizations can be obtained. This may\nbe useful if some domains specified in a certificate no longer point at this\nsystem.\n\nWhenever you obtain a new certificate in any of these ways, the new\ncertificate exists alongside any previously obtained certificates, whether\nor not the previous certificates have expired. The generation of a new\ncertificate counts against several rate limits that are intended to prevent\nabuse of the ACME protocol, as described\n`here <https://letsencrypt.org/docs/rate-limits/>`__.\n\n.. _changing:\n\nChanging a Certificate's Domains\n--------------------------------\n\nThe ``--cert-name`` flag can also be used to modify the domains a certificate contains,\nby specifying new domains using the ``-d`` or ``--domains`` flag. If certificate ``example.com``\npreviously contained ``example.com`` and ``www.example.com``, it can be modified to only\ncontain ``example.com`` by specifying only ``example.com`` with the ``-d`` or ``--domains`` flag. Example::\n\n  certbot certonly --cert-name example.com -d example.com\n\nThe same format can be used to expand the set of domains a certificate contains, or to\nreplace that set entirely::\n\n  certbot certonly --cert-name example.com -d example.org,www.example.org\n\n.. _using-ecdsa-keys:\n\nRSA and ECDSA keys\n------------------------\n\nCertbot supports two certificate private key algorithms: ``rsa`` and ``ecdsa``.\n\nAs of version 2.0.0, Certbot defaults to ECDSA ``secp256r1`` (P-256) certificate private keys\nfor all new certificates. Existing certificates will continue to renew using their existing key\ntype, unless a key type change is requested.\n\nThe type of key used by Certbot can be controlled through the ``--key-type`` option.\nYou can use the ``--elliptic-curve`` option to control the curve used in ECDSA\ncertificates and the ``--rsa-key-size`` option to control the size of RSA keys.\n\n.. warning:: If you obtain certificates using ECDSA keys, you should be careful\n   not to downgrade to a Certbot version earlier than 1.10.0 where ECDSA keys were\n   not supported. Downgrades like this are possible if you switch from something like\n   the snaps or pip to packages provided by your operating system which often lag behind.\n\nChanging a certificate's key type\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nUnless you are aware that you need to support very old HTTPS clients that are\nnot supported by most sites, you can safely transition your site to use\nECDSA keys instead of RSA keys.\n\nIf you want to change a single certificate to use ECDSA keys, you'll need to\ncreate or renew a certificate while setting ``--key-type ecdsa`` on the command line:\n\n.. code-block:: shell\n\n  certbot renew --key-type ecdsa --cert-name example.com --force-renewal\n\nIf you want to use ECDSA keys for all certificates in the future (including renewals\nof existing certificates), you can add the following line to Certbot's\n:ref:`configuration file <config-file>`:\n\n.. code-block:: ini\n\n  key-type = ecdsa\n\nwhich will take effect upon the next renewal of each certificate.\n\nRevoking certificates\n---------------------\n\nIf you need to revoke a certificate, use the ``revoke`` subcommand to do so.\n\nA certificate may be revoked by providing its name (see ``certbot certificates``) or by providing\nits path directly::\n\n  certbot revoke --cert-name example.com\n\n  certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem\n\nIf the certificate being revoked was obtained via the ``--staging``, ``--test-cert`` or a non-default ``--server`` flag,\nthat flag must be passed to the ``revoke`` subcommand.\n\n.. note:: After revocation, Certbot will (by default) ask whether you want to **delete** the certificate.\n          Unless deleted, Certbot will try to renew revoked certificates the next time ``certbot renew`` runs.\n\nYou can also specify the reason for revoking your certificate by using the ``reason`` flag.\nReasons include ``unspecified`` which is the default, as well as ``keycompromise``,\n``affiliationchanged``, ``superseded``, and ``cessationofoperation``::\n\n  certbot revoke --cert-name example.com --reason keycompromise\n\nRevoking by account key or certificate private key\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nBy default, Certbot will try revoke the certificate using your ACME account key. If the certificate was created from\nthe same ACME account, the revocation will be successful.\n\nIf you instead have the corresponding private key file to the certificate you wish to revoke, use ``--key-path`` to perform the\nrevocation from any ACME account::\n\n  certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem --key-path /etc/letsencrypt/live/example.com/privkey.pem\n\n.. _deleting:\n\nDeleting certificates\n---------------------\n\nIf you need to delete a certificate, use the ``delete`` subcommand.\n\n.. note:: Read this and the `Safely deleting certificates`_ sections carefully. This is an irreversible operation and must\n          be done with care.\n\nCertbot does not automatically revoke a certificate before deleting it. If you're no longer using a certificate and don't\nplan to use it anywhere else, you may want to follow the instructions in `Revoking certificates`_ instead. Generally, there's\nno need to revoke a certificate if its private key has not been compromised, but you may still receive expiration emails\nfrom Let's Encrypt unless you revoke.\n\n.. note:: Do not manually delete certificate files from inside ``/etc/letsencrypt/``. Always use the ``delete`` subcommand.\n\nA certificate may be deleted by providing its name with ``--cert-name``. \\\nYou may find its name using ``certbot certificates``.\n\nOtherwise, you will be prompted to choose one or more\ncertificates to delete::\n\n  certbot delete --cert-name example.com\n  # or to choose from a list:\n  certbot delete\n\nSafely deleting certificates\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nDeleting a certificate without following the proper steps can result in a non-functioning server. To safely delete a\ncertificate, follow all the steps below to make sure that references to a certificate are removed from the configuration\nof any installed server software (Apache, nginx, Postfix, etc) *before* deleting the certificate.\n\nTo explain further, when installing a certificate, Certbot modifies Apache or nginx's configuration to load the certificate\nand its private key from the ``/etc/letsencrypt/live/`` directory. Before deleting a certificate, it is necessary to undo\nthat modification, by removing any references to the certificate from the webserver's configuration files.\n\nFollow these steps to safely delete a certificate:\n\n1. Find all references to the certificate (substitute ``example.com`` in the command for the name of the certificate\n   you wish to delete)::\n\n     sudo bash -c 'grep -R live/example.com /etc/{nginx,httpd,apache2}'\n\n   If there are no references found, skip directly to Step 4.\n\n   If some references are found, they will look something like::\n\n     /etc/apache2/sites-available/000-default-le-ssl.conf:SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem\n     /etc/apache2/sites-available/000-default-le-ssl.conf:SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem\n\n2. You will need a self-signed certificate to replace the certificate you are deleting. The following command will generate one\n   for you, saving the certificate at ``/etc/letsencrypt/self-signed-cert.pem`` and its private key at\n   ``/etc/letsencrypt/self-signed-privkey.pem``::\n\n     sudo openssl req -nodes -batch -x509 -newkey rsa:2048 -keyout /etc/letsencrypt/self-signed-privkey.pem -out /etc/letsencrypt/self-signed-cert.pem -days 356\n\n3. For each reference found in Step 1, open the file in a text editor and replace the reference to the existing\n   certificate with a reference to the self-signed certificate.\n\n   Continuing from the previous example, you would open ``/etc/apache2/sites-available/000-default-le-ssl.conf`` in a text editor\n   and modify the two matching lines of text to instead say::\n\n     SSLCertificateFile /etc/letsencrypt/self-signed-cert.pem\n     SSLCertificateKeyFile /etc/letsencrypt/self-signed-privkey.pem\n\n4. It is now safe to delete the certificate. Do so by running::\n\n     sudo certbot delete --cert-name example.com\n\n.. _renewal:\n\nRenewing certificates\n---------------------\n\n.. seealso:: Most Certbot installations come with automatic\n   renewal out of the box. See `Automated Renewals`_ for more details.\n\n.. seealso:: Users of the `Manual`_ plugin should note that ``--manual`` certificates\n   will not renew automatically, unless combined with authentication hook scripts.\n   See `Renewal with the manual plugin <#manual-renewal>`_.\n\nCertbot supports a ``renew`` action to check all installed certificates for\nimpending expiry and attempt to renew them. The simplest form is simply\n\n``certbot renew``\n\nThis command attempts to renew any previously-obtained certificates which are ready\nfor renewal. As of Certbot 4.0.0, a certificate is considered ready for renewal\nwhen less than 1/3rd of its lifetime remains. For certificates with a lifetime\nof 10 days or less, that threshold is 1/2 of the lifetime. Prior to Certbot 4.0.0\nthe threshold was a fixed 30 days.\n\nThe same plugin and options that were used\nat the time the certificate was originally issued will be used for the\nrenewal attempt, unless you specify other plugins or options. Unlike ``certonly``, ``renew`` acts on\nmultiple certificates and always takes into account whether each one is near\nexpiry. Because of this, ``renew`` is suitable (and designed) for automated use,\nto allow your system to automatically renew each certificate when appropriate.\nSince ``renew`` only renews certificates that are near expiry it can be\nrun as frequently as you want - since it will usually take no action.\n\nThe ``renew`` command includes hooks for running commands or scripts before or after a certificate is\nrenewed. For example, if you have a single certificate obtained using\nthe standalone_ plugin, you might need to stop the webserver\nbefore renewing so standalone can bind to the necessary ports, and\nthen restart it after the plugin is finished. Example::\n\n  certbot renew --pre-hook \"service nginx stop\" --post-hook \"service nginx start\"\n\nIf a hook exits with a non-zero exit code, the error will be printed\nto ``stderr`` but renewal will be attempted anyway. A failing hook\ndoesn't directly cause Certbot to exit with a non-zero exit code, but\nsince Certbot exits with a non-zero exit code when renewals fail, a\nfailed hook causing renewal failures will indirectly result in a\nnon-zero exit code. Hooks will only be run if a certificate is due for\nrenewal, so you can run the above command frequently without\nunnecessarily stopping your webserver.\n\nWhen Certbot detects that a certificate is due for renewal, ``--pre-hook``\nand ``--post-hook`` hooks run before and after each attempt to renew it.\nIf you want your hook to run only after a successful renewal, use\n``--deploy-hook`` in a command like this.\n\n``certbot renew --deploy-hook /path/to/deploy-hook-script``\n\nYou can also specify hooks by placing files in subdirectories of Certbot's\nconfiguration directory. Assuming your configuration directory is\n``/etc/letsencrypt``, any executable files found in\n``/etc/letsencrypt/renewal-hooks/pre``,\n``/etc/letsencrypt/renewal-hooks/deploy``, and\n``/etc/letsencrypt/renewal-hooks/post`` will be run as pre, deploy, and post\nhooks respectively. These hooks are run in alphabetical order. (The order the\nhooks are run is determined by the byte value of the characters in their\nfilenames and is not dependent on your locale.)\n\nPrior to certbot 3.2.0, hooks in directories were only run when certificates\nwere renewed with the ``renew`` subcommand, but as of 3.2.0, they are run for\nany subcommand.\n\nHooks specified in the command line, :ref:`configuration file\n<config-file>`, or :ref:`renewal configuration files <renewal-config-file>` are\nrun as usual after running all hooks in these directories. One minor exception\nto this is if a hook specified elsewhere is simply the path to an executable\nfile in the hook directory of the same type (e.g. your pre-hook is the path to\nan executable in ``/etc/letsencrypt/renewal-hooks/pre``), the file is not run a\nsecond time. You can stop Certbot from automatically running executables found\nin these directories by including ``--no-directory-hooks`` on the command line.\n\nMore information about hooks can be found by running\n``certbot --help renew``.\n\nIf you're sure that this command executes successfully without human\nintervention, you can add the command to ``crontab`` (since certificates\nare only renewed when they're determined to be near expiry, the command\ncan run on a regular basis, like every week or every day). In that case,\nyou are likely to want to use the ``-q`` or ``--quiet`` quiet flag to\nsilence all output except errors.\n\nIf you are manually renewing all of your certificates, the\n``--force-renewal`` flag may be helpful; it causes the expiration time of\nthe certificate(s) to be ignored when considering renewal, and attempts to\nrenew each and every installed certificate regardless of its age. (This\nform is not appropriate to run daily because each certificate will be\nrenewed every day, which will quickly run into the certificate authority\nrate limit.)\n\nStarting with Certbot 2.7.0, certbot provides the environment variables\n`RENEWED_DOMAINS` and `FAILED_DOMAINS` to all post renewal hooks. These\nvariables contain a space separated list of domains. These variables can be used\nto determine if a renewal has succeeded or failed as part of your post renewal\nhook.\n\nNote that options provided to ``certbot renew`` will apply to\n*every* certificate for which renewal is attempted; for example,\n``certbot renew --rsa-key-size 4096`` would try to replace every\nnear-expiry certificate with an equivalent certificate using a 4096-bit\nRSA public key. If a certificate is successfully renewed using\nspecified options, those options will be saved and used for future\nrenewals of that certificate.\n\nAn alternative form that provides for more fine-grained control over the\nrenewal process (while renewing specified certificates one at a time),\nis ``certbot certonly`` with the complete set of subject domains of\na specific certificate specified via `-d` flags. You may also want to\ninclude the ``-n`` or ``--noninteractive`` flag to prevent blocking on\nuser input (which is useful when running the command from cron).\n\n``certbot certonly -n -d example.com -d www.example.com``\n\nAll of the domains covered by the certificate must be specified in\nthis case in order to renew and replace the old certificate rather\nthan obtaining a new one; don't forget any `www.` domains! Specifying\na subset of the domains creates a new, separate certificate containing\nonly those domains, rather than replacing the original certificate.\nWhen run with a set of domains corresponding to an existing certificate,\nthe ``certonly`` command attempts to renew that specific certificate.\n\nPlease note that the CA will send notification emails to the address\nyou provide if you do not renew certificates that are about to expire.\n\nCertbot is working hard to improve the renewal process, and we\napologize for any inconvenience you encounter in integrating these\ncommands into your individual environment.\n\n.. note:: ``certbot renew`` exit status will only be 1 if a renewal attempt failed.\n  This means ``certbot renew`` exit status will be 0 if no certificate needs to be updated.\n  If you write a custom script and expect to run a command only after a certificate was actually renewed\n  you will need to use the ``--deploy-hook`` since the exit status will be 0 both on successful renewal\n  and when renewal is not necessary.\n\n.. _renaming:\n\nRenaming certificates\n------------------------------------------------------------\n\nWhile certbot does not have an internal rename function, it is possible to rename certs by\nrequesting a new certificate using certbot's ``--cert-name`` flag.\n\n1. View certificates with ``certbot certificates``\n\n   The output might be something like::\n\n    Found the following certs:\n      Certificate Name: yourdomain.com-0001\n        Serial Number: 2ce74f4e2b822211dd7648ea26c8927bdef6\n        Key Type: ECDSA\n        Domains: yourdomain.com subdomain.yourdomain.com\n        Expiry Date: 2025-11-16 20:27:27+00:00 (INVALID: TEST_CERT)\n        Certificate Path: /etc/letsencrypt/live/yourdomain.com-0001/fullchain.pem\n        Private Key Path: /etc/letsencrypt/live/yourdomain.com-0001/privkey.pem\n      Certificate Name: yourdomain.com\n        Serial Number: 2c72aa8cf58aa9fe9a33208d82304642d9ed\n        Key Type: ECDSA\n        Domains: yourdomain.com\n        Expiry Date: 2025-11-16 19:22:47+00:00 (INVALID: TEST_CERT)\n        Certificate Path: /etc/letsencrypt/live/yourdomain.com/fullchain.pem\n        Private Key Path: /etc/letsencrypt/live/yourdomain.com/privkey.pem\n\n   In this example, we will rename certificate ``yourdomain.com-0001`` to ``yourdomain.com``.\n\n2. If the name you want is in use and you're not using that cert, delete it using the instructions\n   in the `Deleting certificates`_ section.\n\n   You'll then have::\n\n    Found the following certs:\n      Certificate Name: yourdomain.com-0001\n        Serial Number: 2ce74f4e2b822211dd7648ea26c8927bdef6\n        Key Type: ECDSA\n        Domains: yourdomain.com subdomain.yourdomain.com\n        Expiry Date: 2025-11-16 20:27:27+00:00 (INVALID: TEST_CERT)\n        Certificate Path: /etc/letsencrypt/live/yourdomain.com-0001/fullchain.pem\n        Private Key Path: /etc/letsencrypt/live/yourdomain.com-0001/privkey.pem\n\n3. If you're not sure how you set the cert up initially, note relevant configuration options by\n   inspecting ``/etc/letsencrypt/renewal/yourdomain.com-0001.conf``\n\n   It will look something like::\n\n    version = 4.2.0\n    archive_dir = /etc/letsencrypt/archive/yourdomain.com-0001\n    cert = /etc/letsencrypt/live/yourdomain.com-0001/cert.pem\n    privkey = /etc/letsencrypt/live/yourdomain.com-0001/privkey.pem\n    chain = /etc/letsencrypt/live/yourdomain.com-0001/chain.pem\n    fullchain = /etc/letsencrypt/live/yourdomain.com-0001/fullchain.pem\n    [renewalparams]\n    account = abcdef12345678910\n    server = https://acme-staging-v02.api.letsencrypt.org/directory\n    authenticator = standalone\n    key_type = ecdsa\n    post_hook = \"echo 'shut down server'\"\n\n   You'll want to note the authenticator, installer, server, any hooks, etc.\n\n4. Create a new cert with the options and name you want by running\n   ``certbot certonly --cert-name yourdomain.com -d yourdomain.com -d otherdomain.com --any-other-options``.\n   The value for ``--cert-name`` can be anything you want; by default it is the first domain\n   listed on the cert.\n\n5. Make sure that any links to your certificates are updated. If you're using certbot to manage\n   the installation of certs on your webserver, you can run\n   ``certbot install --cert-name yourdomain.com``.\n\n6. Restart your webserver, if applicable.\n\n7. You can now delete the old certificate, again using the `Deleting certificates`_ section.\n\n.. _renewal-config-file:\n.. _Modifying the Renewal Configuration File:\n\nModifying the Renewal Configuration of Existing Certificates\n------------------------------------------------------------\n\nWhen creating a certificate, Certbot will keep track of all of the relevant options chosen by the user. At renewal\ntime, Certbot will remember these options and apply them once again.\n\nSometimes, you may encounter the need to change some of these options for future certificate renewals. To achieve this,\nyou will need to perform the following steps:\n\nCertbot v2.3.0 and newer\n~~~~~~~~~~~~~~~~~~~~~~~~\nThe ``certbot reconfigure`` command can be used to change a certificate's renewal options.\nThis command will use the new renewal options to perform a test renewal against the Let's Encrypt staging server.\nIf this is successful, the new renewal options will be saved and will apply to future renewals.\n\nYou will need to specify the ``--cert-name``, which can be found by running ``certbot certificates``.\n\nA list of common options that may be updated with the ``reconfigure`` command can be found by running\n``certbot help reconfigure``.\n\nAs a practical example, if you were using the ``webroot`` authenticator and had relocated your website to another directory,\nyou can change the ``--webroot-path`` to the new directory using the following command:\n\n.. code-block:: shell\n\n  certbot reconfigure --cert-name example.com --webroot-path /path/to/new/location\n\nCertbot v2.2.0 and older\n~~~~~~~~~~~~~~~~~~~~~~~~\n1. Perform a *dry run renewal* with the amended options on the command line. This allows you to confirm that the change\n   is valid and will result in successful future renewals.\n2. If the dry run is successful, perform a *live renewal* of the certificate. This will persist the change for future\n   renewals. If the certificate is not yet due to expire, you will need to force a renewal using ``--force-renewal``.\n\n.. note:: Rate limits from the certificate authority may prevent you from performing multiple renewals in a short\n   period of time. It is strongly recommended to perform the second step only once, when you have decided on what\n   options should change.\n\nAs a practical example, if you were using the ``webroot`` authenticator and had relocated your website to another directory,\nyou would need to change the ``--webroot-path`` to the new directory. Following the above advice:\n\n1. Perform a *dry-run renewal* of the individual certificate with the amended options::\n\n     certbot renew --cert-name example.com --webroot-path /path/to/new/location --dry-run\n\n2. If the dry-run was successful, make the change permanent by performing a *live renewal* of the certificate with the\n   amended options, including ``--force-renewal``::\n\n     certbot renew --cert-name example.com --webroot-path /path/to/new/location --force-renewal\n\n   ``--cert-name`` selects the particular certificate to be modified. Without this option, all certificates will be selected.\n\n   ``--webroot-path`` is the option intended to be changed. All other previously selected options will be kept the same\n   and do not need to be included in the command.\n\nFor advanced certificate management tasks, it is also possible to manually modify the certificate's renewal configuration\nfile, but this is discouraged since it can easily break Certbot's ability to renew your certificates. These renewal\nconfiguration files are located at ``/etc/letsencrypt/renewal/CERTNAME.conf``. If you choose to modify the renewal\nconfiguration file we advise you to make a backup of the file beforehand and test its validity with the ``certbot renew --dry-run`` command.\n\n.. warning:: Manually modifying files under ``/etc/letsencrypt/renewal/`` can damage them if done improperly and we do not recommend doing so.\n\n\nAutomated Renewals\n------------------\n\nMost Certbot installations come with automatic renewals preconfigured. This\nis done by means of a scheduled task which runs ``certbot renew`` periodically.\n\nIf you are unsure whether you need to configure automated renewal:\n\n1. Review the instructions for your system and installation method at\n   https://certbot.eff.org/instructions. They will describe how to set up a scheduled task,\n   if necessary. If no step is listed, your system comes with automated renewal pre-installed,\n   and you should not need to take any additional actions.\n2. On Linux and BSD, you can check to see if your installation method has pre-installed a timer\n   for you. To do so, look for the ``certbot renew`` command in either your system's crontab\n   (typically `/etc/crontab` or `/etc/cron.*/*`) or systemd timers (``systemctl list-timers``).\n3. If you're still not sure, you can configure automated renewal manually by following the steps\n   in the next section. Certbot has been carefully engineered to handle the case where both manual\n   automated renewal and pre-installed automated renewal are set up.\n\nSetting up automated renewal\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nIf you think you may need to set up automated renewal, follow these instructions to set up a\nscheduled task to automatically renew your certificates in the background. If you are unsure\nwhether your system has a pre-installed scheduled task for Certbot, it is safe to follow these\ninstructions to create one.\n\n.. note::\n   If you're using Windows, these instructions are not necessary as Certbot on Windows comes with\n   a scheduled task for automated renewal pre-installed.\n\n   If you are using macOS and installed Certbot using Homebrew, follow the instructions at\n   https://certbot.eff.org/instructions to set up automated renewal. The instructions below\n   are not applicable on macOS.\n\nRun the following line, which will add a cron job to `/etc/crontab`:\n\n.. code-block:: shell\n\n  SLEEPTIME=$(awk 'BEGIN{srand(); print int(rand()*(3600+1))}'); echo \"0 0,12 * * * root sleep $SLEEPTIME && certbot renew -q\" | sudo tee -a /etc/crontab > /dev/null\n\nIf you needed to stop your webserver to run Certbot, you'll want to\nadd ``pre`` and ``post`` hooks to stop and start your webserver automatically.\nFor example, if your webserver is HAProxy, run the following commands to create the hook files\nin the appropriate directory:\n\n.. code-block:: shell\n\n  sudo sh -c 'printf \"#!/bin/sh\\nservice haproxy stop\\n\" > /etc/letsencrypt/renewal-hooks/pre/haproxy.sh'\n  sudo sh -c 'printf \"#!/bin/sh\\nservice haproxy start\\n\" > /etc/letsencrypt/renewal-hooks/post/haproxy.sh'\n  sudo chmod 755 /etc/letsencrypt/renewal-hooks/pre/haproxy.sh\n  sudo chmod 755 /etc/letsencrypt/renewal-hooks/post/haproxy.sh\n\nCongratulations, Certbot will now automatically renew your certificates in the background.\n\nIf you are interested in learning more about how Certbot renews your certificates, see the\n`Renewing certificates`_ section above.\n\n.. _where-certs:\n\nWhere are my certificates?\n==========================\n\nAll generated keys and issued certificates can be found in\n``/etc/letsencrypt/live/$domain``, where ``$domain`` is the certificate\nname (see the note below). Rather than copying, please point your (web)\nserver configuration directly to those files (or create symlinks).\nDuring the renewal_, ``/etc/letsencrypt/live`` is updated with the latest\nnecessary files.\n\n.. note::\n  The certificate name ``$domain`` used in the path ``/etc/letsencrypt/live/$domain``\n  follows this convention:\n\n  * it is the name given to ``--cert-name``,\n  * if ``--cert-name`` is not set by the user it is the first domain given to\n    ``--domains``,\n  * if the first domain is a wildcard domain (eg. ``*.example.com``) the\n    certificate name will be ``example.com``,\n  * if a name collision would occur with a certificate already named ``example.com``,\n    the new certificate name will be constructed using a numerical sequence\n    as ``example.com-001``.\n\nFor historical reasons, the containing directories are created with\npermissions of ``0700`` meaning that certificates are accessible only\nto servers that run as the root user.  **If you will never downgrade\nto an older version of Certbot**, then you can safely fix this using\n``chmod 0755 /etc/letsencrypt/{live,archive}``.\n\nFor servers that drop root privileges before attempting to read the\nprivate key file, you will also need to use ``chgrp`` and ``chmod\n0640`` to allow the server to read\n``/etc/letsencrypt/live/$domain/privkey.pem``.\n\nThe following files are available:\n\n``privkey.pem``\n  Private key for the certificate.\n\n  .. warning:: This **must be kept secret at all times**! Never share\n     it with anyone, including Certbot developers. You cannot\n     put it into a safe, however - your server still needs to access\n     this file in order for SSL/TLS to work.\n\n  .. note:: As of Certbot version 0.29.0, private keys for new certificate\n     default to ``0600``. Any changes to the group mode or group owner (gid)\n     of this file will be preserved on renewals.\n\n  This is what Apache needs for `SSLCertificateKeyFile\n  <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatekeyfile>`_,\n  and Nginx for `ssl_certificate_key\n  <https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate_key>`_.\n\n``fullchain.pem``\n  All certificates, **including** server certificate (aka leaf certificate or\n  end-entity certificate). The server certificate is the first one in this file,\n  followed by any intermediates.\n\n  This is what Apache >= 2.4.8 needs for `SSLCertificateFile\n  <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_,\n  and what Nginx needs for `ssl_certificate\n  <https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate>`_.\n\n``cert.pem`` and ``chain.pem`` (less common)\n  ``cert.pem`` contains the server certificate by itself, and\n  ``chain.pem`` contains the additional intermediate certificate or\n  certificates that web browsers will need in order to validate the\n  server certificate. If you provide one of these files to your web\n  server, you **must** provide both of them, or some browsers will show\n  \"This Connection is Untrusted\" errors for your site, `some of the time\n  <https://whatsmychaincert.com/>`_.\n\n  Apache < 2.4.8 needs these for `SSLCertificateFile\n  <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_.\n  and `SSLCertificateChainFile\n  <https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatechainfile>`_,\n  respectively.\n\n  If you're using OCSP stapling with Nginx >= 1.3.7, ``chain.pem`` should be\n  provided as the `ssl_trusted_certificate\n  <https://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_trusted_certificate>`_\n  to validate OCSP responses.\n\n.. note:: All files are PEM-encoded.\n   If you need other format, such as DER or PFX, then you\n   could convert using ``openssl``. You can automate that with\n   ``--deploy-hook`` if you're using automatic renewal_.\n\n.. _hooks:\n\nPre and Post Validation Hooks\n=============================\n\nCertbot allows for the specification of pre and post validation hooks when run\nin manual mode. The flags to specify these scripts are ``--manual-auth-hook``\nand ``--manual-cleanup-hook`` respectively and can be used as follows:\n\n::\n\n certbot certonly --manual --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com\n\nThis will run the ``authenticator.sh`` script, attempt the validation, and then run\nthe ``cleanup.sh`` script. Additionally certbot will pass relevant environment\nvariables to these scripts:\n\n- ``CERTBOT_IDENTIFIER``: The domain or IP address being authenticated\n- ``CERTBOT_VALIDATION``: The validation string\n- ``CERTBOT_TOKEN``: Resource name part of the HTTP-01 challenge (HTTP-01 only)\n- ``CERTBOT_REMAINING_CHALLENGES``: Number of challenges remaining after the current challenge\n- ``CERTBOT_ALL_IDENTIFIERS``: A comma-separated list of all identifiers challenged for the current certificate\n\nAdditionally for cleanup:\n\n- ``CERTBOT_AUTH_OUTPUT``: Whatever the auth script wrote to stdout\n\nCertbot also sets ``CERTBOT_DOMAIN`` and ``CERTBOT_ALL_DOMAINS`` to the same values as\n``CERTBOT_IDENTIFIER`` and ``CERTBOT_ALL_IDENTIFIERS`` respectively for backwards compatibility,\nhowever, the variables names containing \"identifier\" are preferred since Certbot has added support\nfor obtaining certificates for IP addresses.\n\nExample usage for HTTP-01:\n\n::\n\n certbot certonly --manual --preferred-challenges=http --manual-auth-hook /path/to/http/authenticator.sh --manual-cleanup-hook /path/to/http/cleanup.sh -d secure.example.com\n\n/path/to/http/authenticator.sh\n\n.. code-block:: none\n\n   #!/bin/bash\n   echo $CERTBOT_VALIDATION > /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN\n\n/path/to/http/cleanup.sh\n\n.. code-block:: none\n\n   #!/bin/bash\n   rm -f /var/www/htdocs/.well-known/acme-challenge/$CERTBOT_TOKEN\n\nExample usage for DNS-01 (Cloudflare API v4) (for example purposes only, do not use as-is)\n\n::\n\n certbot certonly --manual --preferred-challenges=dns --manual-auth-hook /path/to/dns/authenticator.sh --manual-cleanup-hook /path/to/dns/cleanup.sh -d secure.example.com\n\n/path/to/dns/authenticator.sh\n\n.. code-block:: none\n\n   #!/bin/bash\n\n   # Get your API key from https://www.cloudflare.com/a/account/my-account\n   API_KEY=\"your-api-key\"\n   EMAIL=\"your.email@example.com\"\n\n   # Strip only the top domain to get the zone id\n   DOMAIN=$(expr match \"$CERTBOT_IDENTIFIER\" '.*\\.\\(.*\\..*\\)')\n\n   # Get the Cloudflare zone id\n   ZONE_EXTRA_PARAMS=\"status=active&page=1&per_page=20&order=status&direction=desc&match=all\"\n   ZONE_ID=$(curl -s -X GET \"https://api.cloudflare.com/client/v4/zones?name=$DOMAIN&$ZONE_EXTRA_PARAMS\" \\\n        -H     \"X-Auth-Email: $EMAIL\" \\\n        -H     \"X-Auth-Key: $API_KEY\" \\\n        -H     \"Content-Type: application/json\" | python -c \"import sys,json;print(json.load(sys.stdin)['result'][0]['id'])\")\n\n   # Create TXT record\n   CREATE_DOMAIN=\"_acme-challenge.$CERTBOT_IDENTIFIER\"\n   RECORD_ID=$(curl -s -X POST \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records\" \\\n        -H     \"X-Auth-Email: $EMAIL\" \\\n        -H     \"X-Auth-Key: $API_KEY\" \\\n        -H     \"Content-Type: application/json\" \\\n        --data '{\"type\":\"TXT\",\"name\":\"'\"$CREATE_DOMAIN\"'\",\"content\":\"'\"$CERTBOT_VALIDATION\"'\",\"ttl\":120}' \\\n                | python -c \"import sys,json;print(json.load(sys.stdin)['result']['id'])\")\n   # Save info for cleanup\n   if [ ! -d /tmp/CERTBOT_$CERTBOT_IDENTIFIER ];then\n           mkdir -m 0700 /tmp/CERTBOT_$CERTBOT_IDENTIFIER\n   fi\n   echo $ZONE_ID > /tmp/CERTBOT_$CERTBOT_IDENTIFIER/ZONE_ID\n   echo $RECORD_ID > /tmp/CERTBOT_$CERTBOT_IDENTIFIER/RECORD_ID\n\n   # Sleep to make sure the change has time to propagate over to DNS\n   sleep 25\n\n/path/to/dns/cleanup.sh\n\n.. code-block:: none\n\n   #!/bin/bash\n\n   # Get your API key from https://www.cloudflare.com/a/account/my-account\n   API_KEY=\"your-api-key\"\n   EMAIL=\"your.email@example.com\"\n\n   if [ -f /tmp/CERTBOT_$CERTBOT_IDENTIFIER/ZONE_ID ]; then\n           ZONE_ID=$(cat /tmp/CERTBOT_$CERTBOT_IDENTIFIER/ZONE_ID)\n           rm -f /tmp/CERTBOT_$CERTBOT_IDENTIFIER/ZONE_ID\n   fi\n\n   if [ -f /tmp/CERTBOT_$CERTBOT_IDENTIFIER/RECORD_ID ]; then\n           RECORD_ID=$(cat /tmp/CERTBOT_$CERTBOT_IDENTIFIER/RECORD_ID)\n           rm -f /tmp/CERTBOT_$CERTBOT_IDENTIFIER/RECORD_ID\n   fi\n\n   # Remove the challenge TXT record from the zone\n   if [ -n \"${ZONE_ID}\" ]; then\n       if [ -n \"${RECORD_ID}\" ]; then\n           curl -s -X DELETE \"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID\" \\\n                   -H \"X-Auth-Email: $EMAIL\" \\\n                   -H \"X-Auth-Key: $API_KEY\" \\\n                   -H \"Content-Type: application/json\"\n       fi\n   fi\n\n.. _lock-files:\n\nChanging the ACME Server\n========================\n\nBy default, Certbot uses Let's Encrypt's production server at\nhttps://acme-v02.api.letsencrypt.org/directory. You can tell Certbot to use a\ndifferent CA by providing ``--server`` on the command line or in a\n:ref:`configuration file <config-file>` with the URL of the server's\nACME directory. For example, if you would like to use Let's Encrypt's\nstaging server, you would add ``--server\nhttps://acme-staging-v02.api.letsencrypt.org/directory`` to the command line.\n\n.. note:: ``--dry-run`` uses the Let's Encrypt staging server, unless ``--server``\n   is specified on the CLI or in the :ref:`cli.ini configuration file <config-file>`.\n   Take caution when using ``--dry-run`` with a custom server, as it may cause real\n   certificates to be issued and discarded.\n\nIf Certbot does not trust the SSL certificate used by the ACME server, you\ncan use the `REQUESTS_CA_BUNDLE\n<https://requests.readthedocs.io/en/latest/user/advanced/#ssl-cert-verification>`_\nenvironment variable to override the root certificates trusted by Certbot. Certbot\nuses the ``requests`` library, which does not use the operating system trusted root store.\nMake sure that ``REQUESTS_CA_BUNDLE`` is set globally in the environment and not only on\nthe CLI, or scheduled renewal will not succeed.\n\n\nLock Files\n==========\n\nWhen processing a validation Certbot writes a number of lock files on your system\nto prevent multiple instances from overwriting each other's changes. This means\nthat by default two instances of Certbot will not be able to run in parallel.\n\nSince the directories used by Certbot are configurable, Certbot\nwill write a lock file for all of the directories it uses. This include Certbot's\n``--work-dir``, ``--logs-dir``, and ``--config-dir``. By default these are\n``/var/lib/letsencrypt``, ``/var/log/letsencrypt``, and ``/etc/letsencrypt``\nrespectively. Additionally if you are using Certbot with Apache or nginx it will\nlock the configuration folder for that program, which are typically also in the\n``/etc`` directory.\n\nNote that these lock files will only prevent other instances of Certbot from\nusing those directories, not other processes. If you'd like to run multiple\ninstances of Certbot simultaneously you should specify different directories\nas the ``--work-dir``, ``--logs-dir``, and ``--config-dir`` for each instance\nof Certbot that you would like to run.\n\n.. _config-file:\n\nConfiguration file\n==================\n\nCertbot accepts a global configuration file that applies its options to all invocations\nof Certbot. Certificate-specific configuration choices are stored in the ``.conf``\nfiles that can be found in ``/etc/letsencrypt/renewal``. See \n`Modifying the Renewal Configuration of Existing Certificates`_ for more information\nabout modifying certificate-specific options. Note that it is not recommended to modify\nthese certificate-specific renewal configuration files manually.\n\nBy default no cli.ini file is created (though it may exist already if you installed Certbot\nvia a package manager, for instance).\nAfter creating one it is possible to specify the location of this configuration file with\n``certbot --config cli.ini`` (or shorter ``-c cli.ini``). An\nexample configuration file is shown below:\n\n.. include:: ../examples/cli.ini\n   :code: ini\n\nBy default, the following locations are searched:\n\n- ``/etc/letsencrypt/cli.ini``\n- ``$XDG_CONFIG_HOME/letsencrypt/cli.ini`` (or\n  ``~/.config/letsencrypt/cli.ini`` if ``$XDG_CONFIG_HOME`` is not\n  set).\n\nSince this configuration file applies to all invocations of certbot it is incorrect\nto list domains in it. Listing domains in cli.ini may prevent renewal from working.\nAdditionally due to how arguments in cli.ini are parsed, options which wish to\nnot be set should not be listed. Options set to false will instead be read\nas being set to true by older versions of Certbot, since they have been listed\nin the config file.\n\n.. keep it up to date with constants.py\n\n.. _log-rotation:\n\nLog Rotation\n============\n\nBy default certbot stores status logs in ``/var/log/letsencrypt``. By default\ncertbot will begin rotating logs once there are 1000 logs in the log directory.\nMeaning that once 1000 files are in ``/var/log/letsencrypt`` Certbot will delete\nthe oldest one to make room for new logs. The number of subsequent logs can be\nchanged by passing the desired number to the command line flag\n``--max-log-backups``. Setting this flag to 0 disables log rotation entirely,\ncausing certbot to always append to the same log file.\n\n.. note:: Some distributions, including Debian and Ubuntu, disable\n   certbot's internal log rotation in favor of a more traditional\n   logrotate script.  If you are using a distribution's packages and\n   want to alter the log rotation, check `/etc/logrotate.d/` for a\n   certbot rotation script.\n\n.. _command-line:\n\nCertbot command-line options\n============================\n\nCertbot supports a lot of command line options. Here's the full list, from\n``certbot --help all``:\n\n.. literalinclude:: cli-help.txt\n\nGetting help\n============\n\nIf you're having problems, we recommend posting on the Let's Encrypt\n`Community Forum <https://community.letsencrypt.org>`_.\n\nIf you find a bug in the software, please do report it in our `issue\ntracker <https://github.com/certbot/certbot/issues>`_. Remember to\ngive us as much information as possible:\n\n- copy and paste exact command line used and the output (though mind\n  that the latter might include some personally identifiable\n  information, including your email and domains)\n- copy and paste logs from ``/var/log/letsencrypt`` (though mind they\n  also might contain personally identifiable information)\n- copy and paste ``certbot --version`` output\n- your operating system, including specific version\n- specify which installation method you've chosen\n"
  },
  {
    "path": "certbot/docs/what.rst",
    "content": "======================\nWhat is a Certificate?\n======================\n\nA public key or digital *certificate* (formerly called an SSL certificate) uses a public key\nand a private key to enable secure communication between a client program (web browser, email client,\netc.) and a server over an encrypted SSL (secure socket layer) or TLS (transport layer security) connection.\nThe certificate is used both to encrypt the initial stage of communication (secure key exchange)\nand to identify the server. The certificate\nincludes information about the key, information about the server identity, and the digital signature\nof the certificate issuer. If the issuer is trusted by the software that initiates the communication,\nand the signature is valid, then the key can be used to communicate securely with the server identified by\nthe certificate. Using a certificate is a good way to prevent \"man-in-the-middle\" attacks, in which\nsomeone in between you and the server you think you are talking to is able to insert their own (harmful)\ncontent.\n\nYou can use Certbot to easily obtain and configure a free certificate from Let's Encrypt, a\njoint project of EFF, Mozilla, and many other sponsors.\n\nCertificates and Lineages\n=========================\n\nCertbot introduces the concept of a *lineage,* which is a collection of all the versions of a certificate\nplus Certbot configuration information maintained for that certificate from\nrenewal to renewal. Whenever you renew a certificate, Certbot keeps the same configuration unless\nyou explicitly change it, for example by adding or removing domains. If you add domains, you can\neither add them to an existing lineage or create\na new one.\n\nSee also:\n:ref:`updating_certs`\n"
  },
  {
    "path": "certbot/examples/.gitignore",
    "content": "# generate-csr.sh:\n/key.pem\n/csr.der"
  },
  {
    "path": "certbot/examples/cli.ini",
    "content": "# This is an example of the kind of things you can do in a configuration file.\n# All flags used by the client can be configured here. Run Certbot with\n# \"--help\" to learn more about the available options.\n#\n# Note that these options apply automatically to all use of Certbot for\n# obtaining or renewing certificates, so options specific to a single\n# certificate on a system with several certificates should not be placed\n# here.\n\n# Use ECC for the private key\nkey-type = ecdsa\nelliptic-curve = secp384r1\n\n# Use a 4096 bit RSA key instead of 2048\nrsa-key-size = 4096\n\n# Uncomment and update to register with the specified e-mail address\n# email = foo@example.com\n\n# Uncomment to use the standalone authenticator on port 80\n# authenticator = standalone\n\n# Uncomment to use the webroot authenticator. Replace webroot-path with the\n# path to the public_html / webroot folder being served by your web server.\n# authenticator = webroot\n# webroot-path = /usr/share/nginx/html\n\n# Uncomment to automatically agree to the terms of service of the ACME server\n# agree-tos = true\n\n# An example of using an alternate ACME server that uses EAB credentials\n# server = https://acme.sectigo.com/v2/InCommonRSAOV \n# eab-kid = somestringofstuffwithoutquotes\n# eab-hmac-key = yaddayaddahexhexnotquoted\n"
  },
  {
    "path": "certbot/examples/dev-cli.ini",
    "content": "# Always use the staging/testing server - avoids rate limiting\nserver = https://acme-staging-v02.api.letsencrypt.org/directory\n\n# This is an example configuration file for developers\nconfig-dir = /tmp/le/conf\nwork-dir = /tmp/le/conf\nlogs-dir = /tmp/le/logs\n\n# make sure to use a valid email and domains!\nemail = foo@example.com\ndomains = example.com\n\ntext = True\nagree-tos = True\ndebug = True\nverbose-level = 2 # -vv (debug)\n\nauthenticator = standalone\n"
  },
  {
    "path": "certbot/examples/generate-csr.sh",
    "content": "#!/bin/sh\n# This script generates a simple SAN CSR to be used with Let's Encrypt\n# CA. Mostly intended for \"auth --csr\" testing, but, since it's easily\n# auditable, feel free to adjust it and use it on your production web\n# server.\n\nif [ \"$#\" -lt 1 ]\nthen\n  echo \"Usage: $0 domain [domain...]\" >&2\n  exit 1\nfi\n\ndomains=\"DNS:$1\"\nshift\nfor x in \"$@\"\ndo\n  domains=\"$domains,DNS:$x\"\ndone\n\nSAN=\"$domains\" openssl req -config \"${OPENSSL_CNF:-openssl.cnf}\" \\\n  -new -nodes -subj '/' -reqexts san \\\n  -out \"${CSR_PATH:-csr.der}\" \\\n  -keyout \"${KEY_PATH:-key.pem}\" \\\n  -newkey rsa:2048 \\\n  -outform DER\n# 512 or 1024 too low for Boulder, 2048 is smallest for tests\n\necho \"You can now run: certbot auth --csr ${CSR_PATH:-csr.der}\"\n"
  },
  {
    "path": "certbot/examples/openssl.cnf",
    "content": "[ req ]\ndistinguished_name = req_distinguished_name\n[ req_distinguished_name ]\n[ san ]\nsubjectAltName=${ENV::SAN}\n"
  },
  {
    "path": "certbot/examples/plugins/certbot_example_plugins.py",
    "content": "\"\"\"Example Certbot plugins.\n\nFor full examples, see `certbot.plugins`.\n\n\"\"\"\nfrom certbot import interfaces\nfrom certbot.plugins import common\n\n\nclass Authenticator(common.Plugin, interfaces.Authenticator):\n    \"\"\"Example Authenticator.\"\"\"\n\n    description = \"Example Authenticator plugin\"\n\n    # Implement all methods from Authenticator, remembering to add\n    # \"self\" as first argument, e.g. def prepare(self)...\n\n\nclass Installer(common.Plugin, interfaces.Installer):\n    \"\"\"Example Installer.\"\"\"\n\n    description = \"Example Installer plugin\"\n\n    # Implement all methods from Installer, remembering to add\n    # \"self\" as first argument, e.g. def get_all_names(self)...\n"
  },
  {
    "path": "certbot/examples/plugins/setup.py",
    "content": "from setuptools import setup\n\nsetup(\n    name='certbot-example-plugins',\n    package='certbot_example_plugins.py',\n    install_requires=[\n        'certbot',\n    ],\n    entry_points={\n        'certbot.plugins': [\n            'example_authenticator = certbot_example_plugins:Authenticator',\n            'example_installer = certbot_example_plugins:Installer',\n        ],\n    },\n)\n"
  },
  {
    "path": "certbot/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"ACME client\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Console\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n\n[project.optional-dependencies]\ndev = [\n    \"azure-devops\",\n    \"build\",\n    \"ipdb\",\n    # allows us to use newer urllib3 https://github.com/python-poetry/poetry-plugin-export/issues/183\n    \"poetry-plugin-export>=1.9.0\",\n    # poetry 1.2.0+ is required for it to pin pip, setuptools, and wheel. See\n    # https://github.com/python-poetry/poetry/issues/1584.\n    \"poetry>=1.2.0\",\n    \"towncrier\",\n    \"twine\",\n]\ndocs = [\n    # If you have Sphinx<1.5.1, you need docutils<0.13.1\n    # https://github.com/sphinx-doc/sphinx/issues/3212\n    \"Sphinx>=1.2\", # Annotation support\n    \"sphinx_rtd_theme\",\n]\n# Tools like pip, wheel, and tox are listed here to ensure they are properly\n# pinned and installed during automated testing.\ntest = [\n    \"coverage\",\n    \"mypy\",\n    \"pip\",\n    \"pylint\",\n    \"pytest\",\n    \"pytest-cov>=4.1.0\", # https://github.com/pytest-dev/pytest-cov/pull/558\n    \"pytest-xdist\",\n    \"ruff\",\n    \"setuptools\",\n    \"tox\",\n    \"types-httplib2\",\n    \"types-pyRFC3339\",\n    \"types-pywin32\",\n    \"types-requests\",\n    \"types-setuptools\",\n    \"uv\",\n    \"wheel\",\n]\nall = [\n    \"certbot[dev,docs,test]\"\n]\n\n[project.scripts]\ncertbot = \"certbot.main:main\"\n\n[project.entry-points.\"certbot.plugins\"]\nmanual = \"certbot._internal.plugins.manual:Authenticator\"\nnull = \"certbot._internal.plugins.null:Installer\"\nstandalone = \"certbot._internal.plugins.standalone:Authenticator\"\nwebroot = \"certbot._internal.plugins.webroot:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]  # list of folders that contain the packages ([\".\"] by default)\nexclude = ['docs', 'examples', 'tests', 'venv']  # exclude packages matching these glob patterns\n\n[tool.setuptools.dynamic]\nversion = {attr = \"certbot.__version__\"}  # any module attribute compatible with ast.literal_eval\n"
  },
  {
    "path": "certbot/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install .[docs]\" does not work as\n# expected and \"pip install -e certbot[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot[docs]\n"
  },
  {
    "path": "certbot/setup.py",
    "content": "import codecs\nimport os\nimport re\n\nfrom setuptools import setup\n\n\ndef read_file(filename, encoding='utf8'):\n    \"\"\"Read unicode from given file.\"\"\"\n    with codecs.open(filename, encoding=encoding) as fd:\n        return fd.read()\n\n\nhere = os.path.abspath(os.path.dirname(__file__))\n\n# read version number (and other metadata) from package init\ninit_fn = os.path.join(here, 'src', 'certbot', '__init__.py')\nmeta = dict(re.findall(r\"\"\"__([a-z]+)__ = '([^']+)\"\"\", read_file(init_fn)))\n\nversion = meta['version']\n\n# This package relies on PyOpenSSL and requests, however, it isn't specified\n# here to avoid masking the more specific request requirements in acme. See\n# https://github.com/pypa/pip/issues/988 for more info.\ninstall_requires = [\n    # We specify the minimum acme version as the current Certbot version for\n    # simplicity. See https://github.com/certbot/certbot/issues/8761 for more\n    # info.\n    f'acme>={version}',\n    'ConfigArgParse>=1.5.3',\n    'configobj>=5.0.6',\n    'cryptography>=43.0.0',\n    'distro>=1.0.1',\n    'importlib_metadata>=8.6.1; python_version < \"3.10\"',\n    'josepy>=2.0.0',\n    'parsedatetime>=2.6',\n    'pyrfc3339',\n    # This dependency needs to be added using environment markers to avoid its\n    # installation on Linux.\n    'pywin32>=300 ; sys_platform == \"win32\"',\n]\n\n\nsetup(\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot/src/certbot/__init__.py",
    "content": "\"\"\"Certbot client.\"\"\"\n\n# version number like 1.2.3a0, must have at least 2 parts, like 1.2\n__version__ = '5.5.0.dev0'\n"
  },
  {
    "path": "certbot/src/certbot/_internal/__init__.py",
    "content": "\"\"\"\nModules internal to Certbot.\n\nThis package contains modules that are not considered part of Certbot's public\nAPI. They may be changed without updating Certbot's major version.\n\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/account.py",
    "content": "\"\"\"Creates ACME accounts for server.\"\"\"\nimport datetime\nimport functools\nimport hashlib\nimport logging\nimport shutil\nimport socket\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\n\nfrom cryptography.hazmat.primitives import serialization\nimport josepy as jose\nimport pyrfc3339\n\nfrom acme import fields as acme_fields\nfrom acme import messages\nfrom acme.client import ClientV2\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n\nclass Account:\n    \"\"\"ACME protocol registration.\n\n    :ivar .RegistrationResource regr: Registration Resource\n    :ivar .JWK key: Authorized Account Key\n    :ivar .Meta: Account metadata\n    :ivar str id: Globally unique account identifier.\n\n    \"\"\"\n\n    class Meta(jose.JSONObjectWithFields):\n        \"\"\"Account metadata\n\n        :ivar datetime.datetime creation_dt: Creation date and time (UTC).\n        :ivar str creation_host: FQDN of host, where account has been created.\n        :ivar str register_to_eff: If not None, Certbot will register the provided\n                                        email during the account registration.\n\n        .. note:: ``creation_dt`` and ``creation_host`` are useful in\n            cross-machine migration scenarios.\n\n        \"\"\"\n        creation_dt: datetime.datetime = acme_fields.rfc3339(\"creation_dt\")\n        creation_host: str = jose.field(\"creation_host\")\n        register_to_eff: str = jose.field(\"register_to_eff\", omitempty=True)\n\n    def __init__(self, regr: messages.RegistrationResource, key: jose.JWK,\n                 meta: Optional['Meta'] = None) -> None:\n        self.key = key\n        self.regr = regr\n        self.meta = self.Meta(\n            # pyrfc3339 drops microseconds, make sure __eq__ is sane\n            creation_dt=datetime.datetime.now(tz=datetime.timezone.utc).replace(microsecond=0),\n            creation_host=socket.gethostname(),\n            register_to_eff=None) if meta is None else meta\n\n        # try MD5, else use MD5 in non-security mode (e.g. for FIPS systems / RHEL)\n        try:\n            hasher = hashlib.md5()\n        except ValueError:\n            # This cast + dictionary expansion is made to make mypy happy without the need of a\n            # \"type: ignore\" directive that will also require to disable the check on useless\n            # \"type: ignore\" directives when mypy is run on Python 3.9+.\n            hasher = hashlib.new('md5', **cast(Mapping[str, Any], {\"usedforsecurity\": False}))\n\n        hasher.update(self.key.key.public_key().public_bytes(\n            encoding=serialization.Encoding.PEM,\n            format=serialization.PublicFormat.SubjectPublicKeyInfo)\n        )\n\n        self.id = hasher.hexdigest()\n        # Implementation note: Email? Multiple accounts can have the\n        # same email address. Registration URI? Assigned by the\n        # server, not guaranteed to be stable over time, nor\n        # canonical URI can be generated. ACME protocol doesn't allow\n        # account key (and thus its fingerprint) to be updated...\n\n    @property\n    def slug(self) -> str:\n        \"\"\"Short account identification string, useful for UI.\"\"\"\n        return \"{1}@{0} ({2})\".format(pyrfc3339.generate(\n            self.meta.creation_dt), self.meta.creation_host, self.id[:4])\n\n    def __repr__(self) -> str:\n        return \"<{0}({1}, {2}, {3})>\".format(\n            self.__class__.__name__, self.regr, self.id, self.meta)\n\n    def __eq__(self, other: Any) -> bool:\n        return (isinstance(other, self.__class__) and\n                self.key == other.key and self.regr == other.regr and\n                self.meta == other.meta)\n\n\nclass AccountMemoryStorage(interfaces.AccountStorage):\n    \"\"\"In-memory account storage.\"\"\"\n\n    def __init__(self, initial_accounts: Optional[dict[str, Account]] = None) -> None:\n        self.accounts = initial_accounts if initial_accounts is not None else {}\n\n    def find_all(self) -> list[Account]:\n        return list(self.accounts.values())\n\n    def save(self, account: Account, client: ClientV2) -> None:\n        if account.id in self.accounts:\n            logger.debug(\"Overwriting account: %s\", account.id)\n        self.accounts[account.id] = account\n\n    def load(self, account_id: str) -> Account:\n        try:\n            return self.accounts[account_id]\n        except KeyError:\n            raise errors.AccountNotFound(account_id)\n\n\nclass AccountFileStorage(interfaces.AccountStorage):\n    \"\"\"Accounts file storage.\n\n    :ivar certbot.configuration.NamespaceConfig config: Client configuration\n\n    \"\"\"\n    def __init__(self, config: configuration.NamespaceConfig) -> None:\n        self.config = config\n        util.make_or_verify_dir(config.accounts_dir, 0o700, self.config.strict_permissions)\n\n    def _account_dir_path(self, account_id: str) -> str:\n        return self._account_dir_path_for_server_path(account_id, self.config.server_path)\n\n    def _account_dir_path_for_server_path(self, account_id: str, server_path: str) -> str:\n        accounts_dir = self.config.accounts_dir_for_server_path(server_path)\n        return os.path.join(accounts_dir, account_id)\n\n    @classmethod\n    def _regr_path(cls, account_dir_path: str) -> str:\n        return os.path.join(account_dir_path, \"regr.json\")\n\n    @classmethod\n    def _key_path(cls, account_dir_path: str) -> str:\n        return os.path.join(account_dir_path, \"private_key.json\")\n\n    @classmethod\n    def _metadata_path(cls, account_dir_path: str) -> str:\n        return os.path.join(account_dir_path, \"meta.json\")\n\n    def _find_all_for_server_path(self, server_path: str) -> list[Account]:\n        accounts_dir = self.config.accounts_dir_for_server_path(server_path)\n        try:\n            candidates = os.listdir(accounts_dir)\n        except OSError:\n            return []\n\n        accounts = []\n        for account_id in candidates:\n            try:\n                accounts.append(self._load_for_server_path(account_id, server_path))\n            except errors.AccountStorageError:\n                logger.debug(\"Account loading problem\", exc_info=True)\n\n        if not accounts and server_path in constants.LE_REUSE_SERVERS:\n            # find all for the next link down\n            prev_server_path = constants.LE_REUSE_SERVERS[server_path]\n            prev_accounts = self._find_all_for_server_path(prev_server_path)\n            # if we found something, link to that\n            if prev_accounts:\n                try:\n                    self._symlink_to_accounts_dir(prev_server_path, server_path)\n                except OSError:\n                    return []\n            accounts = prev_accounts\n        return accounts\n\n    def find_all(self) -> list[Account]:\n        return self._find_all_for_server_path(self.config.server_path)\n\n    def _symlink_to_account_dir(self, prev_server_path: str, server_path: str,\n                                account_id: str) -> None:\n        prev_account_dir = self._account_dir_path_for_server_path(account_id, prev_server_path)\n        new_account_dir = self._account_dir_path_for_server_path(account_id, server_path)\n        os.symlink(prev_account_dir, new_account_dir)\n\n    def _symlink_to_accounts_dir(self, prev_server_path: str, server_path: str) -> None:\n        accounts_dir = self.config.accounts_dir_for_server_path(server_path)\n        if os.path.islink(accounts_dir):\n            os.unlink(accounts_dir)\n        else:\n            os.rmdir(accounts_dir)\n        prev_account_dir = self.config.accounts_dir_for_server_path(prev_server_path)\n        os.symlink(prev_account_dir, accounts_dir)\n\n    def _load_for_server_path(self, account_id: str, server_path: str) -> Account:\n        account_dir_path = self._account_dir_path_for_server_path(account_id, server_path)\n        if not os.path.isdir(account_dir_path): # isdir is also true for symlinks\n            if server_path in constants.LE_REUSE_SERVERS:\n                prev_server_path = constants.LE_REUSE_SERVERS[server_path]\n                prev_loaded_account = self._load_for_server_path(account_id, prev_server_path)\n                # we didn't error so we found something, so create a symlink to that\n                accounts_dir = self.config.accounts_dir_for_server_path(server_path)\n                # If accounts_dir isn't empty, make an account specific symlink\n                if os.listdir(accounts_dir):\n                    self._symlink_to_account_dir(prev_server_path, server_path, account_id)\n                else:\n                    self._symlink_to_accounts_dir(prev_server_path, server_path)\n                return prev_loaded_account\n            raise errors.AccountNotFound(f\"Account at {account_dir_path} does not exist\")\n\n        try:\n            with open(self._regr_path(account_dir_path)) as regr_file:\n                regr = messages.RegistrationResource.json_loads(regr_file.read())\n            with open(self._key_path(account_dir_path)) as key_file:\n                key = jose.JWK.json_loads(key_file.read())\n            with open(self._metadata_path(account_dir_path)) as metadata_file:\n                meta = Account.Meta.json_loads(metadata_file.read())\n        except OSError as error:\n            raise errors.AccountStorageError(error)\n\n        return Account(regr, key, meta)\n\n    def load(self, account_id: str) -> Account:\n        return self._load_for_server_path(account_id, self.config.server_path)\n\n    def save(self, account: Account, client: ClientV2) -> None:\n        \"\"\"Create a new account.\n\n        :param Account account: account to create\n        :param ClientV2 client: ACME client associated to the account\n\n        \"\"\"\n        try:\n            dir_path = self._prepare(account)\n            self._create(account, dir_path)\n            self._update_meta(account, dir_path)\n            self._update_regr(account, dir_path)\n        except OSError as error:\n            raise errors.AccountStorageError(error)\n\n    def update_regr(self, account: Account) -> None:\n        \"\"\"Update the registration resource.\n\n        :param Account account: account to update\n\n        \"\"\"\n        try:\n            dir_path = self._prepare(account)\n            self._update_regr(account, dir_path)\n        except OSError as error:\n            raise errors.AccountStorageError(error)\n\n    def update_meta(self, account: Account) -> None:\n        \"\"\"Update the meta resource.\n\n        :param Account account: account to update\n\n        \"\"\"\n        try:\n            dir_path = self._prepare(account)\n            self._update_meta(account, dir_path)\n        except OSError as error:\n            raise errors.AccountStorageError(error)\n\n    def delete(self, account_id: str) -> None:\n        \"\"\"Delete registration info from disk\n\n        :param account_id: id of account which should be deleted\n\n        \"\"\"\n        account_dir_path = self._account_dir_path(account_id)\n        if not os.path.isdir(account_dir_path):\n            raise errors.AccountNotFound(f\"Account at {account_dir_path} does not exist\")\n        # Step 1: Delete account specific links and the directory\n        self._delete_account_dir_for_server_path(account_id, self.config.server_path)\n\n        # Step 2: Remove any accounts links and directories that are now empty\n        if not os.listdir(self.config.accounts_dir):\n            self._delete_accounts_dir_for_server_path(self.config.server_path)\n\n    def _delete_account_dir_for_server_path(self, account_id: str, server_path: str) -> None:\n        link_func = functools.partial(self._account_dir_path_for_server_path, account_id)\n        nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func)\n        shutil.rmtree(nonsymlinked_dir)\n\n    def _delete_accounts_dir_for_server_path(self, server_path: str) -> None:\n        link_func = self.config.accounts_dir_for_server_path\n        nonsymlinked_dir = self._delete_links_and_find_target_dir(server_path, link_func)\n        os.rmdir(nonsymlinked_dir)\n\n    def _delete_links_and_find_target_dir(self, server_path: str,\n                                          link_func: Callable[[str], str]) -> str:\n        \"\"\"Delete symlinks and return the nonsymlinked directory path.\n\n        :param str server_path: file path based on server\n        :param callable link_func: callable that returns possible links\n            given a server_path\n\n        :returns: the final, non-symlinked target\n        :rtype: str\n\n        \"\"\"\n        dir_path = link_func(server_path)\n\n        # does an appropriate directory link to me? if so, make sure that's gone\n        reused_servers = {}\n        for k, v in constants.LE_REUSE_SERVERS.items():\n            reused_servers[v] = k\n\n        # is there a next one up?\n        possible_next_link = True\n        while possible_next_link:\n            possible_next_link = False\n            if server_path in reused_servers:\n                next_server_path = reused_servers[server_path]\n                next_dir_path = link_func(next_server_path)\n                if os.path.islink(next_dir_path) and filesystem.readlink(next_dir_path) == dir_path:\n                    possible_next_link = True\n                    server_path = next_server_path\n                    dir_path = next_dir_path\n\n        # if there's not a next one up to delete, then delete me\n        # and whatever I link to\n        while os.path.islink(dir_path):\n            target = filesystem.readlink(dir_path)\n            os.unlink(dir_path)\n            dir_path = target\n\n        return dir_path\n\n    def _prepare(self, account: Account) -> str:\n        account_dir_path = self._account_dir_path(account.id)\n        util.make_or_verify_dir(account_dir_path, 0o700, self.config.strict_permissions)\n        return account_dir_path\n\n    def _create(self, account: Account, dir_path: str) -> None:\n        with util.safe_open(self._key_path(dir_path), \"w\", chmod=0o400) as key_file:\n            key_file.write(account.key.json_dumps())\n\n    def _update_regr(self, account: Account, dir_path: str) -> None:\n        with open(self._regr_path(dir_path), \"w\") as regr_file:\n            regr = messages.RegistrationResource(\n                body={},\n                uri=account.regr.uri)\n            regr_file.write(regr.json_dumps())\n\n    def _update_meta(self, account: Account, dir_path: str) -> None:\n        with open(self._metadata_path(dir_path), \"w\") as metadata_file:\n            metadata_file.write(account.meta.json_dumps())\n"
  },
  {
    "path": "certbot/src/certbot/_internal/auth_handler.py",
    "content": "\"\"\"ACME AuthHandler.\"\"\"\nimport datetime\nimport logging\nimport time\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Sequence\n\nimport josepy\nfrom requests.models import Response\n\nfrom acme import challenges\nfrom acme import client\nfrom acme import errors as acme_errors\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal import error_handler\nfrom certbot._internal.account import Account\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common as plugin_common\n\nlogger = logging.getLogger(__name__)\n\n\nclass AuthHandler:\n    \"\"\"ACME Authorization Handler for a client.\n\n    :ivar auth: Authenticator capable of solving\n        :class:`~acme.challenges.Challenge` types\n    :type auth: certbot.interfaces.Authenticator\n\n    :ivar acme.client.ClientV2 acme_client: ACME client API.\n\n    :ivar account: Client's Account\n    :type account: :class:`certbot._internal.account.Account`\n\n    :ivar list pref_challs: sorted user specified preferred challenges\n        type strings with the most preferred challenge listed first\n\n    \"\"\"\n    def __init__(self, auth: interfaces.Authenticator, acme_client: Optional[client.ClientV2],\n                 account: Optional[Account], pref_challs: list[str]) -> None:\n        self.auth = auth\n        self.acme = acme_client\n\n        self.account = account\n        self.pref_challs = pref_challs\n\n    def handle_authorizations(self, orderr: messages.OrderResource,\n                              config: configuration.NamespaceConfig, best_effort: bool = False,\n                              max_retries: int = 30,\n                              max_time_mins: float = 30) -> list[messages.AuthorizationResource]:\n        \"\"\"\n        Retrieve all authorizations, perform all challenges required to validate\n        these authorizations, then poll and wait for the authorization to be checked.\n        :param acme.messages.OrderResource orderr: must have authorizations filled in\n        :param certbot.configuration.NamespaceConfig config: current Certbot configuration\n        :param bool best_effort: if True, not all authorizations need to be validated (eg. renew)\n        :param int max_retries: maximum number of retries to poll authorizations\n        :param float max_time_mins: maximum time (in minutes) to poll authorizations\n        :returns: list of all validated authorizations\n        :rtype: List\n\n        :raises .AuthorizationError: If unable to retrieve all authorizations\n        \"\"\"\n        authzrs = orderr.authorizations[:]\n        if not authzrs:\n            raise errors.AuthorizationError('No authorization to handle.')\n        if not self.acme:\n            raise errors.Error(\"No ACME client defined, authorizations cannot be handled.\")\n\n        # Retrieve challenges that need to be performed to validate authorizations.\n        achalls = self._choose_challenges(authzrs)\n        if not achalls:\n            return authzrs\n\n        # Starting now, challenges will be cleaned at the end no matter what.\n        with error_handler.ExitHandler(self._cleanup_challenges, achalls):\n            # To begin, let's ask the authenticator plugin to perform all challenges.\n            try:\n                resps = self.auth.perform(achalls)\n\n                # If debug is on, wait for user input before starting the verification process.\n                if config.debug_challenges:\n                    display_util.notification(\n                        'Challenges loaded. Press continue to submit to CA.\\n' +\n                        self._debug_challenges_msg(achalls, config), pause=True)\n            except errors.AuthorizationError as error:\n                logger.critical('Failure in setting up challenges.')\n                logger.info('Attempting to clean up outstanding challenges...')\n                raise error\n            # All challenges should have been processed by the authenticator.\n            assert len(resps) == len(achalls), 'Some challenges have not been performed.'\n\n            # Inform the ACME CA server that challenges are available for validation.\n            for achall, resp in zip(achalls, resps):\n                self.acme.answer_challenge(achall.challb, resp)\n\n            # Wait for authorizations to be checked.\n            logger.info('Waiting for verification...')\n            self._poll_authorizations(authzrs, max_retries, max_time_mins, best_effort)\n\n            # Keep validated authorizations only. If there is none, no certificate can be issued.\n            authzrs_validated = [authzr for authzr in authzrs\n                                 if authzr.body.status == messages.STATUS_VALID]\n            if not authzrs_validated:\n                raise errors.AuthorizationError('All challenges have failed.')\n\n            return authzrs_validated\n\n        raise errors.Error(\"An unexpected error occurred while handling the authorizations.\")\n\n    def deactivate_valid_authorizations(self, orderr: messages.OrderResource) -> tuple[list, list]:\n        \"\"\"\n        Deactivate all `valid` authorizations in the order, so that they cannot be reused\n        in subsequent orders.\n        :param messages.OrderResource orderr: must have authorizations filled in\n        :returns: tuple of list of successfully deactivated authorizations, and\n                  list of unsuccessfully deactivated authorizations.\n        :rtype: tuple\n        \"\"\"\n        if not self.acme:\n            raise errors.Error(\"No ACME client defined, cannot deactivate valid authorizations.\")\n\n        to_deactivate = [authzr for authzr in orderr.authorizations\n                         if authzr.body.status == messages.STATUS_VALID]\n        deactivated = []\n        failed = []\n\n        for authzr in to_deactivate:\n            try:\n                authzr = self.acme.deactivate_authorization(authzr)\n                deactivated.append(authzr)\n            except acme_errors.Error as e:\n                failed.append(authzr)\n                logger.debug('Failed to deactivate authorization %s: %s', authzr.uri, e)\n\n        return (deactivated, failed)\n\n    def _poll_authorizations(self, authzrs: list[messages.AuthorizationResource], max_retries: int,\n                             deadline_minutes: float, best_effort: bool) -> None:\n        \"\"\"\n        Poll the ACME CA server, to wait for confirmation that authorizations have their challenges\n        all verified. The poll may occur several times, until all authorizations are checked\n        (valid or invalid), or a maximum of retries, or the polling deadline is reached.\n        \"\"\"\n        if not self.acme:\n            raise errors.Error(\"No ACME client defined, cannot poll authorizations.\")\n\n        authzrs_to_check: dict[int, tuple[messages.AuthorizationResource,\n                                          Optional[Response]]] = {index: (authzr, None)\n                            for index, authzr in enumerate(authzrs)}\n        authzrs_failed_to_report = []\n        deadline = datetime.datetime.now() + datetime.timedelta(minutes=deadline_minutes)\n        # Give an initial second to the ACME CA server to check the authorizations\n        sleep_seconds: float = 1\n        for _ in range(max_retries):\n            # Wait for appropriate time (from Retry-After, initial wait, or no wait)\n            if sleep_seconds > 0:\n                time.sleep(sleep_seconds)\n            # Poll all updated authorizations.\n            authzrs_to_check = {index: self.acme.poll(authzr) for index, (authzr, _)\n                                in authzrs_to_check.items()}\n            # Update the original list of authzr with the updated authzrs from server.\n            for index, (authzr, _) in authzrs_to_check.items():\n                authzrs[index] = authzr\n\n            # Gather failed authorizations\n            authzrs_failed = [authzr for authzr, _ in authzrs_to_check.values()\n                              if authzr.body.status == messages.STATUS_INVALID]\n            for authzr_failed in authzrs_failed:\n                logger.info('Challenge failed for domain %s',\n                               authzr_failed.body.identifier.value)\n            # Accumulating all failed authzrs to build a consolidated report\n            # on them at the end of the polling.\n            authzrs_failed_to_report.extend(authzrs_failed)\n\n            # Extract out the authorization already checked for next poll iteration.\n            # Poll may stop here because there is no pending authorizations anymore.\n            authzrs_to_check = {index: (authzr, resp) for index, (authzr, resp)\n                                in authzrs_to_check.items()\n                                if authzr.body.status == messages.STATUS_PENDING}\n            if not authzrs_to_check or datetime.datetime.now() > deadline:\n                # Polling process is finished, we can leave the loop\n                break\n\n            # Be merciful with the ACME server CA, check the Retry-After header returned,\n            # and wait this time before polling again in next loop iteration.\n            # From all the pending authorizations, we take the greatest Retry-After value\n            # to avoid polling an authorization before its relevant Retry-After value.\n            # (by construction resp cannot be None at that time, but mypy do not know it).\n            retry_after = max(self.acme.retry_after(resp, 3)\n                              for _, resp in authzrs_to_check.values()\n                              if resp is not None)\n            # Whatever Retry-After the ACME server requests, the polling must not take\n            # longer than the overall deadline (https://github.com/certbot/certbot/issues/9526).\n            retry_after = min(retry_after, deadline)\n            sleep_seconds = (retry_after - datetime.datetime.now()).total_seconds()\n\n        # In case of failed authzrs, create a report to the user.\n        if authzrs_failed_to_report:\n            self._report_failed_authzrs(authzrs_failed_to_report)\n            if not best_effort:\n                # Without best effort, having failed authzrs is critical and fail the process.\n                raise errors.AuthorizationError('Some challenges have failed.')\n\n        if authzrs_to_check:\n            # Here authzrs_to_check is still not empty, meaning we exceeded the max polling attempt.\n            raise errors.AuthorizationError('All authorizations were not finalized by the CA.')\n\n    def _choose_challenges(self, authzrs: Iterable[messages.AuthorizationResource]\n                           ) -> list[achallenges.AnnotatedChallenge]:\n        \"\"\"\n        Retrieve necessary and pending challenges to satisfy server.\n        NB: Necessary and already validated challenges are not retrieved,\n        as they can be reused for a certificate issuance.\n        \"\"\"\n        if not self.acme:\n            raise errors.Error(\"No ACME client defined, cannot choose the challenges.\")\n\n        pending_authzrs = [authzr for authzr in authzrs\n                           if authzr.body.status != messages.STATUS_VALID]\n        achalls: list[achallenges.AnnotatedChallenge] = []\n        if pending_authzrs:\n            logger.info(\"Performing the following challenges:\")\n        for authzr in pending_authzrs:\n            authzr_challenges = authzr.body.challenges\n\n            path = gen_challenge_path(\n                authzr_challenges,\n                self._get_chall_pref(authzr.body.identifier.value))\n\n            achalls.extend(self._challenge_factory(authzr, path))\n\n        return achalls\n\n    def _get_chall_pref(self, identifier: str) -> list[type[challenges.Challenge]]:\n        \"\"\"Return list of challenge preferences.\n\n        :param str domain: domain for which you are requesting preferences\n\n        \"\"\"\n        chall_prefs = []\n        # The 'identifier' parameter of `get_chall_pref` used to be called `domain`.\n        # There may be plugins in the wild that name their parameter `domain`. To keep\n        # working with those plugins, make sure to continue to pass `identifier` as a\n        # positional parameter rather than a kwarg.\n        # Also, make sure to make a copy...\n        plugin_pref = self.auth.get_chall_pref(identifier)\n        if self.pref_challs:\n            plugin_pref_types = {chall.typ for chall in plugin_pref}\n            for typ in self.pref_challs:\n                if typ in plugin_pref_types:\n                    chall_prefs.append(challenges.Challenge.TYPES[typ])\n            if chall_prefs:\n                return chall_prefs\n            raise errors.AuthorizationError(\n                \"None of the preferred challenges \"\n                \"are supported by the selected plugin\")\n        chall_prefs.extend(plugin_pref)\n        return chall_prefs\n\n    def _cleanup_challenges(self, achalls: list[achallenges.AnnotatedChallenge]) -> None:\n        \"\"\"Cleanup challenges.\n\n        :param achalls: annotated challenges to cleanup\n        :type achalls: `list` of :class:`certbot.achallenges.AnnotatedChallenge`\n\n        \"\"\"\n        logger.info(\"Cleaning up challenges\")\n        self.auth.cleanup(achalls)\n\n    def _challenge_factory(self, authzr: messages.AuthorizationResource,\n                           path: Sequence[int]) -> list[achallenges.AnnotatedChallenge]:\n        \"\"\"Construct Namedtuple Challenges\n\n        :param messages.AuthorizationResource authzr: authorization\n\n        :param list path: List of indices from `challenges`.\n\n        :returns: achalls, list of challenge type\n            :class:`certbot.achallenges.AnnotatedChallenge`\n        :rtype: list\n\n        :raises .errors.Error: if challenge type is not recognized\n\n        \"\"\"\n        if not self.account:\n            raise errors.Error(\"Account is not set.\")\n        achalls = []\n\n        for index in path:\n            challb = authzr.body.challenges[index]\n            achalls.append(challb_to_achall(challb, self.account.key, authzr.body.identifier))\n\n        return achalls\n\n    def _report_failed_authzrs(self, failed_authzrs: list[messages.AuthorizationResource]) -> None:\n        \"\"\"Notifies the user about failed authorizations.\"\"\"\n        if not self.account:\n            raise errors.Error(\"Account is not set.\")\n        problems: dict[str, list[achallenges.AnnotatedChallenge]] = {}\n        failed_achalls = [challb_to_achall(challb, self.account.key, authzr.body.identifier)\n                        for authzr in failed_authzrs for challb in authzr.body.challenges\n                        if challb.error]\n\n        for achall in failed_achalls:\n            problems.setdefault(achall.error.typ, []).append(achall)\n\n        msg = [\"\\nCertbot failed to authenticate some domains \"\n               f\"(authenticator: {self.auth.name}).\"\n               \" The Certificate Authority reported these problems:\"]\n\n        for _, achalls in sorted(problems.items(), key=lambda item: item[0]):\n            msg.append(_generate_failed_chall_msg(achalls))\n\n        # auth_hint will only be called on authenticators that subclass\n        # plugin_common.Plugin. Refer to comment on that function.\n        if failed_achalls and isinstance(self.auth, plugin_common.Plugin):\n            msg.append(f\"\\nHint: {self.auth.auth_hint(failed_achalls)}\\n\")\n\n        display_util.notify(\"\".join(msg))\n\n    def _debug_challenges_msg(self, achalls: list[achallenges.AnnotatedChallenge],\n                              config: configuration.NamespaceConfig) -> str:\n        \"\"\"Construct message for debug challenges prompt\n\n        :param list achalls: A list of\n            :class:`certbot.achallenges.AnnotatedChallenge`.\n        :param certbot.configuration.NamespaceConfig config: current Certbot configuration\n        :returns: Message containing challenge debug info\n        :rtype: str\n\n        \"\"\"\n        if config.verbose_count > 0:\n            msg = []\n            http01_achalls = {}\n            dns01_achalls = {}\n            for achall in achalls:\n                if isinstance(achall.chall, challenges.HTTP01):\n                    http01_achalls[achall.chall.uri(achall.identifier.value)] = (\n                        achall.validation(achall.account_key) + \"\\n\"\n                    )\n                if isinstance(achall.chall, challenges.DNS01):\n                    dns01_achalls[achall.validation_domain_name(achall.identifier.value)] = (\n                        achall.validation(achall.account_key) + \"\\n\"\n                    )\n            if http01_achalls:\n                msg.append(\"The following URLs should be accessible from the \"\n                           \"internet and return the value mentioned:\\n\")\n                for uri, key_authz in http01_achalls.items():\n                    msg.append(f\"URL: {uri}\\nExpected value: {key_authz}\")\n            if dns01_achalls:\n                msg.append(\"The following FQDNs should return a TXT resource \"\n                           \"record with the value mentioned:\\n\")\n                for fqdn, key_authz_hash in dns01_achalls.items():\n                    msg.append(f\"FQDN: {fqdn}\\nExpected value: {key_authz_hash}\")\n            return \"\\n\" + \"\\n\".join(msg)\n        else:\n            return 'Pass \"-v\" for more info about challenges.'\n\n\ndef challb_to_achall(challb: messages.ChallengeBody, account_key: josepy.JWK,\n                     identifier: messages.Identifier) -> achallenges.AnnotatedChallenge:\n    \"\"\"Converts a ChallengeBody object to an AnnotatedChallenge.\n\n    :param .ChallengeBody challb: ChallengeBody\n    :param .JWK account_key: Authorized Account Key\n    :param str domain: Domain of the challb\n\n    :returns: Appropriate AnnotatedChallenge\n    :rtype: :class:`certbot.achallenges.AnnotatedChallenge`\n\n    \"\"\"\n    chall = challb.chall\n    logger.info(\"%s challenge for %s\", chall.typ, identifier)\n\n    if isinstance(chall, challenges.KeyAuthorizationChallenge):\n        return achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=challb, account_key=account_key, identifier=identifier)\n    elif isinstance(chall, challenges.DNS):\n        return achallenges.DNS(challb=challb, identifier=identifier)\n    else:\n        return achallenges.Other(challb=challb, identifier=identifier)\n\n\ndef gen_challenge_path(challbs: list[messages.ChallengeBody],\n                       preferences: list[type[challenges.Challenge]]) -> tuple[int, ...]:\n    \"\"\"Generate a plan to get authority over the identity.\n\n    :param tuple challbs: A tuple of challenges\n        (:class:`acme.messages.Challenge`) from\n        :class:`acme.messages.AuthorizationResource` to be\n        fulfilled by the client in order to prove possession of the\n        identifier.\n\n    :param list preferences: List of challenge preferences for domain\n        (:class:`acme.challenges.Challenge` subclasses)\n\n    :returns: list of indices from ``challenges``.\n    :rtype: list\n\n    :raises certbot.errors.AuthorizationError: If a\n        path cannot be created that satisfies the CA given the preferences and\n        combinations.\n\n    \"\"\"\n    chall_cost = {}\n    max_cost = 1\n    for i, chall_cls in enumerate(preferences):\n        chall_cost[chall_cls] = i\n        max_cost += i\n\n    # max_cost is now equal to sum(indices) + 1\n\n    best_combo: Optional[tuple[int, ...]] = None\n    # Set above completing all of the available challenges\n    best_combo_cost = max_cost\n\n    combinations = tuple((i,) for i in range(len(challbs)))\n\n    combo_total = 0\n    for combo in combinations:\n        for challenge_index in combo:\n            combo_total += chall_cost.get(challbs[\n                challenge_index].chall.__class__, max_cost)\n\n        if combo_total < best_combo_cost:\n            best_combo = combo\n            best_combo_cost = combo_total\n\n        combo_total = 0\n\n    if not best_combo:\n        raise _report_no_chall_path(challbs)\n\n    return best_combo\n\n\ndef _report_no_chall_path(challbs: list[messages.ChallengeBody]) -> errors.AuthorizationError:\n    \"\"\"Logs and return a raisable error reporting that no satisfiable chall path exists.\n\n    :param challbs: challenges from the authorization that can't be satisfied\n\n    :returns: An authorization error\n    :rtype: certbot.errors.AuthorizationError\n\n    \"\"\"\n    msg = (\"Client with the currently selected authenticator does not support \"\n           \"any combination of challenges that will satisfy the CA.\")\n    if len(challbs) == 1 and isinstance(challbs[0].chall, challenges.DNS01):\n        msg += (\n            \" You may need to use an authenticator \"\n            \"plugin that can do challenges over DNS.\")\n    logger.critical(msg)\n    return errors.AuthorizationError(msg)\n\n\ndef _generate_failed_chall_msg(failed_achalls: list[achallenges.AnnotatedChallenge]) -> str:\n    \"\"\"Creates a user friendly error message about failed challenges.\n\n    :param list failed_achalls: A list of failed\n        :class:`certbot.achallenges.AnnotatedChallenge` with the same error\n        type.\n    :returns: A formatted error message for the client.\n    :rtype: str\n\n    \"\"\"\n    error = failed_achalls[0].error\n    typ = error.typ\n    if messages.is_acme_error(error):\n        typ = error.code\n    msg = []\n\n    for achall in failed_achalls:\n        msg.append(\"\\n  Identifier: %s\\n  Type:   %s\\n  Detail: %s\\n\" % (\n            achall.identifier.value, typ, achall.error.detail))\n\n    return \"\".join(msg)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cert_manager.py",
    "content": "\"\"\"Tools for managing certificates.\"\"\"\nimport datetime\nimport logging\nimport re\nimport traceback\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TypeVar\nfrom typing import Union\n\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import ocsp\nfrom certbot._internal import san\nfrom certbot._internal import storage\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\n\nlogger = logging.getLogger(__name__)\n\n###################\n# Commands\n###################\n\n\ndef certificates(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Display information about certs configured with Certbot\n\n    :param config: Configuration.\n    :type config: :class:`certbot._internal.configuration.NamespaceConfig`\n    \"\"\"\n    parsed_certs = []\n    parse_failures = []\n    for renewal_file in storage.renewal_conf_files(config):\n        try:\n            renewal_candidate = storage.RenewableCert(renewal_file, config)\n            crypto_util.verify_renewable_cert(renewal_candidate)\n            parsed_certs.append(renewal_candidate)\n        except Exception as e:  # pylint: disable=broad-except\n            logger.warning(\"Renewal configuration file %s produced an \"\n                           \"unexpected error: %s. Skipping.\", renewal_file, e)\n            logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n            parse_failures.append(renewal_file)\n\n    # Describe all the certs\n    _describe_certs(config, parsed_certs, parse_failures)\n\n\ndef delete(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Delete Certbot files associated with a certificate lineage.\"\"\"\n    certnames = get_certnames(config, \"delete\", allow_multiple=True)\n    msg = [\"The following certificate(s) are selected for deletion:\\n\"]\n    for certname in certnames:\n        msg.append(\"  * \" + certname)\n    msg.append(\n        \"\\nWARNING: Before continuing, ensure that the listed certificates are not being used \"\n        \"by any installed server software (e.g. Apache, nginx, mail servers). Deleting a \"\n        \"certificate that is still being used will cause the server software to stop working. \"\n        \"See https://certbot.org/deleting-certs for information on deleting certificates safely.\"\n    )\n    msg.append(\"\\nAre you sure you want to delete the above certificate(s)?\")\n    if not display_util.yesno(\"\\n\".join(msg), default=True):\n        logger.info(\"Deletion of certificate(s) canceled.\")\n        return\n    for certname in certnames:\n        storage.delete_files(config, certname)\n        display_util.notify(\"Deleted all files relating to certificate {0}.\"\n                            .format(certname))\n\n###################\n# Public Helpers\n###################\n\n\ndef lineage_for_certname(cli_config: configuration.NamespaceConfig,\n                         certname: str) -> Optional[storage.RenewableCert]:\n    \"\"\"Find a lineage object with name certname.\"\"\"\n    configs_dir = cli_config.renewal_configs_dir\n    # Verify the directory is there\n    util.make_or_verify_dir(configs_dir, mode=0o755)\n    try:\n        renewal_file = storage.renewal_file_for_certname(cli_config, certname)\n    except errors.CertStorageError:\n        return None\n    try:\n        return storage.RenewableCert(renewal_file, cli_config)\n    except (OSError, errors.CertStorageError):\n        logger.debug(\"Renewal conf file %s is broken.\", renewal_file)\n        logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n        return None\n\n\ndef sans_for_certname(config: configuration.NamespaceConfig,\n                      certname: str) -> Optional[list[san.SAN]]:\n    \"\"\"Find the domains and/or IP addresses in the cert with name certname.\"\"\"\n    lineage = lineage_for_certname(config, certname)\n    return lineage.sans() if lineage else None\n\n\ndef find_duplicative_certs(config: configuration.NamespaceConfig,\n                           sans: list[san.SAN]) -> tuple[Optional[storage.RenewableCert],\n                                                         Optional[storage.RenewableCert]]:\n    \"\"\"Find existing certs that match the given domain names.\n\n    This function searches for certificates whose domains are equal to\n    the `domains` parameter and certificates whose domains are a subset\n    of the domains in the `domains` parameter. If multiple certificates\n    are found whose names are a subset of `domains`, the one whose names\n    are the largest subset of `domains` is returned.\n\n    If multiple certificates' domains are an exact match or equally\n    sized subsets, which matching certificates are returned is\n    undefined.\n\n    :param config: Configuration.\n    :type config: :class:`certbot._internal.configuration.NamespaceConfig`\n    :param domains: List of domain names\n    :type domains: `list` of `str`\n\n    :returns: lineages representing the identically matching cert and the\n        largest subset if they exist\n    :rtype: `tuple` of `storage.RenewableCert` or `None`\n\n    \"\"\"\n    def update_certs_for_domain_matches(candidate_lineage: storage.RenewableCert,\n                                        rv: tuple[Optional[storage.RenewableCert],\n                                                  Optional[storage.RenewableCert]]\n                                        ) -> tuple[Optional[storage.RenewableCert],\n                                                   Optional[storage.RenewableCert]]:\n        \"\"\"Return cert as identical_names_cert if it matches,\n           or subset_names_cert if it matches as subset\n        \"\"\"\n        # TODO: Handle these differently depending on whether they are\n        #       expired or still valid?\n        identical_names_cert, subset_names_cert = rv\n        candidate_names = set(candidate_lineage.sans())\n        if candidate_names == set(sans):\n            identical_names_cert = candidate_lineage\n        elif candidate_names.issubset(set(sans)):\n            # This logic finds and returns the largest subset-names cert\n            # in the case where there are several available.\n            if subset_names_cert is None:\n                subset_names_cert = candidate_lineage\n            elif len(candidate_names) > len(subset_names_cert.sans()):\n                subset_names_cert = candidate_lineage\n        return (identical_names_cert, subset_names_cert)\n\n    init: tuple[Optional[storage.RenewableCert], Optional[storage.RenewableCert]] = (None, None)\n\n    return _search_lineages(config, update_certs_for_domain_matches, init)\n\n\ndef _archive_files(candidate_lineage: storage.RenewableCert, filetype: str) -> Optional[list[str]]:\n    \"\"\" In order to match things like:\n        /etc/letsencrypt/archive/example.com/chain1.pem.\n\n        Anonymous functions which call this function are eventually passed (in a list) to\n        `match_and_check_overlaps` to help specify the acceptable_matches.\n\n        :param `.storage.RenewableCert` candidate_lineage: Lineage whose archive dir is to\n            be searched.\n        :param str filetype: main file name prefix e.g. \"fullchain\" or \"chain\".\n\n        :returns: Files in candidate_lineage's archive dir that match the provided filetype.\n        :rtype: list of str or None\n    \"\"\"\n    archive_dir = candidate_lineage.archive_dir\n    pattern = [os.path.join(archive_dir, f) for f in os.listdir(archive_dir)\n                    if re.match(\"{0}[0-9]*.pem\".format(filetype), f)]\n    if pattern:\n        return pattern\n    return None\n\n\ndef _acceptable_matches() -> list[Union[Callable[[storage.RenewableCert], str],\n                                        Callable[[storage.RenewableCert], Optional[list[str]]]]]:\n    \"\"\" Generates the list that's passed to match_and_check_overlaps. Is its own function to\n    make unit testing easier.\n\n    :returns: list of functions\n    :rtype: list\n    \"\"\"\n    return [lambda x: x.fullchain_path, lambda x: x.cert_path,\n            lambda x: _archive_files(x, \"cert\"), lambda x: _archive_files(x, \"fullchain\")]\n\n\ndef cert_path_to_lineage(cli_config: configuration.NamespaceConfig) -> str:\n    \"\"\" If config.cert_path is defined, try to find an appropriate value for config.certname.\n\n    :param `configuration.NamespaceConfig` cli_config: parsed command line arguments\n\n    :returns: a lineage name\n    :rtype: str\n\n    :raises `errors.Error`: If the specified cert path can't be matched to a lineage name.\n    :raises `errors.OverlappingMatchFound`: If the matched lineage's archive is shared.\n    \"\"\"\n    acceptable_matches = _acceptable_matches()\n    match = match_and_check_overlaps(cli_config, acceptable_matches,\n                                     lambda x: cli_config.cert_path, lambda x: x.lineagename)\n    return match[0]\n\n\ndef match_and_check_overlaps(cli_config: configuration.NamespaceConfig,\n                             acceptable_matches: Iterable[Union[\n                                 Callable[[storage.RenewableCert], str],\n                                 Callable[[storage.RenewableCert], Optional[list[str]]]]],\n                             match_func: Callable[[storage.RenewableCert], str],\n                             rv_func: Callable[[storage.RenewableCert], str]) -> list[str]:\n    \"\"\" Searches through all lineages for a match, and checks for duplicates.\n    If a duplicate is found, an error is raised, as performing operations on lineages\n    that have their properties incorrectly duplicated elsewhere is probably a bad idea.\n\n    :param `configuration.NamespaceConfig` cli_config: parsed command line arguments\n    :param list acceptable_matches: a list of functions that specify acceptable matches\n    :param function match_func: specifies what to match\n    :param function rv_func: specifies what to return\n\n    \"\"\"\n    def find_matches(candidate_lineage: storage.RenewableCert, return_value: list[str],\n                     acceptable_matches: Iterable[Union[\n                         Callable[[storage.RenewableCert], str],\n                         Callable[[storage.RenewableCert], Optional[list[str]]]]]) -> list[str]:\n        \"\"\"Returns a list of matches using _search_lineages.\"\"\"\n        acceptable_matches_resolved = [func(candidate_lineage) for func in acceptable_matches]\n        acceptable_matches_rv: list[str] = []\n        for item in acceptable_matches_resolved:\n            if isinstance(item, list):\n                acceptable_matches_rv += item\n            elif item:\n                acceptable_matches_rv.append(item)\n        match = match_func(candidate_lineage)\n        if match in acceptable_matches_rv:\n            return_value.append(rv_func(candidate_lineage))\n        return return_value\n\n    matched: list[str] = _search_lineages(cli_config, find_matches, [], acceptable_matches)\n    if not matched:\n        raise errors.Error(f\"No match found for cert-path {cli_config.cert_path}!\")\n    elif len(matched) > 1:\n        raise errors.OverlappingMatchFound()\n    return matched\n\n\ndef human_readable_cert_info(config: configuration.NamespaceConfig, cert: storage.RenewableCert,\n                             skip_filter_checks: bool = False) -> Optional[str]:\n    \"\"\" Returns a human readable description of info about a RenewableCert object\"\"\"\n    certinfo = []\n    checker = ocsp.RevocationChecker()\n\n    config_sans = set(config.domains + config.ip_addresses)\n\n    if config.certname and cert.lineagename != config.certname and not skip_filter_checks:\n        return None\n    if config_sans and not config_sans.issubset(cert.sans()):\n        return None\n    now = datetime.datetime.now(datetime.timezone.utc)\n\n    reasons = []\n    if cert.is_test_cert:\n        reasons.append('TEST_CERT')\n    if cert.target_expiry <= now:\n        reasons.append('EXPIRED')\n    elif checker.ocsp_revoked(cert):\n        reasons.append('REVOKED')\n\n    if reasons:\n        status = \"INVALID: \" + \", \".join(reasons)\n    else:\n        diff = cert.target_expiry - now\n        if diff.days == 1:\n            status = \"VALID: 1 day\"\n        elif diff.days < 1:\n            status = f\"VALID: {diff.seconds // 3600} hour(s)\"\n        else:\n            status = f\"VALID: {diff.days} days\"\n\n    valid_string = \"{0} ({1})\".format(cert.target_expiry, status)\n    serial = format(crypto_util.get_serial_from_cert(cert.cert_path), 'x')\n    certinfo.append(f\"  Certificate Name: {cert.lineagename}\\n\"\n                    f\"    Serial Number: {serial}\\n\"\n                    f\"    Key Type: {cert.private_key_type}\\n\"\n                    f'    Identifiers: {\" \".join(map(str, cert.sans()))}\\n'\n                    f\"    Expiry Date: {valid_string}\\n\"\n                    f\"    Certificate Path: {cert.fullchain}\\n\"\n                    f\"    Private Key Path: {cert.privkey}\")\n    return \"\".join(certinfo)\n\n\ndef get_certnames(config: configuration.NamespaceConfig, verb: str, allow_multiple: bool = False,\n                  custom_prompt: Optional[str] = None) -> list[str]:\n    \"\"\"Get certname from flag, interactively, or error out.\"\"\"\n    certname = config.certname\n    if certname:\n        certnames = [certname]\n    else:\n        filenames = storage.renewal_conf_files(config)\n        choices = [storage.lineagename_for_filename(name) for name in filenames]\n        if not choices:\n            raise errors.Error(\"No existing certificates found.\")\n        if allow_multiple:\n            if not custom_prompt:\n                prompt = \"Which certificate(s) would you like to {0}?\".format(verb)\n            else:\n                prompt = custom_prompt\n            code, certnames = display_util.checklist(\n                prompt, choices, cli_flag=\"--cert-name\", force_interactive=True)\n            if code != display_util.OK:\n                raise errors.Error(\"User ended interaction.\")\n        else:\n            if not custom_prompt:\n                prompt = \"Which certificate would you like to {0}?\".format(verb)\n            else:\n                prompt = custom_prompt\n\n            code, index = display_util.menu(\n                prompt, choices, cli_flag=\"--cert-name\", force_interactive=True)\n\n            if code != display_util.OK or index not in range(0, len(choices)):\n                raise errors.Error(\"User ended interaction.\")\n            certnames = [choices[index]]\n    return certnames\n\n###################\n# Private Helpers\n###################\n\n\ndef _report_lines(msgs: Iterable[str]) -> str:\n    \"\"\"Format a results report for a category of single-line renewal outcomes\"\"\"\n    return \"  \" + \"\\n  \".join(str(msg) for msg in msgs)\n\n\ndef _report_human_readable(config: configuration.NamespaceConfig,\n                           parsed_certs: Iterable[storage.RenewableCert]) -> str:\n    \"\"\"Format a results report for a parsed cert\"\"\"\n    certinfo = []\n    for cert in parsed_certs:\n        cert_info = human_readable_cert_info(config, cert)\n        if cert_info is not None:\n            certinfo.append(cert_info)\n    return \"\\n\".join(certinfo)\n\n\ndef _describe_certs(config: configuration.NamespaceConfig,\n                    parsed_certs: Iterable[storage.RenewableCert],\n                    parse_failures: Iterable[str]) -> None:\n    \"\"\"Print information about the certs we know about\"\"\"\n    out: list[str] = []\n\n    notify = out.append\n\n    if not parsed_certs and not parse_failures:\n        notify(\"No certificates found.\")\n    else:\n        if parsed_certs:\n            if config.certname or config.domains or config.ip_addresses:\n                notify(\"Found the following matching certs:\")\n            else:\n                notify(\"Found the following certs:\")\n            notify(_report_human_readable(config, parsed_certs))\n        if parse_failures:\n            notify(\"\\nThe following renewal configurations \"\n               \"were invalid:\")\n            notify(_report_lines(parse_failures))\n\n    display_util.notification(\"\\n\".join(out), pause=False, wrap=False)\n\n\nT = TypeVar('T')\n\ndef _search_lineages(cli_config: configuration.NamespaceConfig, func: Callable[..., T],\n                     initial_rv: T, *args: Any) -> T:\n    \"\"\"Iterate func over unbroken lineages, allowing custom return conditions.\n\n    Allows flexible customization of return values, including multiple\n    return values and complex checks.\n\n    :param `configuration.NamespaceConfig` cli_config: parsed command line arguments\n    :param function func: function used while searching over lineages\n    :param initial_rv: initial return value of the function (any type)\n\n    :returns: Whatever was specified by `func` if a match is found.\n    \"\"\"\n    configs_dir = cli_config.renewal_configs_dir\n    # Verify the directory is there\n    util.make_or_verify_dir(configs_dir, mode=0o755)\n\n    rv = initial_rv\n    for renewal_file in storage.renewal_conf_files(cli_config):\n        try:\n            candidate_lineage = storage.RenewableCert(renewal_file, cli_config)\n        except (OSError, errors.CertStorageError):\n            logger.debug(\"Renewal conf file %s is broken. Skipping.\", renewal_file)\n            logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n            continue\n        rv = func(candidate_lineage, rv, *args)\n    return rv\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/__init__.py",
    "content": "\"\"\"Certbot command line argument & config processing.\"\"\"\n# pylint: disable=too-many-lines\n# ruff: noqa: F401\nimport argparse\nimport logging\nimport logging.handlers\nimport sys\nfrom typing import Any\nfrom typing import List\nfrom typing import Optional\nfrom typing import Type\n\nimport certbot\nfrom certbot.configuration import NamespaceConfig\nfrom certbot._internal import constants\nfrom certbot._internal.cli.cli_constants import ARGPARSE_PARAMS_TO_REMOVE\nfrom certbot._internal.cli.cli_constants import cli_command\nfrom certbot._internal.cli.cli_constants import COMMAND_OVERVIEW\nfrom certbot._internal.cli.cli_constants import DEPRECATED_OPTIONS\nfrom certbot._internal.cli.cli_constants import EXIT_ACTIONS\nfrom certbot._internal.cli.cli_constants import HELP_AND_VERSION_USAGE\nfrom certbot._internal.cli.cli_constants import SHORT_USAGE\nfrom certbot._internal.cli.cli_constants import VAR_MODIFIERS\nfrom certbot._internal.cli.cli_constants import ZERO_ARG_ACTIONS\nfrom certbot._internal.cli.cli_utils import _EncodeReasonAction\nfrom certbot._internal.cli.cli_utils import _PrefChallAction\nfrom certbot._internal.cli.cli_utils import _user_agent_comment_type\nfrom certbot._internal.cli.cli_utils import add_dns_name\nfrom certbot._internal.cli.cli_utils import add_ip_address\nfrom certbot._internal.cli.cli_utils import CaseInsensitiveList\nfrom certbot._internal.cli.cli_utils import config_help\nfrom certbot._internal.cli.cli_utils import CustomHelpFormatter\nfrom certbot._internal.cli.cli_utils import DomainsAction\nfrom certbot._internal.cli.cli_utils import IPAddressAction\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot._internal.cli.cli_utils import HelpfulArgumentGroup\nfrom certbot._internal.cli.cli_utils import nonnegative_int\nfrom certbot._internal.cli.cli_utils import parse_preferred_challenges\nfrom certbot._internal.cli.cli_utils import read_file\nfrom certbot._internal.cli.cli_utils import set_test_server_options\nfrom certbot._internal.cli.group_adder import _add_all_groups\nfrom certbot._internal.cli.helpful import HelpfulArgumentParser\nfrom certbot._internal.cli.paths_parser import _paths_parser\nfrom certbot._internal.cli.plugins_parsing import _plugins_parsing\nfrom certbot._internal.cli.subparsers import _create_subparsers\nfrom certbot._internal.cli.verb_help import VERB_HELP\nfrom certbot._internal.cli.verb_help import VERB_HELP_MAP\nfrom certbot._internal.plugins import disco as plugins_disco\nimport certbot._internal.plugins.selection as plugin_selection\nfrom certbot.plugins import enhancements\n\nlogger = logging.getLogger(__name__)\n\n\n# Global, to save us from a lot of argument passing within the scope of this module\nhelpful_parser: Optional[HelpfulArgumentParser] = None\n\n\ndef prepare_and_parse_args(plugins: plugins_disco.PluginsRegistry, args: list[str]\n                           ) -> NamespaceConfig:\n    \"\"\"Returns parsed command line arguments.\n\n    :param .PluginsRegistry plugins: available plugins\n    :param list args: command line arguments with the program name removed\n\n    :returns: parsed command line arguments\n    :rtype: configuration.NamespaceConfig\n\n    \"\"\"\n\n    helpful = HelpfulArgumentParser(args, plugins)\n    _add_all_groups(helpful)\n\n    # --help is automatically provided by argparse\n    helpful.add(\n        None, \"-v\", \"--verbose\", dest=\"verbose_count\", action=\"count\",\n        default=flag_default(\"verbose_count\"), help=\"This flag can be used \"\n        \"multiple times to incrementally increase the verbosity of output, \"\n        \"e.g. -vvv.\")\n    # This is for developers to set the level in the cli.ini, and overrides\n    # the --verbose flag\n    helpful.add(\n        None, \"--verbose-level\", dest=\"verbose_level\",\n        default=flag_default(\"verbose_level\"), help=argparse.SUPPRESS)\n    helpful.add(\n        None, \"-t\", \"--text\", dest=\"text_mode\", action=\"store_true\",\n        default=flag_default(\"text_mode\"), help=argparse.SUPPRESS)\n    helpful.add(\n        None, \"--max-log-backups\", type=nonnegative_int,\n        default=flag_default(\"max_log_backups\"),\n        help=\"Specifies the maximum number of backup logs that should \"\n             \"be kept by Certbot's built in log rotation. Setting this \"\n             \"flag to 0 disables log rotation entirely, causing \"\n             \"Certbot to always append to the same log file.\")\n    helpful.add(\n        None, \"--preconfigured-renewal\", dest=\"preconfigured_renewal\",\n        action=\"store_true\", default=flag_default(\"preconfigured_renewal\"),\n        help=argparse.SUPPRESS\n    )\n    helpful.add(\n        [None, \"automation\", \"run\", \"certonly\", \"enhance\"],\n        \"-n\", \"--non-interactive\", \"--noninteractive\",\n        dest=\"noninteractive_mode\", action=\"store_true\",\n        default=flag_default(\"noninteractive_mode\"),\n        help=\"Run without ever asking for user input. This may require \"\n              \"additional command line flags; the client will try to explain \"\n              \"which ones are required if it finds one missing\")\n    helpful.add(\n        [None, \"register\", \"run\", \"certonly\", \"enhance\"],\n        constants.FORCE_INTERACTIVE_FLAG, action=\"store_true\",\n        default=flag_default(\"force_interactive\"),\n        help=\"Force Certbot to be interactive even if it detects it's not \"\n             \"being run in a terminal. This flag cannot be used with the \"\n             \"renew subcommand.\")\n    helpful.add(\n        [None, \"run\", \"certonly\", \"certificates\", \"enhance\"],\n        \"-d\", \"--domains\", \"--domain\", dest=\"domains\",\n        metavar=\"DOMAIN\", action=DomainsAction,\n        default=flag_default(\"domains\"),\n        help=\"Domain names to include. For multiple domains you can use multiple -d flags \"\n             \"or enter a comma separated list of domains as a parameter. All domains will \"\n             \"be included as Subject Alternative Names on the certificate. The first domain \"\n             \"will be used as the certificate name, unless otherwise specified or if you \"\n             \"already have a certificate with the same name. In the case of a name conflict, \"\n             \"a number like -0001 will be appended to the certificate name. (default: Ask)\")\n    helpful.add(\n        [None, \"certonly\", \"certificates\"],\n        \"--ip-address\", dest=\"ip_addresses\",\n        action=IPAddressAction,\n        default=flag_default(\"ip_addresses\"),\n        help=\"IP addresses to include. For multiple IP addresses you can use multiple \"\n             \"--ip-address flags. All IP addresses will be included as Subject Alternative Names \"\n             \"on the certificate.\")\n    helpful.add(\n        [None, \"run\", \"certonly\", \"register\"],\n        \"--eab-kid\", dest=\"eab_kid\",\n        metavar=\"EAB_KID\",\n        help=\"Key Identifier for External Account Binding\"\n    )\n    helpful.add(\n        [None, \"run\", \"certonly\", \"register\"],\n        \"--eab-hmac-key\", dest=\"eab_hmac_key\",\n        metavar=\"EAB_HMAC_KEY\",\n        help=\"HMAC key for External Account Binding\"\n    )\n    helpful.add(\n        [None, \"run\", \"certonly\", \"register\"],\n        \"--eab-hmac-alg\", dest=\"eab_hmac_alg\",\n        metavar=\"EAB_HMAC_ALG\",\n        default=flag_default(\"eab_hmac_alg\"),\n        help=\"HMAC algorithm for External Account Binding\"\n    )\n    helpful.add(\n        [None, \"run\", \"certonly\", \"manage\", \"delete\", \"certificates\",\n         \"renew\", \"enhance\", \"reconfigure\"], \"--cert-name\", dest=\"certname\",\n        metavar=\"CERTNAME\", default=flag_default(\"certname\"),\n        help=\"Certificate name to apply. This name is used by Certbot for housekeeping \"\n             \"and in file paths; it doesn't affect the content of the certificate itself. \"\n             \"Certificate name cannot contain filepath separators (i.e. '/' or '\\\\', depending \"\n             \"on the platform). \"\n             \"To see certificate names, run 'certbot certificates'. \"\n             \"When creating a new certificate, specifies the new certificate's name. \"\n             \"(default: the first provided domain or the name of an existing \"\n             \"certificate on your system for the same domains)\")\n    helpful.add(\n        [None, \"testing\", \"renew\", \"certonly\"],\n        \"--dry-run\", action=\"store_true\", dest=\"dry_run\",\n        default=flag_default(\"dry_run\"),\n        help=\"Perform a test run against the Let's Encrypt staging server, obtaining test\"\n             \" (invalid) certificates but not saving them to disk. This can only be used with the\"\n             \" 'certonly' and 'renew' subcommands. It may trigger webserver reloads to \"\n             \" temporarily modify & roll back configuration files.\"\n             \" --pre-hook and --post-hook commands run by default.\"\n             \" --deploy-hook commands do not run, unless enabled by --run-deploy-hooks.\"\n             \" The test server may be overridden with --server.\")\n    helpful.add(\n        [\"testing\", \"renew\", \"certonly\", \"reconfigure\"],\n        \"--run-deploy-hooks\", action=\"store_true\", dest=\"run_deploy_hooks\",\n        default=flag_default(\"run_deploy_hooks\"),\n        help=\"When performing a test run using `--dry-run` or `reconfigure`, run any applicable\"\n             \" deploy hooks. This includes hooks set on the command line, saved in the\"\n             \" certificate's renewal configuration file, or present in the renewal-hooks directory.\"\n             \" To exclude directory hooks, use --no-directory-hooks. The hook(s) will only\"\n             \" be run if the dry run succeeds, and will use the current active certificate, not\"\n             \" the temporary test certificate acquired during the dry run. This flag is recommended\"\n             \" when modifying the deploy hook using `reconfigure`.\")\n    helpful.add(\n        [\"register\", \"automation\"], \"--register-unsafely-without-email\", action=\"store_true\",\n        default=flag_default(\"register_unsafely_without_email\"),\n        help=argparse.SUPPRESS)\n    helpful.add(\n        [\"register\", \"update_account\", \"unregister\", \"automation\"], \"-m\", \"--email\",\n        default=flag_default(\"email\"),\n        help=config_help(\"email\"))\n    helpful.add([\"register\", \"update_account\", \"automation\"], \"--eff-email\", action=\"store_true\",\n                default=flag_default(\"eff_email\"), dest=\"eff_email\",\n                help=\"Share your e-mail address with EFF (default: Ask)\")\n    helpful.add([\"register\", \"update_account\", \"automation\"], \"--no-eff-email\",\n                action=\"store_false\", default=flag_default(\"eff_email\"), dest=\"eff_email\",\n                help=\"Don't share your e-mail address with EFF (default: Ask)\")\n    helpful.add(\n        [\"automation\", \"certonly\", \"run\"],\n        \"--keep-until-expiring\", \"--keep\", \"--reinstall\",\n        dest=\"reinstall\", action=\"store_true\", default=flag_default(\"reinstall\"),\n        help=\"If the requested certificate matches an existing certificate, always keep the \"\n             \"existing one until it is due for renewal (for the \"\n             \"'run' subcommand this means reinstall the existing certificate). (default: Ask)\")\n    helpful.add(\n        \"automation\", \"--expand\", action=\"store_true\", default=flag_default(\"expand\"),\n        help=\"If an existing certificate is a strict subset of the requested names, \"\n             \"always expand and replace it with the additional names. (default: Ask)\")\n    helpful.add(\n        \"automation\", \"--version\", action=\"version\",\n        version=\"%(prog)s {0}\".format(certbot.__version__),\n        help=\"show program's version number and exit\")\n    helpful.add(\n        [\"automation\", \"renew\"],\n        \"--force-renewal\", \"--renew-by-default\", dest=\"renew_by_default\",\n        action=\"store_true\", default=flag_default(\"renew_by_default\"),\n        help=\"If a certificate \"\n             \"already exists for the requested domains, renew it now, \"\n             \"regardless of whether it is near expiry. (Often \"\n             \"--keep-until-expiring is more appropriate). Also implies \"\n             \"--expand.\")\n    helpful.add(\n        \"automation\", \"--renew-with-new-domains\", dest=\"renew_with_new_domains\",\n        action=\"store_true\", default=flag_default(\"renew_with_new_domains\"),\n        help=\"If a \"\n             \"certificate already exists for the requested certificate name \"\n             \"but does not match the requested domains, renew it now, \"\n             \"regardless of whether it is near expiry.\")\n    helpful.add(\n        \"automation\", \"--reuse-key\", dest=\"reuse_key\",\n        action=\"store_true\", default=flag_default(\"reuse_key\"),\n        help=\"When renewing, use the same private key as the existing \"\n             \"certificate.\")\n    helpful.add(\n        \"automation\", \"--no-reuse-key\", dest=\"reuse_key\",\n        action=\"store_false\", default=flag_default(\"reuse_key\"),\n        help=\"When renewing, do not use the same private key as the existing \"\n             \"certificate. Not reusing private keys is the default behavior of \"\n             \"Certbot. This option may be used to unset --reuse-key on an \"\n             \"existing certificate.\")\n    helpful.add(\n        \"automation\", \"--new-key\",\n        dest=\"new_key\", action=\"store_true\", default=flag_default(\"new_key\"),\n        help=\"When renewing or replacing a certificate, generate a new private key, \"\n             \"even if --reuse-key is set on the existing certificate. Combining \"\n             \"--new-key and --reuse-key will result in the private key being replaced and \"\n             \"then reused in future renewals.\")\n\n    helpful.add(\n        [\"automation\", \"renew\", \"certonly\"],\n        \"--allow-subset-of-names\", action=\"store_true\",\n        default=flag_default(\"allow_subset_of_names\"),\n        help=\"When performing domain validation, do not consider it a failure \"\n             \"if authorizations can not be obtained for a strict subset of \"\n             \"the requested domains. This may be useful for allowing renewals for \"\n             \"multiple domains to succeed even if some domains no longer point \"\n             \"at this system. This option cannot be used with --csr.\")\n    helpful.add(\n        \"automation\", \"--agree-tos\", dest=\"tos\", action=\"store_true\",\n        default=flag_default(\"tos\"),\n        help=\"Agree to the ACME Subscriber Agreement (default: Ask)\")\n    helpful.add(\n        [\"unregister\", \"automation\"], \"--account\", metavar=\"ACCOUNT_ID\",\n        default=flag_default(\"account\"),\n        help=\"Account ID to use\")\n    helpful.add(\n        \"automation\", \"--duplicate\", dest=\"duplicate\", action=\"store_true\",\n        default=flag_default(\"duplicate\"),\n        help=\"Allow making a certificate lineage that duplicates an existing one \"\n             \"(both can be renewed in parallel)\")\n    helpful.add(\n        [\"automation\", \"renew\", \"certonly\", \"run\"],\n        \"-q\", \"--quiet\", dest=\"quiet\", action=\"store_true\",\n        default=flag_default(\"quiet\"),\n        help=\"Silence all output except errors. Useful for automation via cron.\"\n             \" Implies --non-interactive.\")\n    # overwrites server, handled in HelpfulArgumentParser.parse_args()\n    helpful.add([\"testing\", \"revoke\", \"run\"], \"--test-cert\", \"--staging\",\n        dest=\"staging\", action=\"store_true\", default=flag_default(\"staging\"),\n        help=\"Use the Let's Encrypt staging server to obtain or revoke test (invalid) \"\n             \"certificates; equivalent to --server \" + constants.STAGING_URI)\n    helpful.add(\n        \"testing\", \"--debug\", action=\"store_true\", default=flag_default(\"debug\"),\n        help=\"Show tracebacks in case of errors\")\n    helpful.add(\n        [None, \"certonly\", \"run\"], \"--debug-challenges\", action=\"store_true\",\n        default=flag_default(\"debug_challenges\"),\n        help=\"After setting up challenges, wait for user input before \"\n             \"submitting to CA. When used in combination with the `-v` \"\n             \"option, the challenge URLs or FQDNs and their expected \"\n             \"return values are shown.\")\n    helpful.add(\n        \"testing\", \"--no-verify-ssl\", action=\"store_true\",\n        help=config_help(\"no_verify_ssl\"),\n        default=flag_default(\"no_verify_ssl\"))\n    helpful.add(\n        [\"testing\", \"standalone\", \"manual\"], \"--http-01-port\", type=int,\n        dest=\"http01_port\",\n        default=flag_default(\"http01_port\"), help=config_help(\"http01_port\"))\n    helpful.add(\n        [\"testing\", \"standalone\"], \"--http-01-address\",\n        dest=\"http01_address\",\n        default=flag_default(\"http01_address\"), help=config_help(\"http01_address\"))\n    helpful.add(\n        [\"testing\", \"nginx\"], \"--https-port\", type=int,\n        default=flag_default(\"https_port\"),\n        help=config_help(\"https_port\"))\n    helpful.add(\n        \"testing\", \"--break-my-certs\", action=\"store_true\",\n        default=flag_default(\"break_my_certs\"),\n        help=\"Be willing to replace or renew valid certificates with invalid \"\n             \"(testing/staging) certificates\")\n    helpful.add(\n        \"security\", \"--rsa-key-size\", type=int, metavar=\"N\",\n        default=flag_default(\"rsa_key_size\"), help=config_help(\"rsa_key_size\"))\n    helpful.add(\n        \"security\", \"--key-type\", choices=['rsa', 'ecdsa'], type=str,\n        default=flag_default(\"key_type\"), help=config_help(\"key_type\"))\n    helpful.add(\n        \"security\", \"--elliptic-curve\", type=str, choices=[\n            'secp256r1',\n            'secp384r1',\n            'secp521r1',\n        ], metavar=\"N\",\n        default=flag_default(\"elliptic_curve\"), help=config_help(\"elliptic_curve\"))\n    helpful.add(\n        \"security\", \"--must-staple\", action=\"store_true\",\n        dest=\"must_staple\", default=flag_default(\"must_staple\"),\n        help=config_help(\"must_staple\"))\n    helpful.add(\n        [\"security\", \"enhance\"],\n        \"--redirect\", action=\"store_true\", dest=\"redirect\",\n        default=flag_default(\"redirect\"),\n        help=\"Automatically redirect all HTTP traffic to HTTPS for the newly \"\n             \"authenticated vhost. (default: redirect enabled for install and run, \"\n             \"disabled for enhance)\")\n    helpful.add(\n        \"security\", \"--no-redirect\", action=\"store_false\", dest=\"redirect\",\n        default=flag_default(\"redirect\"),\n        help=\"Do not automatically redirect all HTTP traffic to HTTPS for the newly \"\n             \"authenticated vhost. (default: redirect enabled for install and run, \"\n             \"disabled for enhance)\")\n    helpful.add(\n        [\"security\", \"enhance\"],\n        \"--hsts\", action=\"store_true\", dest=\"hsts\", default=flag_default(\"hsts\"),\n        help=\"Add the Strict-Transport-Security header to every HTTP response.\"\n             \" Forcing browser to always use SSL for the domain.\"\n             \" Defends against SSL Stripping. (default: False)\")\n    helpful.add(\n        \"security\", \"--no-hsts\", action=\"store_false\", dest=\"hsts\",\n        default=flag_default(\"hsts\"), help=argparse.SUPPRESS)\n    helpful.add(\n        [\"security\", \"enhance\"],\n        \"--uir\", action=\"store_true\", dest=\"uir\", default=flag_default(\"uir\"),\n        help='Add the \"Content-Security-Policy: upgrade-insecure-requests\"'\n             ' header to every HTTP response. Forcing the browser to use'\n             ' https:// for every http:// resource. (default: False)')\n    helpful.add(\n        \"security\", \"--no-uir\", action=\"store_false\", dest=\"uir\", default=flag_default(\"uir\"),\n        help=argparse.SUPPRESS)\n    helpful.add(\n        \"security\", \"--staple-ocsp\", action=\"store_true\", dest=\"staple\",\n        default=flag_default(\"staple\"),\n        help=\"Enables OCSP Stapling. A valid OCSP response is stapled to\"\n        \" the certificate that the server offers during TLS. (default: False)\")\n    helpful.add(\n        \"security\", \"--no-staple-ocsp\", action=\"store_false\", dest=\"staple\",\n        default=flag_default(\"staple\"), help=argparse.SUPPRESS)\n    helpful.add(\n        \"security\", \"--strict-permissions\", action=\"store_true\",\n        default=flag_default(\"strict_permissions\"),\n        help=\"Require that all configuration files are owned by the current \"\n             \"user; only needed if your config is somewhere unsafe like /tmp/\")\n    helpful.add(\n        [None, \"certonly\", \"renew\", \"run\"],\n        \"--required-profile\", dest=\"required_profile\",\n        default=flag_default(\"required_profile\"), help=config_help(\"required_profile\")\n    )\n    helpful.add(\n        [None, \"certonly\", \"renew\", \"run\"],\n        \"--preferred-profile\", dest=\"preferred_profile\",\n        default=flag_default(\"preferred_profile\"), help=config_help(\"preferred_profile\")\n    )\n    helpful.add(\n        [None, \"certonly\", \"renew\", \"run\"],\n        \"--preferred-chain\", dest=\"preferred_chain\",\n        default=flag_default(\"preferred_chain\"), help=config_help(\"preferred_chain\")\n    )\n    helpful.add(\n        [\"manual\", \"standalone\", \"certonly\", \"renew\"],\n        \"--preferred-challenges\", dest=\"pref_challs\",\n        action=_PrefChallAction, default=flag_default(\"pref_challs\"),\n        help='A sorted, comma delimited list of the preferred challenge to '\n             'use during authorization with the most preferred challenge '\n             'listed first (Eg, \"dns\" or \"http,dns\"). '\n             'Not all plugins support all challenges. See '\n             'https://certbot.eff.org/docs/using.html#plugins for details. '\n             'ACME Challenges are versioned, but if you pick \"http\" rather '\n             'than \"http-01\", Certbot will select the latest version '\n             'automatically.')\n    helpful.add(\n        [None, \"certonly\", \"run\"], \"--issuance-timeout\", type=nonnegative_int,\n        dest=\"issuance_timeout\",\n        default=flag_default(\"issuance_timeout\"),\n        help=config_help(\"issuance_timeout\"))\n    helpful.add(\n        [\"renew\", \"reconfigure\"], \"--pre-hook\",\n        help=\"Command to be run in a shell before obtaining any certificates.\"\n        \" Unless --disable-hook-validation is used, the command’s first word\"\n        \" must be the absolute pathname of an executable or one found via the\"\n        \" PATH environment variable.\"\n        \" Intended primarily for renewal, where it can be used to temporarily\"\n        \" shut down a webserver that might conflict with the standalone\"\n        \" plugin. This will only be called if a certificate is actually to be\"\n        \" obtained/renewed. When renewing several certificates that have\"\n        \" identical pre-hooks, only the first will be executed.\")\n    helpful.add(\n        [\"renew\", \"reconfigure\"], \"--post-hook\",\n        help=\"Command to be run in a shell after attempting to obtain/renew\"\n        \" certificates.\"\n        \" Unless --disable-hook-validation is used, the command’s first word\"\n        \" must be the absolute pathname of an executable or one found via the\"\n        \" PATH environment variable.\"\n        \" Can be used to deploy renewed certificates, or to\"\n        \" restart any servers that were stopped by --pre-hook. This is only\"\n        \" run if an attempt was made to obtain/renew a certificate. If\"\n        \" multiple renewed certificates have identical post-hooks, only\"\n        \" one will be run.\")\n    helpful.add([\"renew\", \"reconfigure\"], \"--renew-hook\",\n                dest=\"deploy_hook\", help=argparse.SUPPRESS)\n    helpful.add(\n        \"renew\", \"--no-random-sleep-on-renew\", action=\"store_false\",\n        default=flag_default(\"random_sleep_on_renew\"), dest=\"random_sleep_on_renew\",\n        help=argparse.SUPPRESS)\n    helpful.add(\n        [\"certonly\", \"renew\", \"reconfigure\", \"run\"], \"--deploy-hook\",\n        help='Command to be run in a shell once for each successfully'\n        ' issued certificate, including on subsequent renewals.'\n        ' Unless --disable-hook-validation is used, the command’s first word'\n        ' must be the absolute pathname of an executable or one found via the'\n        ' PATH environment variable.'\n        ' For this command, the shell variable'\n        ' $RENEWED_LINEAGE will point to the config live subdirectory'\n        ' (for example, \"/etc/letsencrypt/live/example.com\") containing'\n        ' the new certificates and keys; the shell variable'\n        ' $RENEWED_DOMAINS will contain a space-delimited list of'\n        ' renewed certificate domains (for example, \"example.com'\n        ' www.example.com\")')\n    helpful.add(\n        \"renew\", \"--disable-hook-validation\",\n        action=\"store_false\", dest=\"validate_hooks\",\n        default=flag_default(\"validate_hooks\"),\n        help=\"Ordinarily the commands specified for\"\n        \" --pre-hook/--post-hook/--deploy-hook will be checked for\"\n        \" validity, to see if the programs being run are in the $PATH,\"\n        \" so that mistakes can be caught early, even when the hooks\"\n        \" aren't being run just yet. The validation is rather\"\n        \" simplistic and fails if you use more advanced shell\"\n        \" constructs, so you can use this switch to disable it.\"\n        \" (default: False)\")\n    helpful.add(\n        \"renew\", \"--no-directory-hooks\", action=\"store_false\",\n        default=flag_default(\"directory_hooks\"), dest=\"directory_hooks\",\n        help=\"Disable running executables found in Certbot's hook directories.\"\n        \" (default: False)\")\n    helpful.add(\n        \"renew\", \"--disable-renew-updates\", action=\"store_true\",\n        default=flag_default(\"disable_renew_updates\"), dest=\"disable_renew_updates\",\n        help=\"Disable automatic updates to your server configuration that\"\n        \" would otherwise be done by the selected installer plugin, and triggered\"\n        \" when the user executes \\\"certbot renew\\\", regardless of if the certificate\"\n        \" is renewed. This setting does not apply to important TLS configuration\"\n        \" updates.\")\n    helpful.add(\n        \"renew\", \"--no-autorenew\", action=\"store_false\",\n        default=flag_default(\"autorenew\"), dest=\"autorenew\",\n        help=\"Disable auto renewal of certificates. (default: False)\")\n\n    # Deprecated arguments\n    helpful.add_deprecated_argument(\"--os-packages-only\", 0)\n    helpful.add_deprecated_argument(\"--no-self-upgrade\", 0)\n    helpful.add_deprecated_argument(\"--no-bootstrap\", 0)\n    helpful.add_deprecated_argument(\"--no-permissions-check\", 0)\n\n    # Populate the command line parameters for new style enhancements\n    enhancements.populate_cli(helpful.add)\n\n    _create_subparsers(helpful)\n    _paths_parser(helpful)\n    # _plugins_parsing should be the last thing to act upon the main\n    # parser (--help should display plugin-specific options last)\n    _plugins_parsing(helpful, plugins)\n\n    global helpful_parser # pylint: disable=global-statement\n    helpful_parser = helpful\n    return helpful.parse_args()\n\n\ndef argparse_type(variable: Any) -> type:\n    \"\"\"Return our argparse type function for a config variable (default: str)\"\"\"\n    # pylint: disable=protected-access\n    if helpful_parser is not None:\n        for action in helpful_parser.actions:\n            if action.type is not None and action.dest == variable:\n                return action.type\n    return str\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/cli_constants.py",
    "content": "\"\"\"Certbot command line constants\"\"\"\ncli_command = \"certbot\"\n\n\n# Argparse's help formatting has a lot of unhelpful peculiarities, so we want\n# to replace as much of it as we can...\n\n# This is the stub to include in help generated by argparse\nSHORT_USAGE = \"\"\"\n  {0} [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...\n\nCertbot can obtain and install HTTPS/TLS/SSL certificates.  By default,\nit will attempt to use a webserver both for obtaining and installing the\ncertificate. \"\"\".format(cli_command)\n\n# This section is used for --help and --help all ; it needs information\n# about installed plugins to be fully formatted\nCOMMAND_OVERVIEW = \"\"\"The most common SUBCOMMANDS and flags are:\n\nobtain, install, and renew certificates:\n    (default) run   Obtain & install a certificate in your current webserver\n    certonly        Obtain or renew a certificate, but do not install it\n    renew           Renew all previously obtained certificates that are near expiry\n    enhance         Add security enhancements to your existing configuration\n   -d DOMAINS       Comma-separated list of domains to obtain a certificate for\n\n  %s\n  --standalone      Run a standalone webserver for authentication\n  %s\n  --webroot         Place files in a server's webroot folder for authentication\n  --manual          Obtain certificates interactively, or using shell script hooks\n\n   -n               Run non-interactively\n  --test-cert       Obtain a test certificate from a staging server\n  --dry-run         Test \"renew\" or \"certonly\" without saving any certificates to disk\n\nmanage certificates:\n    certificates    Display information about certificates you have from Certbot\n    revoke          Revoke a certificate (supply --cert-name or --cert-path)\n    delete          Delete a certificate (supply --cert-name)\n    reconfigure     Update a certificate's configuration (supply --cert-name)\n\nmanage your account:\n    register        Create an ACME account\n    unregister      Deactivate an ACME account\n    update_account  Update an ACME account\n    show_account    Display account details\n  --agree-tos       Agree to the ACME server's Subscriber Agreement\n   -m EMAIL         Email address for important account notifications\n\"\"\"\n\n# This is the short help for certbot --help, where we disable argparse\n# altogether\nHELP_AND_VERSION_USAGE = \"\"\"\nMore detailed help:\n\n  -h, --help [TOPIC]    print this message, or detailed help on a topic;\n                        the available TOPICS are:\n\n   all, automation, commands, paths, security, testing, or any of the\n   subcommands or plugins (certonly, renew, install, register, nginx,\n   apache, standalone, webroot, etc.)\n  -h all                print a detailed help page including all topics\n  --version             print the version number\n\"\"\"\n\n# These argparse parameters should be removed when detecting defaults.\nARGPARSE_PARAMS_TO_REMOVE = (\"const\", \"nargs\", \"type\",)\n\n\n# These sets are used when to help detect options set by the user.\nEXIT_ACTIONS = {\"help\", \"version\",}\n\n\nZERO_ARG_ACTIONS = {\"store_const\", \"store_true\",\n                        \"store_false\", \"append_const\", \"count\",}\n\n\n# Maps a config option to a set of config options that may have modified it.\n# This dictionary is used recursively, so if A modifies B and B modifies C,\n# it is determined that C was modified by the user if A was modified.\nVAR_MODIFIERS = {\"account\": {\"server\",},\n                 \"server\": {\"dry_run\", \"staging\",},\n                 \"webroot_map\": {\"webroot_path\",}}\n\n# This is a list of all CLI options that we have ever deprecated. It lets us\n# opt out of the default detection, which can interact strangely with option\n# deprecation. See https://github.com/certbot/certbot/issues/8540 for more info.\nDEPRECATED_OPTIONS = {\n    \"manual_public_ip_logging_ok\",\n    \"os_packages_only\",\n    \"no_self_upgrade\",\n    \"no_bootstrap\",\n    \"no_permissions_check\",\n    \"dns_route53_propagation_seconds\",\n    \"certbot_route53:auth_propagation_seconds\"\n}\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/cli_utils.py",
    "content": "\"\"\"Certbot command line util function\"\"\"\nimport argparse\nimport copy\nimport glob\nimport inspect\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom acme import challenges\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot._internal import constants\nfrom certbot._internal import san\nfrom certbot.compat import os\n\nif TYPE_CHECKING:\n    from certbot._internal.cli import helpful\n\n\ndef read_file(filename: str, mode: str = \"rb\") -> tuple[str, Any]:\n    \"\"\"Returns the given file's contents.\n\n    :param str filename: path to file\n    :param str mode: open mode (see `open`)\n\n    :returns: absolute path of filename and its contents\n    :rtype: tuple\n\n    :raises argparse.ArgumentTypeError: File does not exist or is not readable.\n\n    \"\"\"\n    try:\n        filename = os.path.abspath(filename)\n        with open(filename, mode) as the_file:\n            contents = the_file.read()\n        return filename, contents\n    except OSError as exc:\n        raise argparse.ArgumentTypeError(exc.strerror)\n\n\ndef flag_default(name: str) -> Any:\n    \"\"\"Default value for CLI flag.\"\"\"\n    # XXX: this is an internal housekeeping notion of defaults before\n    # argparse has been set up; it is not accurate for all flags.  Call it\n    # with caution.  Plugin defaults are missing, and some things are using\n    # defaults defined in this file, not in constants.py :(\n    return copy.deepcopy(constants.CLI_DEFAULTS[name])\n\n\ndef config_help(name: str, hidden: bool = False) -> Optional[str]:\n    \"\"\"Extract the help message for a `configuration.NamespaceConfig` property docstring.\"\"\"\n    if hidden:\n        return argparse.SUPPRESS\n    return inspect.getdoc(getattr(configuration.NamespaceConfig, name))\n\n\nclass HelpfulArgumentGroup:\n    \"\"\"Emulates an argparse group for use with HelpfulArgumentParser.\n\n    This class is used in the add_group method of HelpfulArgumentParser.\n    Command line arguments can be added to the group, but help\n    suppression and default detection is applied by\n    HelpfulArgumentParser when necessary.\n\n    \"\"\"\n    def __init__(self, helpful_arg_parser: \"helpful.HelpfulArgumentParser\", topic: str) -> None:\n        self._parser = helpful_arg_parser\n        self._topic = topic\n\n    def add_argument(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Add a new command line argument to the argument group.\"\"\"\n        self._parser.add(self._topic, *args, **kwargs)\n\n\nclass CustomHelpFormatter(argparse.HelpFormatter):\n    \"\"\"This is a clone of ArgumentDefaultsHelpFormatter, with bugfixes.\n\n    In particular we fix https://bugs.python.org/issue28742\n    \"\"\"\n\n    def _get_help_string(self, action: argparse.Action) -> Optional[str]:\n        helpstr = action.help\n        if action.help and '%(default)' not in action.help and '(default:' not in action.help:\n            if action.default != argparse.SUPPRESS:\n                defaulting_nargs = [argparse.OPTIONAL, argparse.ZERO_OR_MORE]\n                if helpstr and (action.option_strings or action.nargs in defaulting_nargs):\n                    helpstr += ' (default: %(default)s)'\n        return helpstr\n\n\nclass DomainsAction(argparse.Action):\n    \"\"\"Action class for parsing domains.\"\"\"\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 values: str | Sequence[Any] | None,\n                 option_string: str | None = None) -> None:\n        match values:\n            case str():\n                for domain in values.split(\",\"):\n                    add_dns_name(namespace, san.DNSName(domain.strip()))\n            case _:\n                # https://docs.python.org/3/library/argparse.html#nargs\n                raise TypeError(\"shouldn't happen: non-str passed by argparse when nargs=None\")\n\n\ndef add_dns_name(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig],\n                 dns_name: san.DNSName) -> None:\n    \"\"\"Registers a new domain to be used during the current client run.\n\n    The domain is not added if it has already been registered.\n\n    :param args_or_config: parsed command line arguments\n    :type args_or_config: argparse.Namespace or\n        configuration.NamespaceConfig\n    :param san.DNSName dns_name: a DNS name\n    \"\"\"\n    if dns_name not in args_or_config.domains:\n        args_or_config.domains.append(dns_name)\n\n\nclass IPAddressAction(argparse.Action):\n    \"\"\"Action class for parsing IP addresses.\"\"\"\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 values: str | Sequence[Any] | None,\n                 option_string: Optional[str] = None) -> None:\n        match values:\n            case str():\n                # This will throw an exception if the IP address doesn't parse.\n                add_ip_address(namespace, san.IPAddress(values))\n            case _:\n                # https://docs.python.org/3/library/argparse.html#nargs\n                raise TypeError(\"shouldn't happen: non-str passed by argparse when nargs=None\")\n\n\ndef add_ip_address(args_or_config: Union[argparse.Namespace, configuration.NamespaceConfig],\n                   ip_address: san.IPAddress) -> None:\n    \"\"\"Registers a new IP address to be used during the current client run.\n\n    The IP address is not added if it has already been registered.\n\n    :param args_or_config: parsed command line arguments\n    :type args_or_config: argparse.Namespace or\n        configuration.NamespaceConfig\n    :param san.IPAddress ip_address: an IP address\n    \"\"\"\n    if ip_address not in args_or_config.ip_addresses:\n        args_or_config.ip_addresses.append(ip_address)\n\nclass CaseInsensitiveList(list):\n    \"\"\"A list that will ignore case when searching.\n\n    This class is passed to the `choices` argument of `argparse.add_arguments`\n    through the `helpful` wrapper. It is necessary due to special handling of\n    command line arguments by `set_by_cli` in which the `type_func` is not applied.\"\"\"\n    def __contains__(self, element: object) -> bool:\n        if not isinstance(element, str):\n            return False\n        return super().__contains__(element.lower())\n\n\ndef _user_agent_comment_type(value: str) -> str:\n    if \"(\" in value or \")\" in value:\n        raise argparse.ArgumentTypeError(\"may not contain parentheses\")\n    return value\n\n\nclass _EncodeReasonAction(argparse.Action):\n    \"\"\"Action class for parsing revocation reason.\"\"\"\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 reason: Union[str, Sequence[Any], None],\n                 option_string: Optional[str] = None) -> None:\n        \"\"\"Encodes the reason for certificate revocation.\"\"\"\n        if reason is None:\n            raise ValueError(\"Unexpected null reason.\")\n        code = constants.REVOCATION_REASONS[str(reason).lower()]\n        setattr(namespace, self.dest, code)\n\n\ndef parse_preferred_challenges(pref_challs: Iterable[str]) -> list[str]:\n    \"\"\"Translate and validate preferred challenges.\n\n    :param pref_challs: list of preferred challenge types\n    :type pref_challs: `list` of `str`\n\n    :returns: validated list of preferred challenge types\n    :rtype: `list` of `str`\n\n    :raises errors.Error: if pref_challs is invalid\n\n    \"\"\"\n    aliases = {\"dns\": \"dns-01\", \"http\": \"http-01\"}\n    challs = [c.strip() for c in pref_challs]\n    challs = [aliases.get(c, c) for c in challs]\n\n    unrecognized = \", \".join(name for name in challs\n                             if name not in challenges.Challenge.TYPES)\n    if unrecognized:\n        raise errors.Error(\n            \"Unrecognized challenges: {0}\".format(unrecognized))\n    return challs\n\n\nclass _PrefChallAction(argparse.Action):\n    \"\"\"Action class for parsing preferred challenges.\"\"\"\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 pref_challs: Union[str, Sequence[Any], None],\n                 option_string: Optional[str] = None) -> None:\n        if pref_challs is None:\n            raise ValueError(\"Unexpected null pref_challs.\")\n        try:\n            challs = parse_preferred_challenges(str(pref_challs).split(\",\"))\n        except errors.Error as error:\n            raise argparse.ArgumentError(self, str(error))\n        namespace.pref_challs.extend(challs)\n\n\ndef nonnegative_int(value: str) -> int:\n    \"\"\"Converts value to an int and checks that it is not negative.\n\n    This function should used as the type parameter for argparse\n    arguments.\n\n    :param str value: value provided on the command line\n\n    :returns: integer representation of value\n    :rtype: int\n\n    :raises argparse.ArgumentTypeError: if value isn't a non-negative integer\n\n    \"\"\"\n    try:\n        int_value = int(value)\n    except ValueError:\n        raise argparse.ArgumentTypeError(\"value must be an integer\")\n\n    if int_value < 0:\n        raise argparse.ArgumentTypeError(\"value must be non-negative\")\n    return int_value\n\ndef set_test_server_options(verb: str, config: configuration.NamespaceConfig) -> None:\n    \"\"\"Updates server, break_my_certs, staging, tos, and\n    register_unsafely_without_email in config as necessary to prepare\n    to use the test server.\n\n    We have --staging/--dry-run; perform sanity check and set config.server\n\n    :param str verb: subcommand called\n\n    :param config: parsed command line arguments\n    :type config: configuration.NamespaceConfig\n\n    :raises errors.Error: if non-default server is used and --staging is set\n    :raises errors.Error: if inapplicable verb is used and --dry-run is set\n    \"\"\"\n\n    # Flag combinations should produce these results:\n    #                             | --staging      | --dry-run   |\n    # ------------------------------------------------------------\n    # | --server acme-v02         | Use staging    | Use staging |\n    # | --server acme-staging-v02 | Use staging    | Use staging |\n    # | --server <other>          | Conflict error | Use <other> |\n\n    default_servers = (flag_default(\"server\"), constants.STAGING_URI)\n\n    if config.staging and config.server not in default_servers:\n        raise errors.Error(\"--server value conflicts with --staging\")\n\n    if config.server == flag_default(\"server\"):\n        config.server = constants.STAGING_URI\n        # If the account has already been loaded (such as by calling reconstitute before this),\n        # clear it so that we don't try to use the prod account on the staging server.\n        config.account = None\n\n    if config.dry_run:\n        if verb not in [\"certonly\", \"renew\", \"reconfigure\"]:\n            raise errors.Error(\"--dry-run currently only works with the \"\n                               \"'certonly' or 'renew' subcommands (%r)\" % verb)\n        config.break_my_certs = config.staging = True\n        if glob.glob(os.path.join(config.config_dir, constants.ACCOUNTS_DIR, \"*\")):\n            # The user has a prod account, but might not have a staging\n            # one; we don't want to start trying to perform interactive registration\n            config.tos = True\n            config.register_unsafely_without_email = True\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/group_adder.py",
    "content": "\"\"\"This module contains a function to add the groups of arguments for the help\ndisplay\"\"\"\nfrom typing import TYPE_CHECKING\n\nfrom certbot._internal.cli.verb_help import VERB_HELP\n\nif TYPE_CHECKING:\n    from certbot._internal.cli import helpful\n\n\ndef _add_all_groups(helpful: \"helpful.HelpfulArgumentParser\") -> None:\n    helpful.add_group(\"automation\", description=\"Flags for automating execution & other tweaks\")\n    helpful.add_group(\"security\", description=\"Security parameters & server settings\")\n    helpful.add_group(\"testing\",\n        description=\"The following flags are meant for testing and integration purposes only.\")\n    helpful.add_group(\"paths\", description=\"Flags for changing execution paths & servers\")\n    helpful.add_group(\"manage\",\n        description=\"Various subcommands and flags are available for managing your certificates:\",\n        verbs=[\"certificates\", \"delete\", \"renew\", \"revoke\", \"reconfigure\"])\n\n    # VERBS\n    for verb, docs in VERB_HELP:\n        name = docs.get(\"realname\", verb)\n        helpful.add_group(name, description=docs[\"opts\"])\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/helpful.py",
    "content": "\"\"\"Certbot command line argument parser\"\"\"\n\nimport argparse\nimport functools\nimport sys\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nimport configargparse\nfrom cryptography import x509\n\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot._internal import hooks\nfrom certbot._internal import san\nfrom certbot._internal.cli.cli_constants import COMMAND_OVERVIEW\nfrom certbot._internal.cli.cli_constants import HELP_AND_VERSION_USAGE\nfrom certbot._internal.cli.cli_constants import SHORT_USAGE\nfrom certbot._internal.cli.cli_utils import CustomHelpFormatter\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot._internal.cli.cli_utils import HelpfulArgumentGroup\nfrom certbot._internal.cli.cli_utils import set_test_server_options\nfrom certbot._internal.cli.verb_help import VERB_HELP\nfrom certbot._internal.cli.verb_help import VERB_HELP_MAP\nfrom certbot._internal.display import obj as display_obj\nfrom certbot._internal.plugins import disco\nfrom certbot.configuration import ArgumentSource\nfrom certbot.configuration import NamespaceConfig\n\n\nclass HelpfulArgumentParser:\n    \"\"\"Argparse Wrapper.\n\n    This class wraps argparse, adding the ability to make --help less\n    verbose, and request help on specific subcategories at a time, eg\n    'certbot --help security' for security options.\n\n    \"\"\"\n    def __init__(self, args: list[str], plugins: Iterable[str]) -> None:\n        from certbot._internal import main\n        self.VERBS = {\n            \"auth\": main.certonly,\n            \"certonly\": main.certonly,\n            \"run\": main.run,\n            \"install\": main.install,\n            \"plugins\": main.plugins_cmd,\n            \"register\": main.register,\n            \"update_account\": main.update_account,\n            \"show_account\": main.show_account,\n            \"unregister\": main.unregister,\n            \"renew\": main.renew,\n            \"revoke\": main.revoke,\n            \"rollback\": main.rollback,\n            \"everything\": main.run,\n            \"certificates\": main.certificates,\n            \"delete\": main.delete,\n            \"enhance\": main.enhance,\n            \"reconfigure\": main.reconfigure,\n        }\n\n        # Get notification function for printing\n        self.notify = display_obj.NoninteractiveDisplay(sys.stdout).notification\n\n        self.actions: list[configargparse.Action] = []\n\n        # List of topics for which additional help can be provided\n        HELP_TOPICS: list[Optional[str]] = [\"all\", \"security\", \"paths\", \"automation\", \"testing\"]\n        HELP_TOPICS += list(self.VERBS) + self.COMMANDS_TOPICS + [\"manage\"]\n\n        plugin_names: list[Optional[str]] = list(plugins)\n        self.help_topics: list[Optional[str]] = HELP_TOPICS + plugin_names + [None]\n\n        self.args = args\n\n        if self.args and self.args[0] == 'help':\n            self.args[0] = '--help'\n\n        self.determine_verb()\n        help1 = self.prescan_for_flag(\"-h\", self.help_topics)\n        help2 = self.prescan_for_flag(\"--help\", self.help_topics)\n        self.help_arg: Union[str, bool]\n        if isinstance(help1, bool) and isinstance(help2, bool):\n            self.help_arg = help1 or help2\n        else:\n            self.help_arg = help1 if isinstance(help1, str) else help2\n\n        short_usage = self._usage_string(plugins, self.help_arg)\n\n        self.visible_topics = self.determine_help_topics(self.help_arg)\n\n        # elements are added by .add_group()\n        self.groups: dict[str, argparse._ArgumentGroup] = {}\n\n        self.parser = configargparse.ArgParser(\n            prog=\"certbot\",\n            usage=short_usage,\n            formatter_class=CustomHelpFormatter,\n            args_for_setting_config_path=[\"-c\", \"--config\"],\n            default_config_files=flag_default(\"config_files\"),\n            config_arg_help_message=\"path to config file (default: {0})\".format(\n                \" and \".join(flag_default(\"config_files\"))))\n\n        # This is the only way to turn off overly verbose config flag documentation\n        self.parser._add_config_file_help = False\n\n        self.verb: str\n\n    # Help that are synonyms for --help subcommands\n    COMMANDS_TOPICS = [\"command\", \"commands\", \"subcommand\", \"subcommands\", \"verbs\"]\n\n    def _list_subcommands(self) -> str:\n        longest = max(len(v) for v in VERB_HELP_MAP)\n\n        text = \"The full list of available SUBCOMMANDS is:\\n\\n\"\n        for verb, props in sorted(VERB_HELP):\n            doc = props.get(\"short\", \"\")\n            text += '{0:<{length}}     {1}\\n'.format(verb, doc, length=longest)\n\n        text += \"\\nYou can get more help on a specific subcommand with --help SUBCOMMAND\\n\"\n        return text\n\n    def _usage_string(self, plugins: Iterable[str], help_arg: Union[str, bool]) -> str:\n        \"\"\"Make usage strings late so that plugins can be initialised late\n\n        :param plugins: all discovered plugins\n        :param help_arg: False for none; True for --help; \"TOPIC\" for --help TOPIC\n        :rtype: str\n        :returns: a short usage string for the top of --help TOPIC)\n        \"\"\"\n        if \"nginx\" in plugins:\n            nginx_doc = \"--nginx           Use the Nginx plugin for authentication & installation\"\n        else:\n            nginx_doc = \"(the certbot nginx plugin is not installed)\"\n        if \"apache\" in plugins:\n            apache_doc = \"--apache          Use the Apache plugin for authentication & installation\"\n        else:\n            apache_doc = \"(the certbot apache plugin is not installed)\"\n\n        usage = SHORT_USAGE\n        if help_arg is True:\n            self.notify(usage + COMMAND_OVERVIEW % (apache_doc, nginx_doc) + HELP_AND_VERSION_USAGE)\n            sys.exit(0)\n        elif help_arg in self.COMMANDS_TOPICS:\n            self.notify(usage + self._list_subcommands())\n            sys.exit(0)\n        elif help_arg == \"all\":\n            # if we're doing --help all, the OVERVIEW is part of the SHORT_USAGE at\n            # the top; if we're doing --help someothertopic, it's OT so it's not\n            usage += COMMAND_OVERVIEW % (apache_doc, nginx_doc)\n        elif isinstance(help_arg, str):\n            custom = VERB_HELP_MAP.get(help_arg, {}).get(\"usage\", None)\n            usage = custom if custom else usage\n        # Only remaining case is help_arg == False, which gives effectively usage == SHORT_USAGE.\n\n        return usage\n\n    def remove_config_file_domains_for_renewal(self, config: NamespaceConfig) -> None:\n        \"\"\"Make \"certbot renew\" safe if domains are set in cli.ini.\"\"\"\n        # Works around https://github.com/certbot/certbot/issues/4096\n        assert config.argument_sources is not None\n        if (config.argument_sources['domains'] == ArgumentSource.CONFIG_FILE and\n                self.verb == \"renew\"):\n            config.domains = []\n\n    def _build_sources_dict(self) -> dict[str, ArgumentSource]:\n        # ConfigArgparse's get_source_to_settings_dict doesn't actually create\n        # default entries for each argument with a default value, omitting many\n        # args we'd otherwise care about. So in general, unless an argument was\n        # specified in a config file/environment variable/command line arg,\n        # consider it as having a \"default\" value\n        result = { action.dest: ArgumentSource.DEFAULT for action in self.actions }\n\n        source_to_settings_dict: dict[str, dict[str, tuple[configargparse.Action, str]]]\n        source_to_settings_dict = self.parser.get_source_to_settings_dict()\n\n        # We'll process the sources dict in order of each source's \"priority\",\n        # i.e. the order in which ConfigArgparse ultimately sets argument\n        # values:\n        #   1. defaults (`result` already has everything marked as such)\n        #   2. config files\n        #   3. env vars (shouldn't be any)\n        #   4. command line\n\n        def update_result(settings_dict: dict[str, tuple[configargparse.Action, str]],\n                          source: ArgumentSource) -> None:\n            actions = [self._find_action_for_arg(arg) if action is None else action\n                       for arg, (action, _) in settings_dict.items()]\n            result.update({ action.dest: source for action in actions })\n\n        # config file sources look like \"config_file|<name of file>\"\n        for source_key in source_to_settings_dict:\n            if source_key.startswith('config_file'):\n                update_result(source_to_settings_dict[source_key], ArgumentSource.CONFIG_FILE)\n\n        update_result(source_to_settings_dict.get('env_var', {}), ArgumentSource.ENV_VAR)\n\n        # The command line settings dict is weird, so handle it separately\n        if 'command_line' in source_to_settings_dict:\n            settings_dict: dict[str, tuple[None, list[str]]]\n            settings_dict = source_to_settings_dict['command_line'] # type: ignore\n            (_, unprocessed_args) = settings_dict['']\n            args = []\n            for arg in unprocessed_args:\n                # ignore non-arguments\n                if not arg.startswith('-'):\n                    continue\n\n                # special case for config file argument, which we don't have an action for\n                if arg in ['-c', '--config']:\n                    result['config_dir'] = ArgumentSource.COMMAND_LINE\n                    continue\n\n                if '=' in arg:\n                    arg = arg.split('=')[0]\n                elif ' ' in arg:\n                    arg = arg.split(' ')[0]\n\n                if arg.startswith('--'):\n                    args.append(arg)\n                # for short args (ones that start with a single hyphen), handle\n                # the case of multiple short args together, e.g. \"-tvm\"\n                else:\n                    for short_arg in arg[1:]:\n                        args.append(f\"-{short_arg}\")\n\n            for arg in args:\n                # find the action corresponding to this arg\n                action = self._find_action_for_arg(arg)\n                result[action.dest] = ArgumentSource.COMMAND_LINE\n\n        return result\n\n    def _find_action_for_arg(self, arg: str) -> configargparse.Action:\n        # Finds a configargparse Action which matches the given arg, where arg\n        # can either be preceded by hyphens (as on the command line) or not (as\n        # in config files)\n\n        # if the argument doesn't have leading hyphens, prefix it so it can be\n        # compared directly w/ action option strings\n        if arg[0] != '-':\n            arg = '--' + arg\n\n        # first, check for exact matches\n        for action in self.actions:\n            if arg in action.option_strings:\n                return action\n\n        # now check for abbreviated (i.e. prefix) matches\n        for action in self.actions:\n            for option_string in action.option_strings:\n                if option_string.startswith(arg):\n                    return action\n\n        raise AssertionError(f\"Action corresponding to argument {arg} is None\")\n\n    def parse_args(self) -> NamespaceConfig:\n        \"\"\"Parses command line arguments and returns the result.\n\n        :returns: parsed command line arguments\n        :rtype: configuration.NamespaceConfig\n\n        \"\"\"\n        parsed_args = self.parser.parse_args(self.args)\n        parsed_args.func = self.VERBS[self.verb]\n        parsed_args.verb = self.verb\n        config = NamespaceConfig(parsed_args)\n        config.set_argument_sources(self._build_sources_dict())\n\n        self.remove_config_file_domains_for_renewal(config)\n\n        # Do any post-parsing homework here\n\n        if self.verb == \"renew\":\n            if config.force_interactive:\n                raise errors.Error(\n                    \"{0} cannot be used with renew\".format(\n                        constants.FORCE_INTERACTIVE_FLAG))\n            config.noninteractive_mode = True\n\n        if config.force_interactive and config.noninteractive_mode:\n            raise errors.Error(\n                \"Flag for non-interactive mode and {0} conflict\".format(\n                    constants.FORCE_INTERACTIVE_FLAG))\n\n        if config.staging or config.dry_run:\n            self.set_test_server(config)\n\n        if config.csr:\n            self.handle_csr(config)\n\n        if config.must_staple and not config.staple:\n            config.staple = True\n\n        if config.validate_hooks:\n            hooks.validate_hooks(config)\n\n        if config.allow_subset_of_names:\n            if any(d.is_wildcard() for d in config.domains):\n                raise errors.Error(\"Using --allow-subset-of-names with a\"\n                                   \" wildcard domain is not supported.\")\n\n        if config.hsts and config.auto_hsts:\n            raise errors.Error(\n                \"Parameters --hsts and --auto-hsts cannot be used simultaneously.\")\n\n        if isinstance(config.key_type, list) and len(config.key_type) > 1:\n            raise errors.Error(\n                \"Only *one* --key-type type may be provided at this time.\")\n\n        return config\n\n    def set_test_server(self, config: NamespaceConfig) -> None:\n        \"\"\"Updates server, break_my_certs, staging, tos, and\n        register_unsafely_without_email in config as necessary to prepare\n        to use the test server.\"\"\"\n        return set_test_server_options(self.verb, config)\n\n    def handle_csr(self, config: NamespaceConfig) -> None:\n        \"\"\"Process a --csr flag.\"\"\"\n        if config.verb != \"certonly\":\n            raise errors.Error(\"Currently, a CSR file may only be specified \"\n                               \"when obtaining a new or replacement \"\n                               \"via the certonly command. Please try the \"\n                               \"certonly command instead.\")\n        if config.allow_subset_of_names:\n            raise errors.Error(\"--allow-subset-of-names cannot be used with --csr\")\n\n        csrfile, contents = config.csr[0:2]\n        util_csr = crypto_util.read_csr_file(csrfile, contents)\n        x509_req = x509.load_pem_x509_csr(util_csr.data)\n        domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions)\n\n        # The SANs from the CSR are added to the domains from command line flags as this config\n        # setting is where main.certonly gets the list of identifiers to request.\n        config.domains.extend(domains)\n\n        if not domains + ip_addresses:\n            # TODO: add CN to domains instead:\n            raise errors.Error(\n                f\"Unfortunately, your CSR {config.csr[0]} needs to have a SubjectAltName for \" +\n                \"every domain or IP address\")\n\n        config.actual_csr = util_csr\n\n        # Check that the original values for --domain and --ip-address set by the user were\n        # a subset of the domains listed in the CSR.\n        if set(config.domains) != set(domains):\n            raise errors.ConfigurationError(\n                \"Inconsistent requests:\\nFrom the CSR: {0}\\nFrom command line/config: {1}\"\n                .format(san.display(san.join(domains, ip_addresses)),\n                        san.display(config.domains)))\n\n\n    def determine_verb(self) -> None:\n        \"\"\"Determines the verb/subcommand provided by the user.\n\n        This function works around some of the limitations of argparse.\n\n        \"\"\"\n        if \"-h\" in self.args or \"--help\" in self.args:\n            # all verbs double as help arguments; don't get them confused\n            self.verb = \"help\"\n            return\n\n        for i, token in enumerate(self.args):\n            if token in self.VERBS:\n                verb = token\n                if verb == \"auth\":\n                    verb = \"certonly\"\n                if verb == \"everything\":\n                    verb = \"run\"\n                self.verb = verb\n                self.args.pop(i)\n                return\n\n        self.verb = \"run\"\n\n    def prescan_for_flag(self, flag: str, possible_arguments: Iterable[Optional[str]]\n                         ) -> Union[str, bool]:\n        \"\"\"Checks cli input for flags.\n\n        Check for a flag, which accepts a fixed set of possible arguments, in\n        the command line; we will use this information to configure argparse's\n        help correctly.  Return the flag's argument, if it has one that matches\n        the sequence @possible_arguments; otherwise return whether the flag is\n        present.\n\n        \"\"\"\n        if flag not in self.args:\n            return False\n        pos = self.args.index(flag)\n        try:\n            nxt = self.args[pos + 1]\n            if nxt in possible_arguments:\n                return nxt\n        except IndexError:\n            pass\n        return True\n\n    def add(self, topics: Optional[Union[list[Optional[str]], str]], *args: Any,\n            **kwargs: Any) -> None:\n        \"\"\"Add a new command line argument.\n\n        :param topics: str or [str] help topic(s) this should be listed under,\n                       or None for options that don't fit under a specific\n                       topic which will only be shown in \"--help all\" output.\n                       The first entry determines where the flag lives in the\n                       \"--help all\" output (None -> \"optional arguments\").\n        :param list *args: the names of this argument flag\n        :param dict **kwargs: various argparse settings for this argument\n\n        \"\"\"\n        self.actions.append(self._add(topics, *args, **kwargs))\n\n    def _add(self, topics: Optional[Union[list[Optional[str]], str]], *args: Any,\n            **kwargs: Any) -> configargparse.Action:\n        action = kwargs.get(\"action\")\n        if action is util.DeprecatedArgumentAction:\n            # If the argument is deprecated through\n            # certbot.util.add_deprecated_argument, it is not shown in the help\n            # output and any value given to the argument is thrown away during\n            # argument parsing. Because of this, we handle this case early\n            # skipping putting the argument in different help topics and\n            # handling default detection since these actions aren't needed and\n            # can cause bugs like\n            # https://github.com/certbot/certbot/issues/8495.\n            return self.parser.add_argument(*args, **kwargs)\n\n        if isinstance(topics, list):\n            # if this flag can be listed in multiple sections, try to pick the one\n            # that the user has asked for help about\n            topic = self.help_arg if self.help_arg in topics else topics[0]\n        else:\n            topic = topics  # there's only one\n\n        if not isinstance(topic, bool) and self.visible_topics[topic]:\n            if topic in self.groups:\n                group = self.groups[topic]\n                return group.add_argument(*args, **kwargs)\n            else:\n                return self.parser.add_argument(*args, **kwargs)\n        else:\n            kwargs[\"help\"] = argparse.SUPPRESS\n            return self.parser.add_argument(*args, **kwargs)\n\n    def add_deprecated_argument(self, argument_name: str, num_args: int) -> None:\n        \"\"\"Adds a deprecated argument with the name argument_name.\n\n        Deprecated arguments are not shown in the help. If they are used\n        on the command line, a warning is shown stating that the\n        argument is deprecated and no other action is taken.\n\n        :param str argument_name: Name of deprecated argument.\n        :param int num_args: Number of arguments the option takes.\n\n        \"\"\"\n        # certbot.util.add_deprecated_argument expects the normal add_argument\n        # interface provided by argparse. This is what is given including when\n        # certbot.util.add_deprecated_argument is used by plugins, however, in\n        # that case the first argument to certbot.util.add_deprecated_argument\n        # is certbot._internal.cli.HelpfulArgumentGroup.add_argument which\n        # internally calls the add method of this class.\n        #\n        # The difference between the add method of this class and the standard\n        # argparse add_argument method caused a bug in the past (see\n        # https://github.com/certbot/certbot/issues/8495) so we use the same\n        # code path here for consistency and to ensure it works. To do that, we\n        # wrap the add method in a similar way to\n        # HelpfulArgumentGroup.add_argument by providing a help topic (which in\n        # this case is set to None).\n        add_func = functools.partial(self.add, None)\n        util.add_deprecated_argument(add_func, argument_name, num_args)\n\n    def add_group(self, topic: str, verbs: Iterable[str] = (),\n                  **kwargs: Any) -> HelpfulArgumentGroup:\n        \"\"\"Create a new argument group.\n\n        This method must be called once for every topic, however, calls\n        to this function are left next to the argument definitions for\n        clarity.\n\n        :param str topic: Name of the new argument group.\n        :param str verbs: List of subcommands that should be documented as part of\n                          this help group / topic\n\n        :returns: The new argument group.\n        :rtype: `HelpfulArgumentGroup`\n\n        \"\"\"\n        if self.visible_topics[topic]:\n            self.groups[topic] = self.parser.add_argument_group(topic, **kwargs)\n            if self.help_arg:\n                for v in verbs:\n                    self.groups[topic].add_argument(v, help=VERB_HELP_MAP[v][\"short\"])\n        return HelpfulArgumentGroup(self, topic)\n\n    def add_plugin_args(self, plugins: disco.PluginsRegistry) -> None:\n        \"\"\"\n\n        Let each of the plugins add its own command line arguments, which\n        may or may not be displayed as help topics.\n\n        \"\"\"\n        for name, plugin_ep in plugins.items():\n            parser_or_group = self.add_group(name,\n                                             description=plugin_ep.long_description)\n            plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)\n\n    def determine_help_topics(self, chosen_topic: Union[str, bool]\n                              ) -> dict[Optional[str], bool]:\n        \"\"\"\n\n        The user may have requested help on a topic, return a dict of which\n        topics to display. @chosen_topic has prescan_for_flag's return type\n\n        :returns: dict\n\n        \"\"\"\n        # topics maps each topic to whether it should be documented by\n        # argparse on the command line\n        if chosen_topic == \"auth\":\n            chosen_topic = \"certonly\"\n        if chosen_topic == \"everything\":\n            chosen_topic = \"run\"\n        if chosen_topic == \"all\":\n            # Addition of condition closes #6209 (removal of duplicate route53 option).\n            return {t: t != 'certbot-route53:auth' for t in self.help_topics}\n        elif not chosen_topic:\n            return {t: False for t in self.help_topics}\n        return {t: t == chosen_topic for t in self.help_topics}\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/paths_parser.py",
    "content": "\"\"\"This is a module that adds configuration to the argument parser regarding\npaths for certificates\"\"\"\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom certbot._internal.cli.cli_utils import config_help\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot.compat import os\n\nif TYPE_CHECKING:\n    from certbot._internal.cli import helpful\n\n\ndef _paths_parser(helpful: \"helpful.HelpfulArgumentParser\") -> None:\n    add = helpful.add\n    verb: Union[str, bool] = helpful.verb\n    if verb == \"help\":\n        verb = helpful.help_arg\n\n    cpkwargs = {\n        \"type\": os.path.abspath,\n        \"help\": \"Path to where certificate is saved (with certonly --csr), installed \"\n                \"from, or revoked\"\n    }\n    if verb == \"certonly\":\n        cpkwargs[\"default\"] = flag_default(\"auth_cert_path\")\n    add([\"paths\", \"install\", \"revoke\", \"certonly\", \"manage\"], \"--cert-path\", **cpkwargs)\n\n    section = \"paths\"\n    if isinstance(verb, str) and verb in (\"install\", \"revoke\"):\n        section = verb\n    add(section, \"--key-path\", type=os.path.abspath,\n        help=\"Path to private key for certificate installation \"\n             \"or revocation (if account key is missing)\")\n\n    default_cp = None\n    if verb == \"certonly\":\n        default_cp = flag_default(\"auth_chain_path\")\n    add([\"paths\", \"install\"], \"--fullchain-path\", default=default_cp, type=os.path.abspath,\n        help=\"Accompanying path to a full certificate chain (certificate plus chain).\")\n    add([\"paths\", \"install\"], \"--chain-path\", default=default_cp, type=os.path.abspath,\n        help=\"Accompanying path to a certificate chain.\")\n    add(\"paths\", \"--config-dir\", default=flag_default(\"config_dir\"),\n        help=config_help(\"config_dir\"))\n    add(\"paths\", \"--work-dir\", default=flag_default(\"work_dir\"),\n        help=config_help(\"work_dir\"))\n    add(\"paths\", \"--logs-dir\", default=flag_default(\"logs_dir\"),\n        help=\"Logs directory.\")\n    add([\"paths\", \"show_account\"], \"--server\", default=flag_default(\"server\"),\n        help=config_help(\"server\"))\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/plugins_parsing.py",
    "content": "\"\"\"This is a module that handles parsing of plugins for the argument parser\"\"\"\nfrom typing import TYPE_CHECKING\n\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot._internal.plugins import disco\n\nif TYPE_CHECKING:\n    from certbot._internal.cli import helpful\n\n\ndef _plugins_parsing(helpful: \"helpful.HelpfulArgumentParser\",\n                     plugins: disco.PluginsRegistry) -> None:\n    # It's nuts, but there are two \"plugins\" topics.  Somehow this works\n    helpful.add_group(\n        \"plugins\", description=\"Plugin Selection: Certbot client supports an \"\n        \"extensible plugins architecture. See '%(prog)s plugins' for a \"\n        \"list of all installed plugins and their names. You can force \"\n        \"a particular plugin by setting options provided below. Running \"\n        \"--help <plugin_name> will list flags specific to that plugin.\")\n\n    helpful.add(\"plugins\", \"--configurator\", default=flag_default(\"configurator\"),\n                help=\"Name of the plugin that is both an authenticator and an installer.\"\n                \" Should not be used together with --authenticator or --installer. \"\n                \"(default: Ask)\")\n    helpful.add([\"plugins\", \"reconfigure\"], \"-a\", \"--authenticator\",\n                default=flag_default(\"authenticator\"), help=\"Authenticator plugin name.\")\n    helpful.add([\"plugins\", \"reconfigure\"], \"-i\", \"--installer\", default=flag_default(\"installer\"),\n                help=\"Installer plugin name (also used to find domains).\")\n    helpful.add([\"plugins\", \"certonly\", \"run\", \"install\"],\n                \"--apache\", action=\"store_true\", default=flag_default(\"apache\"),\n                help=\"Obtain and install certificates using Apache\")\n    helpful.add([\"plugins\", \"certonly\", \"run\", \"install\"],\n                \"--nginx\", action=\"store_true\", default=flag_default(\"nginx\"),\n                help=\"Obtain and install certificates using Nginx\")\n    helpful.add([\"plugins\", \"certonly\"], \"--standalone\", action=\"store_true\",\n                default=flag_default(\"standalone\"),\n                help='Obtain certificates using a \"standalone\" webserver.')\n    helpful.add([\"plugins\", \"certonly\"], \"--manual\", action=\"store_true\",\n                default=flag_default(\"manual\"),\n                help=\"Provide laborious manual instructions for obtaining a certificate\")\n    helpful.add([\"plugins\", \"certonly\", \"reconfigure\"], \"--webroot\", action=\"store_true\",\n                default=flag_default(\"webroot\"),\n                help=\"Obtain certificates by placing files in a webroot directory.\")\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-cloudflare\", action=\"store_true\",\n                default=flag_default(\"dns_cloudflare\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using Cloudflare for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-digitalocean\", action=\"store_true\",\n                default=flag_default(\"dns_digitalocean\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using DigitalOcean for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-dnsimple\", action=\"store_true\",\n                default=flag_default(\"dns_dnsimple\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using DNSimple for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-dnsmadeeasy\", action=\"store_true\",\n                default=flag_default(\"dns_dnsmadeeasy\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using DNS Made Easy for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-gehirn\", action=\"store_true\",\n                default=flag_default(\"dns_gehirn\"),\n                help=(\"Obtain certificates using a DNS TXT record \"\n                      \"(if you are using Gehirn Infrastructure Service for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-google\", action=\"store_true\",\n                default=flag_default(\"dns_google\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using Google Cloud DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-linode\", action=\"store_true\",\n                default=flag_default(\"dns_linode\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using Linode for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-luadns\", action=\"store_true\",\n                default=flag_default(\"dns_luadns\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using LuaDNS for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-nsone\", action=\"store_true\",\n                default=flag_default(\"dns_nsone\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using NS1 for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-ovh\", action=\"store_true\",\n                default=flag_default(\"dns_ovh\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are \"\n                      \"using OVH for DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-rfc2136\", action=\"store_true\",\n                default=flag_default(\"dns_rfc2136\"),\n                help=\"Obtain certificates using a DNS TXT record (if you are using BIND for DNS).\")\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-route53\", action=\"store_true\",\n                default=flag_default(\"dns_route53\"),\n                help=(\"Obtain certificates using a DNS TXT record (if you are using Route53 for \"\n                      \"DNS).\"))\n    helpful.add([\"plugins\", \"certonly\"], \"--dns-sakuracloud\", action=\"store_true\",\n                default=flag_default(\"dns_sakuracloud\"),\n                help=(\"Obtain certificates using a DNS TXT record \"\n                     \"(if you are using Sakura Cloud for DNS).\"))\n\n    # things should not be reorder past/pre this comment:\n    # plugins_group should be displayed in --help before plugin\n    # specific groups (so that plugins_group.description makes sense)\n\n    helpful.add_plugin_args(plugins)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/subparsers.py",
    "content": "\"\"\"This module creates subparsers for the argument parser\"\"\"\nfrom typing import TYPE_CHECKING\n\nfrom certbot import interfaces\nfrom certbot._internal import constants\nfrom certbot._internal.cli.cli_utils import _EncodeReasonAction\nfrom certbot._internal.cli.cli_utils import _user_agent_comment_type\nfrom certbot._internal.cli.cli_utils import CaseInsensitiveList\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot._internal.cli.cli_utils import read_file\n\nif TYPE_CHECKING:\n    from certbot._internal.cli import helpful\n\n\ndef _create_subparsers(helpful: \"helpful.HelpfulArgumentParser\") -> None:\n    from certbot._internal.client import sample_user_agent  # avoid import loops\n    helpful.add(\n        None, \"--user-agent\", default=flag_default(\"user_agent\"),\n        help='Set a custom user agent string for the client. User agent strings allow '\n             'the CA to collect high level statistics about success rates by OS, '\n             'plugin and use case, and to know when to deprecate support for past Python '\n             \"versions and flags. If you wish to hide this information from the Let's \"\n             'Encrypt server, set this to \"\". '\n             '(default: {0}). The flags encoded in the user agent are: '\n             '--duplicate, --force-renew, --allow-subset-of-names, -n, and '\n             'whether any hooks are set.'.format(sample_user_agent()))\n    helpful.add(\n        None, \"--user-agent-comment\", default=flag_default(\"user_agent_comment\"),\n        type=_user_agent_comment_type,\n        help=\"Add a comment to the default user agent string. May be used when repackaging Certbot \"\n             \"or calling it from another tool to allow additional statistical data to be collected.\"\n             \" Ignored if --user-agent is set. (Example: Foo-Wrapper/1.0)\")\n    helpful.add(\"certonly\",\n                \"--csr\", default=flag_default(\"csr\"), type=read_file,\n                help=\"Path to a Certificate Signing Request (CSR) in DER or PEM format.\"\n                \" Currently --csr only works with the 'certonly' subcommand.\")\n    helpful.add(\"revoke\",\n                \"--reason\", dest=\"reason\",\n                choices=CaseInsensitiveList(constants.REVOCATION_REASONS.keys()),\n                action=_EncodeReasonAction, default=flag_default(\"reason\"),\n                help=\"Specify reason for revoking certificate. (default: unspecified)\")\n    helpful.add(\"revoke\",\n                \"--delete-after-revoke\", action=\"store_true\",\n                default=flag_default(\"delete_after_revoke\"),\n                help=\"Delete certificates after revoking them, along with all previous and later \"\n                \"versions of those certificates. (default: Ask)\")\n    helpful.add(\"revoke\",\n                \"--no-delete-after-revoke\", action=\"store_false\",\n                dest=\"delete_after_revoke\",\n                default=flag_default(\"delete_after_revoke\"),\n                help=\"Do not delete certificates after revoking them. This \"\n                     \"option should be used with caution because the 'renew' \"\n                     \"subcommand will attempt to renew undeleted revoked \"\n                     \"certificates. (default: Ask)\")\n    helpful.add(\"rollback\",\n                \"--checkpoints\", type=int, metavar=\"N\",\n                default=flag_default(\"rollback_checkpoints\"),\n                help=\"Revert configuration N number of checkpoints.\")\n    helpful.add(\"plugins\",\n                \"--init\", action=\"store_true\", default=flag_default(\"init\"),\n                help=\"Initialize plugins.\")\n    helpful.add(\"plugins\",\n                \"--prepare\", action=\"store_true\", default=flag_default(\"prepare\"),\n                help=\"Initialize and prepare plugins.\")\n    helpful.add(\"plugins\",\n                \"--authenticators\", action=\"append_const\", dest=\"ifaces\",\n                default=flag_default(\"ifaces\"),\n                const=interfaces.Authenticator, help=\"Limit to authenticator plugins only.\")\n    helpful.add(\"plugins\",\n                \"--installers\", action=\"append_const\", dest=\"ifaces\",\n                default=flag_default(\"ifaces\"),\n                const=interfaces.Installer, help=\"Limit to installer plugins only.\")\n"
  },
  {
    "path": "certbot/src/certbot/_internal/cli/verb_help.py",
    "content": "\"\"\"This module contain help information for verbs supported by certbot\"\"\"\nfrom certbot._internal.cli.cli_constants import SHORT_USAGE\n\n# The attributes here are:\n# short: a string that will be displayed by \"certbot -h commands\"\n# opts:  a string that heads the section of flags with which this command is documented,\n#        both for \"certbot -h SUBCOMMAND\" and \"certbot -h all\"\n# usage: an optional string that overrides the header of \"certbot -h SUBCOMMAND\"\nVERB_HELP = [\n    (\"run (default)\", {\n        \"short\": \"Obtain/renew a certificate, and install it\",\n        \"opts\": \"Options for obtaining & installing certificates\",\n        \"usage\": SHORT_USAGE.replace(\"[SUBCOMMAND]\", \"\"),\n        \"realname\": \"run\"\n    }),\n    (\"certonly\", {\n        \"short\": \"Obtain or renew a certificate, but do not install it\",\n        \"opts\": \"Options for modifying how a certificate is obtained\",\n        \"usage\": (\"\\n\\n  certbot certonly [options] [-d DOMAIN] [-d DOMAIN] ...\\n\\n\"\n                  \"This command obtains a TLS/SSL certificate without installing it anywhere.\")\n    }),\n    (\"renew\", {\n        \"short\": \"Renew all certificates (or one specified with --cert-name)\",\n        \"opts\": (\"The 'renew' subcommand will attempt to renew any certificates\"\n                 \" previously obtained if they are close to expiry, and print a\"\n                 \" summary of the results. By default, 'renew' will reuse the\"\n                 \" plugins and options used to obtain or most recently renew each\"\n                 \" certificate. You can test whether future renewals will succeed\"\n                 \" with `--dry-run`.\"\n                 \" Individual certificates can be renewed with the `--cert-name`\"\n                 \" option. Hooks are available to run commands\"\n                 \" before and after renewal; see\"\n                 \" https://certbot.eff.org/docs/using.html#renewal for more\"\n                 \" information on these.\"),\n        \"usage\": \"\\n\\n  certbot renew [--cert-name CERTNAME] [options]\\n\\n\"\n    }),\n    (\"certificates\", {\n        \"short\": \"List certificates managed by Certbot\",\n        \"opts\": \"List certificates managed by Certbot\",\n        \"usage\": (\"\\n\\n  certbot certificates [options] ...\\n\\n\"\n                  \"Print information about the status of certificates managed by Certbot.\")\n    }),\n    (\"delete\", {\n        \"short\": \"Clean up all files related to a certificate\",\n        \"opts\": \"Options for deleting a certificate\",\n        \"usage\": \"\\n\\n  certbot delete --cert-name CERTNAME\\n\\n\"\n    }),\n    (\"revoke\", {\n        \"short\": \"Revoke a certificate specified with --cert-path or --cert-name\",\n        \"opts\": \"Options for revocation of certificates\",\n        \"usage\": \"\\n\\n  certbot revoke [--cert-path /path/to/fullchain.pem | \"\n        \"--cert-name example.com] [options]\\n\\n\"\n    }),\n    (\"register\", {\n        \"short\": \"Register for account with Let's Encrypt / other ACME server\",\n        \"opts\": \"Options for account registration\",\n        \"usage\": \"\\n\\n  certbot register --email user@example.com [options]\\n\\n\"\n    }),\n    (\"update_account\", {\n        \"short\": \"Update existing account with Let's Encrypt / other ACME server\",\n        \"opts\": \"Options for account modification\",\n        \"usage\": \"\\n\\n  certbot update_account --email updated_email@example.com [options]\\n\\n\"\n    }),\n    (\"unregister\", {\n        \"short\": \"Irrevocably deactivate your account\",\n        \"opts\": \"Options for account deactivation.\",\n        \"usage\": \"\\n\\n  certbot unregister [options]\\n\\n\"\n    }),\n    (\"install\", {\n        \"short\": \"Install an arbitrary certificate in a server\",\n        \"opts\": \"Options for modifying how a certificate is deployed\",\n        \"usage\": \"\\n\\n  certbot install --cert-path /path/to/fullchain.pem \"\n        \" --key-path /path/to/private-key [options]\\n\\n\"\n    }),\n    (\"rollback\", {\n        \"short\": \"Roll back server conf changes made during certificate installation\",\n        \"opts\": \"Options for rolling back server configuration changes\",\n        \"usage\": \"\\n\\n  certbot rollback --checkpoints 3 [options]\\n\\n\"\n    }),\n    (\"plugins\", {\n        \"short\": \"List plugins that are installed and available on your system\",\n        \"opts\": 'Options for the \"plugins\" subcommand',\n        \"usage\": \"\\n\\n  certbot plugins [options]\\n\\n\"\n    }),\n    (\"enhance\", {\n        \"short\": \"Add security enhancements to your existing configuration\",\n        \"opts\": (\"Helps to harden the TLS configuration by adding security enhancements \"\n                 \"to already existing configuration.\"),\n        \"usage\": \"\\n\\n  certbot enhance [options]\\n\\n\"\n    }),\n    (\"show_account\", {\n        \"short\": \"Show account details from an ACME server\",\n        \"opts\": 'Options useful for the \"show_account\" subcommand:',\n        \"usage\": \"\\n\\n  certbot show_account [options]\\n\\n\"\n    }),\n    (\"reconfigure\", {\n        \"short\": \"Update renewal configuration for a certificate specified by --cert-name\",\n        \"opts\": 'Common options that may be updated with the \"reconfigure\" subcommand:',\n        \"usage\": \"\\n\\n  certbot reconfigure --cert-name CERTNAME [options]\\n\\n\"\n    }),\n]\n\n\n# VERB_HELP is a list in order to preserve order, but a dict is sometimes useful\nVERB_HELP_MAP = dict(VERB_HELP)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/client.py",
    "content": "\"\"\"Certbot client API.\"\"\"\nimport datetime\nimport logging\nimport platform\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import IO\nfrom typing import Optional\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric.rsa import generate_private_key\nimport josepy as jose\nfrom josepy import ES256\nfrom josepy import ES384\nfrom josepy import ES512\nfrom josepy import RS256\n\nfrom acme import client as acme_client\nfrom acme import crypto_util as acme_crypto_util\nfrom acme import errors as acme_errors\nfrom acme import messages\nimport certbot\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import account\nfrom certbot._internal import auth_handler\nfrom certbot._internal import cli\nfrom certbot._internal import constants\nfrom certbot._internal import eff\nfrom certbot._internal import error_handler\nfrom certbot._internal import san\nfrom certbot._internal import storage\nfrom certbot._internal.plugins import disco as plugin_disco\nfrom certbot._internal.plugins import selection as plugin_selection\nfrom certbot.compat import os\nfrom certbot.display import ops as display_ops\nfrom certbot.display import util as display_util\nfrom certbot.interfaces import AccountStorage\n\nlogger = logging.getLogger(__name__)\n\n\ndef create_acme_client(config: configuration.NamespaceConfig,\n                         key: Optional[jose.JWK] = None,\n                         regr: Optional[messages.RegistrationResource] = None,\n                         server_override: Optional[str] = None,\n                         ) -> acme_client.ClientV2:\n    \"\"\"Wrangle ACME client construction\"\"\"\n    alg = RS256\n    if key and key.typ == 'EC':\n        public_key = key.key\n        if public_key.key_size == 256:\n            alg = ES256\n        elif public_key.key_size == 384:\n            alg = ES384\n        elif public_key.key_size == 521:\n            alg = ES512\n        else:\n            raise errors.NotSupportedError(\n                \"No matching signing algorithm can be found for the key\"\n            )\n    net = acme_client.ClientNetwork(key, alg=alg, account=regr,\n                                    verify_ssl=(not config.no_verify_ssl),\n                                    user_agent=determine_user_agent(config))\n\n    server = config.server\n    if server_override:\n        server = server_override\n    directory = acme_client.ClientV2.get_directory(server, net)\n    return acme_client.ClientV2(directory, net)\n\n\ndef determine_user_agent(config: configuration.NamespaceConfig) -> str:\n    \"\"\"\n    Set a user_agent string in the config based on the choice of plugins.\n    (this wasn't knowable at construction time)\n\n    :returns: the client's User-Agent string\n    :rtype: `str`\n    \"\"\"\n\n    # WARNING: To ensure changes are in line with Certbot's privacy\n    # policy, talk to a core Certbot team member before making any\n    # changes here.\n    if config.user_agent is None:\n        ua = (\"CertbotACMEClient/{0} ({1}; {2}{8}) Authenticator/{3} Installer/{4} \"\n              \"({5}; flags: {6}) Py/{7}\")\n        if os.environ.get(\"CERTBOT_DOCS\") == \"1\":\n            cli_command = \"certbot\"\n            os_info = \"OS_NAME OS_VERSION\"\n            python_version = \"major.minor.patchlevel\"\n        else:\n            cli_command = cli.cli_command\n            os_info = util.get_os_info_ua()\n            python_version = platform.python_version()\n        ua = ua.format(certbot.__version__, cli_command, os_info,\n                       config.authenticator, config.installer, config.verb,\n                       ua_flags(config), python_version,\n                       \"; \" + config.user_agent_comment if config.user_agent_comment else \"\")\n    else:\n        ua = config.user_agent\n    return ua\n\n\ndef ua_flags(config: configuration.NamespaceConfig) -> str:\n    \"\"\"Turn some very important CLI flags into clues in the user agent.\"\"\"\n    if isinstance(config, DummyConfig):\n        return \"FLAGS\"\n    flags = []\n    if config.duplicate:\n        flags.append(\"dup\")\n    if config.renew_by_default:\n        flags.append(\"frn\")\n    if config.allow_subset_of_names:\n        flags.append(\"asn\")\n    if config.noninteractive_mode:\n        flags.append(\"n\")\n    hook_names = (\"pre\", \"post\", \"deploy\", \"manual_auth\", \"manual_cleanup\")\n    hooks = [getattr(config, h + \"_hook\") for h in hook_names]\n    if any(hooks):\n        flags.append(\"hook\")\n    return \" \".join(flags)\n\n\nclass DummyConfig:\n    \"\"\"Shim for computing a sample user agent.\"\"\"\n    def __init__(self) -> None:\n        self.authenticator = \"XXX\"\n        self.installer = \"YYY\"\n        self.user_agent = None\n        self.verb = \"SUBCOMMAND\"\n\n    def __getattr__(self, name: str) -> Any:\n        \"\"\"Any config properties we might have are None.\"\"\"\n        return None\n\n\ndef sample_user_agent() -> str:\n    \"\"\"Document what this Certbot's user agent string will be like.\"\"\"\n    # DummyConfig is designed to mock certbot.configuration.NamespaceConfig.\n    # Let mypy accept that.\n    return determine_user_agent(cast(configuration.NamespaceConfig, DummyConfig()))\n\n\ndef register(config: configuration.NamespaceConfig, account_storage: AccountStorage,\n             tos_cb: Optional[Callable[[str], None]] = None\n             ) -> tuple[account.Account, acme_client.ClientV2]:\n    \"\"\"Register new account with an ACME CA.\n\n    This function takes care of generating fresh private key,\n    registering the account, optionally accepting CA Terms of Service\n    and finally saving the account. It should be called prior to\n    initialization of `Client`, unless account has already been created.\n\n    :param certbot.configuration.NamespaceConfig config: Client configuration.\n\n    :param .AccountStorage account_storage: Account storage where newly\n        registered account will be saved to. Save happens only after TOS\n        acceptance step, so any account private keys or\n        `.RegistrationResource` will not be persisted if `tos_cb`\n        returns ``False``.\n\n    :param tos_cb: If ACME CA requires the user to accept a Terms of\n        Service before registering account, client action is\n        necessary. For example, a CLI tool would prompt the user\n        acceptance. `tos_cb` must be a callable that should accept\n        a Term of Service URL as a string, and raise an exception\n        if the TOS is not accepted by the client. ``tos_cb`` will be\n        called only if the client action is necessary, i.e. when\n        ``terms_of_service is not None``. This argument is optional,\n        if not supplied it will default to automatic acceptance!\n\n    :raises certbot.errors.Error: In case of any client problems, in\n        particular registration failure, or unaccepted Terms of Service.\n    :raises acme.errors.Error: In case of any protocol problems.\n\n    :returns: Newly registered and saved account, as well as protocol\n        API handle (should be used in `Client` initialization).\n    :rtype: `tuple` of `.Account` and `acme.client.Client`\n\n    \"\"\"\n    # Log non-standard actions, potentially wrong API calls\n    if account_storage.find_all():\n        logger.info(\"There are already existing accounts for %s\", config.server)\n\n    if config.email == \"\":\n        config.email = None\n    # If --dry-run is used, and there is no staging account, create one with no email.\n    if config.dry_run:\n        config.email = None\n\n    # Each new registration shall use a fresh new key\n    rsa_key = generate_private_key(\n            public_exponent=65537,\n            key_size=config.rsa_key_size,\n            backend=default_backend())\n    key = jose.JWKRSA(key=jose.ComparableRSAKey(rsa_key))\n    acme = create_acme_client(config, key)\n    # TODO: add phone?\n    regr = perform_registration(acme, config, tos_cb)\n\n    acc = account.Account(regr, key)\n    account_storage.save(acc, acme)\n\n    eff.prepare_subscription(config, acc)\n\n    return acc, acme\n\n\ndef perform_registration(acme: acme_client.ClientV2, config: configuration.NamespaceConfig,\n                         tos_cb: Optional[Callable[[str], None]]) -> messages.RegistrationResource:\n    \"\"\"\n    Actually register new account, trying repeatedly if there are email\n    problems\n\n    :param acme.client.Client acme: ACME client object.\n    :param certbot.configuration.NamespaceConfig config: Client configuration.\n    :param Callable tos_cb: a callback to handle Term of Service agreement.\n\n    :returns: Registration Resource.\n    :rtype: `acme.messages.RegistrationResource`\n    \"\"\"\n    if not acme.net.key:\n        raise errors.Error(\"acme client with no private key cannot register account.\")\n\n    eab_credentials_supplied = config.eab_kid and config.eab_hmac_key\n    eab: Optional[dict[str, Any]]\n    if eab_credentials_supplied:\n        account_public_key = acme.net.key.public_key()\n        eab = messages.ExternalAccountBinding.from_data(account_public_key=account_public_key,\n                                                        kid=config.eab_kid,\n                                                        hmac_key=config.eab_hmac_key,\n                                                        directory=acme.directory,\n                                                        hmac_alg=config.eab_hmac_alg)\n    else:\n        eab = None\n\n    if acme.external_account_required():\n        if not eab_credentials_supplied:\n            msg = (\"Server requires external account binding.\"\n                   \" Please use --eab-kid and --eab-hmac-key.\")\n            raise errors.Error(msg)\n\n    tos = acme.directory.meta.terms_of_service\n    if tos_cb and tos:\n        tos_cb(tos)\n\n    try:\n        return acme.new_account(messages.NewRegistration.from_data(\n                email=config.email, terms_of_service_agreed=True, external_account_binding=eab))\n    except messages.Error as e:\n        if e.code in (\"invalidEmail\", \"invalidContact\"):\n            if config.noninteractive_mode:\n                msg = (f\"The ACME server believes {config.email} is an invalid email address. \"\n                       \"Please ensure it is a valid email and attempt \"\n                       \"registration again.\")\n                raise errors.Error(msg)\n            config.email = display_ops.get_email(invalid=True)\n            return perform_registration(acme, config, tos_cb)\n        raise\n\n\nclass Client:\n    \"\"\"Certbot's client.\n\n    :ivar certbot.configuration.NamespaceConfig config: Client configuration.\n    :ivar .Account account: Account registered with `register`.\n    :ivar .AuthHandler auth_handler: Authorizations handler that will\n        dispatch DV challenges to appropriate authenticators\n        (providing `.Authenticator` interface).\n    :ivar .Authenticator auth: Prepared (`.Authenticator.prepare`)\n        authenticator that can solve ACME challenges.\n    :ivar .Installer installer: Installer.\n    :ivar acme.client.ClientV2 acme: Optional ACME client API handle. You might\n        already have one from `register`.\n\n    \"\"\"\n\n    def __init__(self, config: configuration.NamespaceConfig, account_: Optional[account.Account],\n                 auth: Optional[interfaces.Authenticator],\n                 installer: Optional[interfaces.Installer],\n                 acme: Optional[acme_client.ClientV2] = None) -> None:\n        \"\"\"Initialize a client.\"\"\"\n        self.config = config\n        self.account = account_\n        self.auth = auth\n        self.installer = installer\n\n        # Initialize ACME if account is provided\n        if acme is None and self.account is not None:\n            acme = create_acme_client(config, self.account.key, self.account.regr)\n        self.acme = acme\n\n        self.auth_handler: Optional[auth_handler.AuthHandler]\n        if auth is not None:\n            self.auth_handler = auth_handler.AuthHandler(\n                auth, self.acme, self.account, self.config.pref_challs)\n        else:\n            self.auth_handler = None\n\n    def obtain_certificate_from_csr(self, csr: util.CSR,\n                                    orderr: Optional[messages.OrderResource] = None\n                                    ) -> tuple[bytes, bytes]:\n        \"\"\"Obtain certificate.\n\n        :param .util.CSR csr: PEM-encoded Certificate Signing\n            Request. The key used to generate this CSR can be different\n            than `authkey`.\n        :param acme.messages.OrderResource orderr: contains authzrs\n\n        :returns: certificate and chain as PEM byte strings\n        :rtype: tuple\n\n        \"\"\"\n        if self.auth_handler is None:\n            msg = (\"Unable to obtain certificate because authenticator is \"\n                   \"not set.\")\n            logger.error(msg)\n            raise errors.Error(msg)\n        if self.account is None or self.account.regr is None:\n            raise errors.Error(\"Please register with the ACME server first.\")\n        if self.acme is None:\n            raise errors.Error(\"ACME client is not set.\")\n\n        logger.debug(\"CSR: %s\", csr)\n\n        if orderr is None:\n            orderr = self._get_order_and_authorizations(csr.data, best_effort=False)\n\n        deadline = datetime.datetime.now() + datetime.timedelta(\n            seconds=self.config.issuance_timeout)\n\n        logger.debug(\"Will poll for certificate issuance until %s\", deadline)\n\n        orderr = self.acme.finalize_order(\n            orderr, deadline, fetch_alternative_chains=self.config.preferred_chain is not None)\n\n        fullchain = orderr.fullchain_pem\n        if self.config.preferred_chain and orderr.alternative_fullchains_pem:\n            fullchain = crypto_util.find_chain_with_issuer(\n                [fullchain] + orderr.alternative_fullchains_pem,\n                self.config.preferred_chain, not self.config.dry_run)\n        cert, chain = crypto_util.cert_and_chain_from_fullchain(fullchain)\n        return cert.encode(), chain.encode()\n\n    def obtain_certificate(self, sans: list[san.SAN], old_keypath: Optional[str] = None\n                           ) -> tuple[bytes, bytes, util.Key, util.CSR]:\n        \"\"\"Obtains a certificate from the ACME server.\n\n        `.register` must be called before `.obtain_certificate`\n\n        :param list sans: domains and/or IP addresses for which to get a certificate.\n\n        :returns: certificate as PEM string, chain as PEM string,\n            newly generated private key (`.util.Key`), and DER-encoded\n            Certificate Signing Request (`.util.CSR`).\n        :rtype: tuple\n\n        \"\"\"\n        # We need to determine the key path, key PEM data, CSR path,\n        # and CSR PEM data.  For a dry run, the paths are None because\n        # they aren't permanently saved to disk.  For a lineage with\n        # --reuse-key, the key path and PEM data are derived from an\n        # existing file.\n\n        if old_keypath is not None:\n            # We've been asked to reuse a specific existing private key.\n            # Therefore, we'll read it now and not generate a new one in\n            # either case below.\n            #\n            # We read in bytes here because the type of `key.pem`\n            # created below is also bytes.\n            with open(old_keypath, \"rb\") as f:\n                keypath = old_keypath\n                keypem = f.read()\n            key: Optional[util.Key] = util.Key(file=keypath, pem=keypem)\n            logger.info(\"Reusing existing private key from %s.\", old_keypath)\n        else:\n            # The key is set to None here but will be created below.\n            key = None\n\n        key_size = self.config.rsa_key_size\n        elliptic_curve = \"secp256r1\"\n\n        # key-type defaults to a list, but we are only handling 1 currently\n        if isinstance(self.config.key_type, list):\n            self.config.key_type = self.config.key_type[0]\n        if self.config.elliptic_curve and self.config.key_type == 'ecdsa':\n            elliptic_curve = self.config.elliptic_curve\n            self.config.auth_chain_path = \"./chain-ecdsa.pem\"\n            self.config.auth_cert_path = \"./cert-ecdsa.pem\"\n            self.config.key_path = \"./key-ecdsa.pem\"\n        elif self.config.rsa_key_size and self.config.key_type.lower() == 'rsa':\n            key_size = self.config.rsa_key_size\n\n        domains, ip_addresses = san.split(sans)\n        domains_str = [d.dns_name for d in domains]\n        ip_addresses_typed = [i.ip_address for i in ip_addresses]\n\n        # Create CSR from names\n        if self.config.dry_run:\n            key = key or util.Key(\n                file=None,\n                pem=crypto_util.make_key(\n                    bits=key_size,\n                    elliptic_curve=elliptic_curve,\n                    key_type=self.config.key_type,\n\n                ),\n            )\n            csr = util.CSR(file=None, form=\"pem\",\n                           data=acme_crypto_util.make_csr(\n                               key.pem, domains_str, self.config.must_staple,\n                               ipaddrs=ip_addresses_typed))\n        else:\n            key = key or crypto_util.generate_key(\n                key_size=key_size,\n                key_dir=None,\n                key_type=self.config.key_type,\n                elliptic_curve=elliptic_curve,\n                strict_permissions=self.config.strict_permissions,\n            )\n            csr = crypto_util.generate_csr(\n                key, domains_str, None, self.config.must_staple, self.config.strict_permissions,\n                ipaddrs=ip_addresses_typed)\n\n        try:\n            orderr = self._get_order_and_authorizations(csr.data, self.config.allow_subset_of_names)\n        except messages.Error as error:\n            # Some sans may be rejected during order creation.\n            # Certbot can retry the operation without the rejected\n            # sans contained within subproblems.\n            if self.config.allow_subset_of_names:\n                successful_sans = self._successful_sans_from_error(error, sans)\n                if successful_sans != sans and len(successful_sans) != 0:\n                    return self._retry_obtain_certificate(sans, successful_sans, old_keypath)\n            raise\n        authzr = orderr.authorizations\n        auth_ident_values = {a.body.identifier.value for a in authzr}\n        successful_sans = [s for s in sans if str(s) in auth_ident_values]\n\n        # allow_subset_of_names is currently disabled for wildcard\n        # certificates. The reason for this and checking allow_subset_of_names\n        # below is because successful_sans == sans is never true if\n        # sans contains a wildcard because the ACME spec forbids identifiers\n        # in authzs from containing a wildcard character.\n        if self.config.allow_subset_of_names and successful_sans != sans:\n            return self._retry_obtain_certificate(sans, successful_sans, old_keypath)\n        else:\n            try:\n                cert, chain = self.obtain_certificate_from_csr(csr, orderr)\n                return cert, chain, key, csr\n            except messages.Error as error:\n                # Some sans may be rejected during the very late stage of\n                # order finalization. Certbot can retry the operation without\n                # the rejected sans contained within subproblems.\n                if self.config.allow_subset_of_names:\n                    successful_sans = self._successful_sans_from_error(error, sans)\n                    if successful_sans != sans and len(successful_sans) != 0:\n                        return self._retry_obtain_certificate(\n                            sans, successful_sans, old_keypath)\n                raise\n\n    def _get_order_and_authorizations(self, csr_pem: bytes,\n                                      best_effort: bool) -> messages.OrderResource:\n        \"\"\"Request a new order and complete its authorizations.\n\n        :param bytes csr_pem: A CSR in PEM format.\n        :param bool best_effort: True if failing to complete all\n            authorizations should not raise an exception\n\n        :returns: order resource containing its completed authorizations\n        :rtype: acme.messages.OrderResource\n\n        \"\"\"\n        if not self.acme:\n            raise errors.Error(\"ACME client is not set.\")\n\n        profile = None\n        available_profiles = self.acme.directory.meta.profiles\n        preferred_profile = self.config.preferred_profile\n        if self.config.required_profile is not None:\n            profile = self.config.required_profile\n        elif (preferred_profile and available_profiles and\n              preferred_profile in available_profiles):\n            profile = preferred_profile\n        try:\n            orderr = self.acme.new_order(csr_pem, profile=profile)\n        except acme_errors.WildcardUnsupportedError:\n            raise errors.Error(\"The currently selected ACME CA endpoint does\"\n                               \" not support issuing wildcard certificates.\")\n\n        if not self.auth_handler:\n            raise errors.Error(\"No authorization handler has been set.\")\n\n        # For a dry run, ensure we have an order with fresh authorizations\n        if orderr and self.config.dry_run:\n            deactivated, failed = self.auth_handler.deactivate_valid_authorizations(orderr)\n            if deactivated:\n                logger.debug(\"Recreating order after authz deactivations\")\n                orderr = self.acme.new_order(csr_pem, profile=profile)\n            if failed:\n                logger.warning(\"Certbot was unable to obtain fresh authorizations for every domain\"\n                               \". The dry run will continue, but results may not be accurate.\")\n\n        authzr = self.auth_handler.handle_authorizations(orderr, self.config, best_effort)\n        return orderr.update(authorizations=authzr)\n\n    def obtain_and_enroll_certificate(self, sans: list[san.SAN], certname: Optional[str]\n                                      ) -> Optional[storage.RenewableCert]:\n        \"\"\"Obtain and enroll certificate.\n\n        Get a new certificate for the specified domains and/or IP addresses using the specified\n        authenticator and installer, and then create a new renewable lineage\n        containing it.\n\n        :param sans: domains and/or IP addresses to request a certificate for\n        :type sans: `list` of `san.SAN`\n        :param certname: requested name of lineage\n        :type certname: `str` or `None`\n\n        :returns: A new :class:`certbot._internal.storage.RenewableCert` instance\n            referred to the enrolled cert lineage, or None if doing a successful dry run.\n\n        \"\"\"\n        new_name = self._choose_lineagename(sans, certname)\n        cert, chain, key, _ = self.obtain_certificate(sans)\n\n        if (self.config.config_dir != constants.CLI_DEFAULTS[\"config_dir\"] or\n                self.config.work_dir != constants.CLI_DEFAULTS[\"work_dir\"]):\n            logger.info(\n                \"Non-standard path(s), might not work with crontab installed \"\n                \"by your operating system package manager\")\n\n        if self.config.dry_run:\n            logger.debug(\"Dry run: Skipping creating new lineage for %s\", new_name)\n            return None\n        return storage.RenewableCert.new_lineage(\n            new_name, cert,\n            key.pem, chain,\n            self.config)\n\n    def _successful_sans_from_error(self, error: messages.Error, sans: list[san.SAN],\n                                    ) -> list[san.SAN]:\n        if error.subproblems is not None:\n            failed_sans: list[san.SAN] = []\n            for problem in error.subproblems:\n                if not problem.identifier:\n                    continue\n                match problem.identifier.typ:\n                    case messages.IDENTIFIER_FQDN:\n                        failed_sans.append(san.DNSName(problem.identifier.value))\n                    case messages.IDENTIFIER_IP:\n                        failed_sans.append(san.IPAddress(problem.identifier.value))\n                    case _:\n                        raise TypeError(f\"invalid identifier type {problem.identifier.typ}\")\n            successful_sans = [x for x in sans if x not in failed_sans]\n            return successful_sans\n        return []\n\n    def _retry_obtain_certificate(self, sans: list[san.SAN], successful_sans: list[san.SAN],\n                                old_keypath: Optional[str]\n                                ) -> tuple[bytes, bytes, util.Key, util.CSR]:\n        failed_sans = [s for s in sans if s not in successful_sans]\n        failed_sans_list = san.display(failed_sans)\n        display_util.notify(\"Unable to obtain a certificate with every requested \"\n            f\"domain. Retrying without: {failed_sans_list}\")\n        return self.obtain_certificate(successful_sans, old_keypath)\n\n    def _choose_lineagename(self, sans: list[san.SAN], certname: Optional[str]) -> str:\n        \"\"\"Chooses a name for the new lineage.\n\n        :param sans: domains and/or IP addresses in certificate request\n        :type sans: `list` of `san.SAN`\n        :param certname: requested name of lineage\n        :type certname: `str` or `None`\n\n        :returns: lineage name that should be used\n        :rtype: str\n\n        :raises errors.Error: If the chosen lineage name is invalid.\n\n        \"\"\"\n        # Remember chosen name for new lineage\n        lineagename = None\n        if certname:\n            lineagename = certname\n        elif sans[0].is_wildcard():\n            # Don't make files and directories starting with *.\n            lineagename = str(sans[0])[2:]\n        else:\n            lineagename = str(sans[0])\n        # Verify whether chosen lineage is valid\n        if self._is_valid_lineagename(lineagename):\n            return lineagename\n        else:\n            raise errors.Error(\n                \"The provided certname cannot be used as a lineage name because it contains \"\n                \"an illegal character (i.e. filepath separator).\" if certname else\n                \"Cannot use domain name as lineage name because it contains an illegal \"\n                \"character (i.e. filepath separator). Specify an explicit lineage name \"\n                \"with --cert-name.\")\n\n    def _is_valid_lineagename(self, name: str) -> bool:\n        \"\"\"Determines whether the provided name is a valid lineagename. A lineagename\n        is invalid when it contains filepath separators.\n\n        :param name: the lineage name to determine validity for\n        :type name: `str`\n\n        :returns: Whether the provided string constitutes a valid lineage name.\n        :rtype: bool\n\n        \"\"\"\n        return os.path.sep not in name\n\n    def save_certificate(self, cert_pem: bytes, chain_pem: bytes,\n                         cert_path: str, chain_path: str, fullchain_path: str\n                         ) -> tuple[str, str, str]:\n        \"\"\"Saves the certificate received from the ACME server.\n\n        :param bytes cert_pem:\n        :param bytes chain_pem:\n        :param str cert_path: Candidate path to a certificate.\n        :param str chain_path: Candidate path to a certificate chain.\n        :param str fullchain_path: Candidate path to a full cert chain.\n\n        :returns: cert_path, chain_path, and fullchain_path as absolute\n            paths to the actual files\n        :rtype: `tuple` of `str`\n\n        :raises IOError: If unable to find room to write the cert files\n\n        \"\"\"\n        for path in cert_path, chain_path, fullchain_path:\n            util.make_or_verify_dir(os.path.dirname(path), 0o755, self.config.strict_permissions)\n\n        cert_file, abs_cert_path = _open_pem_file(self.config, 'cert_path', cert_path)\n\n        try:\n            cert_file.write(cert_pem)\n        finally:\n            cert_file.close()\n\n        chain_file, abs_chain_path = _open_pem_file(self.config, 'chain_path', chain_path)\n        fullchain_file, abs_fullchain_path = _open_pem_file(\n            self.config, 'fullchain_path', fullchain_path)\n\n        _save_chain(chain_pem, chain_file)\n        _save_chain(cert_pem + chain_pem, fullchain_file)\n\n        return abs_cert_path, abs_chain_path, abs_fullchain_path\n\n    def deploy_certificate(self, sans: list[san.DNSName], privkey_path: str, cert_path: str,\n                           chain_path: str, fullchain_path: str) -> None:\n        \"\"\"Install certificate\n\n        :param list sans: list of domains/and or IP addresses to install the certificate\n        :param str privkey_path: path to certificate private key\n        :param str cert_path: certificate file path (optional)\n        :param str fullchain_path: path to the full chain of the certificate\n        :param str chain_path: chain file path\n\n        \"\"\"\n        if self.installer is None:\n            logger.error(\"No installer specified, client is unable to deploy\"\n                           \"the certificate\")\n            raise errors.Error(\"No installer available\")\n\n        chain_path = None if chain_path is None else os.path.abspath(chain_path)\n\n        display_util.notify(\"Deploying certificate\")\n\n        msg = \"Could not install certificate\"\n        domains, ip_addresses = san.split(sans)\n        if ip_addresses:\n            raise TypeError(\"deploy of IP address certificate not supported\")\n        with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):\n            for dom in domains:\n                self.installer.deploy_cert(\n                    domain=dom.dns_name, cert_path=os.path.abspath(cert_path),\n                    key_path=os.path.abspath(privkey_path),\n                    chain_path=chain_path,\n                    fullchain_path=fullchain_path)\n                self.installer.save()  # needed by the Apache plugin\n\n            self.installer.save(\"Deployed ACME Certificate\")\n\n        msg = (\"We were unable to install your certificate, \"\n               \"however, we successfully restored your \"\n               \"server to its prior configuration.\")\n        with error_handler.ErrorHandler(self._rollback_and_restart, msg):\n            # sites may have been enabled / final cleanup\n            self.installer.restart()\n\n    def enhance_config(self, domains: list[san.DNSName], chain_path: str,\n                       redirect_default: bool = True) -> None:\n        \"\"\"Enhance the configuration.\n\n        :param list domains: list of domains to configure.\n        :param chain_path: chain file path\n        :type chain_path: `str` or `None`\n        :param redirect_default: boolean value that the \"redirect\" flag should default to\n\n        :raises .errors.Error: if no installer is specified in the\n            client.\n        \"\"\"\n        if self.installer is None:\n            logger.error(\"No installer is specified, there isn't any \"\n                           \"configuration to enhance.\")\n            raise errors.Error(\"No installer available\")\n\n        enhanced = False\n        enhancement_info = (\n            (\"hsts\", \"ensure-http-header\", \"Strict-Transport-Security\"),\n            (\"redirect\", \"redirect\", None),\n            (\"staple\", \"staple-ocsp\", chain_path),\n            (\"uir\", \"ensure-http-header\", \"Upgrade-Insecure-Requests\"),)\n        supported = self.installer.supported_enhancements()\n\n        for config_name, enhancement_name, option in enhancement_info:\n            config_value = getattr(self.config, config_name)\n            if enhancement_name in supported:\n                if config_name == \"redirect\" and config_value is None:\n                    config_value = redirect_default\n                if config_value:\n                    self.apply_enhancement(domains, enhancement_name, option)\n                    enhanced = True\n            elif config_value:\n                logger.error(\n                    \"Option %s is not supported by the selected installer. \"\n                    \"Skipping enhancement.\", config_name)\n\n        msg = \"We were unable to restart web server\"\n        if enhanced:\n            with error_handler.ErrorHandler(self._rollback_and_restart, msg):\n                self.installer.restart()\n\n    def apply_enhancement(self, domains: list[san.DNSName], enhancement: str,\n                          options: Optional[str] = None) -> None:\n        \"\"\"Applies an enhancement on all domains.\n\n        :param list domains: list of ssl_vhosts (as san.DNSName)\n        :param str enhancement: name of enhancement, e.g. ensure-http-header\n        :param str options: options to enhancement, e.g. Strict-Transport-Security\n\n            .. note:: When more `options` are needed, make options a list.\n\n        :raises .errors.PluginError: If Enhancement is not supported, or if\n            there is any other problem with the enhancement.\n\n\n        \"\"\"\n        if not self.installer:\n            raise errors.Error(\"No installer plugin has been set.\")\n        enh_label = options if enhancement == \"ensure-http-header\" else enhancement\n        with error_handler.ErrorHandler(self._recovery_routine_with_msg, None):\n            for dom in domains:\n                try:\n                    self.installer.enhance(dom.dns_name, enhancement, options)\n                except errors.PluginEnhancementAlreadyPresent:\n                    logger.info(\"Enhancement %s was already set.\", enh_label)\n                except errors.PluginError:\n                    logger.error(\"Unable to set the %s enhancement for %s.\", enh_label, dom)\n                    raise\n\n            self.installer.save(f\"Add enhancement {enh_label}\")\n\n    def _recovery_routine_with_msg(self, success_msg: Optional[str]) -> None:\n        \"\"\"Calls the installer's recovery routine and prints success_msg\n\n        :param str success_msg: message to show on successful recovery\n\n        \"\"\"\n        if self.installer:\n            self.installer.recovery_routine()\n            if success_msg:\n                display_util.notify(success_msg)\n\n    def _rollback_and_restart(self, success_msg: str) -> None:\n        \"\"\"Rollback the most recent checkpoint and restart the webserver\n\n        :param str success_msg: message to show on successful rollback\n\n        \"\"\"\n        if self.installer:\n            logger.info(\"Rolling back to previous server configuration...\")\n            try:\n                self.installer.rollback_checkpoints()\n                self.installer.restart()\n            except:\n                logger.error(\n                    \"An error occurred and we failed to restore your config and \"\n                    \"restart your server. Please post to \"\n                    \"https://community.letsencrypt.org/c/help \"\n                    \"with details about your configuration and this error you received.\"\n                )\n                raise\n            display_util.notify(success_msg)\n\n\ndef validate_key_csr(privkey: util.Key, csr: Optional[util.CSR] = None) -> None:\n    \"\"\"Validate Key and CSR files.\n\n    Verifies that the client key and csr arguments are valid and correspond to\n    one another. This does not currently check the names in the CSR due to\n    the inability to read SANs from CSRs in python crypto libraries.\n\n    If csr is left as None, only the key will be validated.\n\n    :param privkey: Key associated with CSR\n    :type privkey: :class:`certbot.util.Key`\n\n    :param .util.CSR csr: CSR\n\n    :raises .errors.Error: when validation fails\n\n    \"\"\"\n    # TODO: Handle all of these problems appropriately\n    # The client can eventually do things like prompt the user\n    # and allow the user to take more appropriate actions\n\n    # Key must be readable and valid.\n    if privkey.pem and not crypto_util.valid_privkey(privkey.pem):\n        raise errors.Error(\"The provided key is not a valid key\")\n\n    if csr:\n        if csr.form == \"der\":\n            csr_obj = x509.load_der_x509_csr(csr.data)\n            csr_pem = csr_obj.public_bytes(serialization.Encoding.PEM)\n            csr = util.CSR(csr.file, csr_pem, \"pem\")\n\n        # If CSR is provided, it must be readable and valid.\n        if csr.data and not crypto_util.valid_csr(csr.data):\n            raise errors.Error(\"The provided CSR is not a valid CSR\")\n\n        # If both CSR and key are provided, the key must be the same key used\n        # in the CSR.\n        if csr.data and privkey.pem:\n            if not crypto_util.csr_matches_pubkey(\n                    csr.data, privkey.pem):\n                raise errors.Error(\"The key and CSR do not match\")\n\n\ndef rollback(default_installer: str, checkpoints: int,\n             config: configuration.NamespaceConfig, plugins: plugin_disco.PluginsRegistry) -> None:\n    \"\"\"Revert configuration the specified number of checkpoints.\n\n    :param str default_installer: Default installer name to use for the rollback\n    :param int checkpoints: Number of checkpoints to revert.\n    :param config: Configuration.\n    :type config: :class:`certbot.configuration.NamespaceConfiguration`\n    :param plugins: Plugins available\n    :type plugins: :class:`certbot._internal.plugins.disco.PluginsRegistry`\n\n    \"\"\"\n    # Misconfigurations are only a slight problems... allow the user to rollback\n    installer = plugin_selection.pick_installer(\n        config, default_installer, plugins, question=\"Which installer \"\n        \"should be used for rollback?\")\n\n    # No Errors occurred during init... proceed normally\n    # If installer is None... couldn't find an installer... there shouldn't be\n    # anything to rollback\n    if installer is not None:\n        installer.rollback_checkpoints(checkpoints)\n        installer.restart()\n\n\ndef _open_pem_file(config: configuration.NamespaceConfig,\n                   cli_arg_path: str, pem_path: str) -> tuple[IO, str]:\n    \"\"\"Open a pem file.\n\n    If cli_arg_path was set by the client, open that.\n    Otherwise, uniquify the file path.\n\n    :param str cli_arg_path: the cli arg name, e.g. cert_path\n    :param str pem_path: the pem file path to open\n\n    :returns: a tuple of file object and its absolute file path\n\n    \"\"\"\n    if config.set_by_user(cli_arg_path):\n        return util.safe_open(pem_path, chmod=0o644, mode=\"wb\"),\\\n            os.path.abspath(pem_path)\n    uniq = util.unique_file(pem_path, 0o644, \"wb\")\n    return uniq[0], os.path.abspath(uniq[1])\n\n\ndef _save_chain(chain_pem: bytes, chain_file: IO) -> None:\n    \"\"\"Saves chain_pem at a unique path based on chain_path.\n\n    :param bytes chain_pem: certificate chain in PEM format\n    :param str chain_file: chain file object\n\n    \"\"\"\n    try:\n        chain_file.write(chain_pem)\n    finally:\n        chain_file.close()\n"
  },
  {
    "path": "certbot/src/certbot/_internal/constants.py",
    "content": "\"\"\"Certbot constants.\"\"\"\nimport atexit\nimport importlib.resources\nimport logging\nfrom contextlib import ExitStack\nfrom typing import Any\n\nfrom acme import challenges\nfrom certbot.compat import misc\nfrom certbot.compat import os\n\nSETUPTOOLS_PLUGINS_ENTRY_POINT = \"certbot.plugins\"\n\"\"\"Setuptools entry point group name for plugins.\"\"\"\n\nOLD_SETUPTOOLS_PLUGINS_ENTRY_POINT = \"letsencrypt.plugins\"\n\"\"\"Plugins Setuptools entry point before rename.\"\"\"\n\nCLI_DEFAULTS: dict[str, Any] = dict(  # pylint: disable=use-dict-literal\n    config_files=[\n        os.path.join(misc.get_default_folder('config'), 'cli.ini'),\n        # https://freedesktop.org/wiki/Software/xdg-user-dirs/\n        os.path.join(os.environ.get(\"XDG_CONFIG_HOME\", \"~/.config\"),\n                     \"letsencrypt\", \"cli.ini\"),\n    ],\n\n    # Main parser\n    verbose_count=0,\n    verbose_level=None,\n    text_mode=False,\n    max_log_backups=1000,\n    preconfigured_renewal=False,\n    noninteractive_mode=False,\n    force_interactive=False,\n    domains=[],\n    ip_addresses=[],\n    certname=None,\n    dry_run=False,\n    register_unsafely_without_email=False,\n    email=None,\n    eff_email=None, # listed as Ask in help output\n    reinstall=False,\n    expand=False,\n    renew_by_default=False,\n    renew_with_new_domains=False,\n    autorenew=True,\n    allow_subset_of_names=False,\n    tos=False,\n    account=None,\n    duplicate=False,\n    os_packages_only=False,\n    no_self_upgrade=False,\n    no_permissions_check=False,\n    no_bootstrap=False,\n    quiet=False,\n    staging=False,\n    debug=False,\n    debug_challenges=False,\n    no_verify_ssl=False,\n    http01_port=challenges.HTTP01Response.PORT,\n    http01_address=\"\",\n    https_port=443,\n    break_my_certs=False,\n    rsa_key_size=2048,\n    elliptic_curve=\"secp256r1\",\n    key_type=\"ecdsa\",\n    must_staple=False,\n    redirect=None, # default described manually in text in help output\n    auto_hsts=False,\n    hsts=None, # listed as False in help output\n    uir=None, # listed as False in help output\n    staple=None, # listed as False in help output\n    strict_permissions=False,\n    required_profile=None,\n    preferred_profile=None,\n    preferred_chain=None,\n    pref_challs=[],\n    validate_hooks=True,\n    directory_hooks=True,\n    reuse_key=False,\n    new_key=False,\n    disable_renew_updates=False,\n    random_sleep_on_renew=True,\n    eab_hmac_key=None,\n    eab_kid=None,\n    eab_hmac_alg=\"HS256\",\n    issuance_timeout=90,\n    run_deploy_hooks=False,\n\n    # Subparsers\n    num=None,\n    user_agent=None,\n    user_agent_comment=None,\n    csr=None,\n    reason=0,\n    delete_after_revoke=None, # listed as Ask in help output\n    rollback_checkpoints=1,\n    init=False,\n    prepare=False,\n    ifaces=None,\n\n    # Path parsers\n    auth_cert_path=\"./cert.pem\",\n    auth_chain_path=\"./chain.pem\",\n    key_path=None,\n    config_dir=misc.get_default_folder('config'),\n    work_dir=misc.get_default_folder('work'),\n    logs_dir=misc.get_default_folder('logs'),\n    server=\"https://acme-v02.api.letsencrypt.org/directory\",\n\n    # Plugins parsers\n    configurator=None,\n    authenticator=None,\n    installer=None,\n    apache=False,\n    nginx=False,\n    standalone=False,\n    manual=False,\n    webroot=False,\n    dns_cloudflare=False,\n    dns_digitalocean=False,\n    dns_dnsimple=False,\n    dns_dnsmadeeasy=False,\n    dns_gehirn=False,\n    dns_google=False,\n    dns_linode=False,\n    dns_luadns=False,\n    dns_nsone=False,\n    dns_ovh=False,\n    dns_rfc2136=False,\n    dns_route53=False,\n    dns_sakuracloud=False\n\n)\nSTAGING_URI = \"https://acme-staging-v02.api.letsencrypt.org/directory\"\n\nV1_URI = \"https://acme-v01.api.letsencrypt.org/directory\"\n\n# The set of reasons for revoking a certificate is defined in RFC 5280 in\n# section 5.3.1. The reasons that users are allowed to submit are restricted to\n# those accepted by the ACME server implementation. They are listed in\n# `letsencrypt.boulder.revocation.reasons.go`.\nREVOCATION_REASONS = {\n    \"unspecified\": 0,\n    \"keycompromise\": 1,\n    \"affiliationchanged\": 3,\n    \"superseded\": 4,\n    \"cessationofoperation\": 5}\n\n\"\"\"Defaults for CLI flags and `certbot.configuration.NamespaceConfig` attributes.\"\"\"\n\nQUIET_LOGGING_LEVEL = logging.ERROR\n\"\"\"Logging level to use in quiet mode.\"\"\"\n\nDEFAULT_LOGGING_LEVEL = logging.WARNING\n\"\"\"Default logging level to use when not in quiet mode.\"\"\"\n\nARCHIVE_DIR = \"archive\"\n\"\"\"Archive directory, relative to `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nCONFIG_DIRS_MODE = 0o755\n\"\"\"Directory mode for ``certbot.configuration.NamespaceConfig.config_dir`` et al.\"\"\"\n\nACCOUNTS_DIR = \"accounts\"\n\"\"\"Directory where all accounts are saved.\"\"\"\n\nLE_REUSE_SERVERS = {\n    os.path.normpath('acme-v02.api.letsencrypt.org/directory'):\n        os.path.normpath('acme-v01.api.letsencrypt.org/directory'),\n    os.path.normpath('acme-staging-v02.api.letsencrypt.org/directory'):\n        os.path.normpath('acme-staging.api.letsencrypt.org/directory')\n}\n\"\"\"Servers that can reuse accounts from other servers.\"\"\"\n\nBACKUP_DIR = \"backups\"\n\"\"\"Directory (relative to `certbot.configuration.NamespaceConfig.work_dir`)\nwhere backups are kept.\"\"\"\n\nIN_PROGRESS_DIR = \"IN_PROGRESS\"\n\"\"\"Directory used before a permanent checkpoint is finalized (relative to\n`certbot.configuration.NamespaceConfig.work_dir`).\"\"\"\n\nKEY_DIR = \"keys\"\n\"\"\"Directory (relative to `certbot.configuration.NamespaceConfig.config_dir`)\nwhere keys are saved.\"\"\"\n\nLIVE_DIR = \"live\"\n\"\"\"Live directory, relative to `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nTEMP_CHECKPOINT_DIR = \"temp_checkpoint\"\n\"\"\"Temporary checkpoint directory, relative\nto `certbot.configuration.NamespaceConfig.work_dir`.\"\"\"\n\nRENEWAL_CONFIGS_DIR = \"renewal\"\n\"\"\"Renewal configs directory, relative\nto `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nRENEWAL_HOOKS_DIR = \"renewal-hooks\"\n\"\"\"Basename of directory containing hooks to run when getting or renewing certificates.\"\"\"\n\nRENEWAL_PRE_HOOKS_DIR = \"pre\"\n\"\"\"Basename of directory containing pre-hooks to run\nbefore attempting to get or renew certs\"\"\"\n\nRENEWAL_DEPLOY_HOOKS_DIR = \"deploy\"\n\"\"\"Basename of directory containing deploy-hooks to run\nupon successfully getting or renewing certs.\"\"\"\n\nRENEWAL_POST_HOOKS_DIR = \"post\"\n\"\"\"Basename of directory containing post-hooks to run after attempting to get or renew certs.\"\"\"\n\nFORCE_INTERACTIVE_FLAG = \"--force-interactive\"\n\"\"\"Flag to disable TTY checking in certbot.display.util.\"\"\"\n\nEFF_SUBSCRIBE_URI = \"https://supporters.eff.org/subscribe/certbot\"\n\"\"\"EFF URI used to submit the e-mail address of users who opt-in.\"\"\"\n\nSSL_DHPARAMS_DEST = \"ssl-dhparams.pem\"\n\"\"\"Name of the ssl_dhparams file as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\ndef _generate_ssl_dhparams_src_static() -> str:\n    # This code ensures that the resource is accessible as file for the lifetime of current\n    # Python process, and will be automatically cleaned up on exit.\n    file_manager = ExitStack()\n    atexit.register(file_manager.close)\n    ssl_dhparams_src_ref = importlib.resources.files(\"certbot\") / \"ssl-dhparams.pem\"\n    return str(file_manager.enter_context(importlib.resources.as_file(ssl_dhparams_src_ref)))\n\nSSL_DHPARAMS_SRC = _generate_ssl_dhparams_src_static()\n\"\"\"Path to the nginx ssl_dhparams file found in the Certbot distribution.\"\"\"\n\nUPDATED_SSL_DHPARAMS_DIGEST = \".updated-ssl-dhparams-pem-digest.txt\"\n\"\"\"Name of the hash of the updated or informed ssl_dhparams as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nALL_SSL_DHPARAMS_HASHES = [\n    '9ba6429597aeed2d8617a7705b56e96d044f64b07971659382e426675105654b',\n]\n\"\"\"SHA256 hashes of the contents of all versions of SSL_DHPARAMS_SRC\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/display/__init__.py",
    "content": "\"\"\"Certbot display utilities.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/display/completer.py",
    "content": "\"\"\"Provides Tab completion when prompting users for a path.\"\"\"\nimport glob\nfrom types import TracebackType\nfrom typing import Callable\nfrom typing import Iterator\nfrom typing import Literal\nfrom typing import Optional\n\n# readline module is not available on all systems\ntry:\n    import readline\nexcept ImportError:\n    import certbot._internal.display.dummy_readline as readline  # type: ignore\n\n\nclass Completer:\n    \"\"\"Provides Tab completion when prompting users for a path.\n\n    This class is meant to be used with readline to provide Tab\n    completion for users entering paths. The complete method can be\n    passed to readline.set_completer directly, however, this function\n    works best as a context manager. For example:\n\n    with Completer():\n        raw_input()\n\n    In this example, Tab completion will be available during the call to\n    raw_input above, however, readline will be restored to its previous\n    state when exiting the body of the with statement.\n\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._iter: Iterator[str]\n        self._original_completer: Optional[Callable]\n        self._original_delims: str\n\n    def complete(self, text: str, state: int) -> Optional[str]:\n        \"\"\"Provides path completion for use with readline.\n\n        :param str text: text to offer completions for\n        :param int state: which completion to return\n\n        :returns: possible completion for text or ``None`` if all\n            completions have been returned\n        :rtype: str\n\n        \"\"\"\n        if state == 0:\n            self._iter = glob.iglob(text + '*')\n        return next(self._iter, None)\n\n    def __enter__(self) -> None:\n        self._original_completer = readline.get_completer()\n        self._original_delims = readline.get_completer_delims()\n\n        readline.set_completer(self.complete)\n        readline.set_completer_delims(' \\t\\n;')\n\n        # readline can be implemented using GNU readline, pyreadline or libedit\n        # which have different configuration syntax\n        if readline.__doc__ is not None and 'libedit' in readline.__doc__:\n            readline.parse_and_bind('bind ^I rl_complete')\n        else:\n            readline.parse_and_bind('tab: complete')\n\n    def __exit__(self, unused_type: Optional[type[BaseException]],\n                 unused_value: Optional[BaseException],\n                 unused_traceback: Optional[TracebackType]) -> 'Literal[False]':\n        readline.set_completer_delims(self._original_delims)\n        readline.set_completer(self._original_completer)\n        return False\n"
  },
  {
    "path": "certbot/src/certbot/_internal/display/dummy_readline.py",
    "content": "\"\"\"A dummy module with no effect for use on systems without readline.\"\"\"\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Optional\n\n\ndef get_completer() -> Optional[Callable[[], str]]:\n    \"\"\"An empty implementation of readline.get_completer.\"\"\"\n\n\ndef get_completer_delims() -> list[str]:\n    \"\"\"An empty implementation of readline.get_completer_delims.\"\"\"\n    return []\n\n\ndef parse_and_bind(unused_command: str) -> None:\n    \"\"\"An empty implementation of readline.parse_and_bind.\"\"\"\n\n\ndef set_completer(unused_function: Optional[Callable[[], str]] = None) -> None:\n    \"\"\"An empty implementation of readline.set_completer.\"\"\"\n\n\ndef set_completer_delims(unused_delims: Iterable[str]) -> None:\n    \"\"\"An empty implementation of readline.set_completer_delims.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/display/obj.py",
    "content": "\"\"\"This modules define the actual display implementations used in Certbot\"\"\"\nimport logging\nimport sys\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TextIO\nfrom typing import TypeVar\nfrom typing import Union\n\nfrom certbot import errors\nfrom certbot._internal import constants\nfrom certbot._internal.display import completer\nfrom certbot._internal.display import util\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n# Display exit codes\nOK = \"ok\"\n\"\"\"Display exit code indicating user acceptance.\"\"\"\n\nCANCEL = \"cancel\"\n\"\"\"Display exit code for a user canceling the display.\"\"\"\n\n# Display constants\nSIDE_FRAME = (\"- \" * 39) + \"-\"\n\"\"\"Display boundary (alternates spaces, so when copy-pasted, markdown doesn't interpret\nit as a heading)\"\"\"\n\n\n# This class holds the global state of the display service. Using this class\n# eliminates potential gotchas that exist if self.display was just a global\n# variable. In particular, in functions `_DISPLAY = <value>` would create a\n# local variable unless the programmer remembered to use the `global` keyword.\n# Adding a level of indirection causes the lookup of the global _DisplayService\n# object to happen first avoiding this potential bug.\nclass _DisplayService:\n    def __init__(self) -> None:\n        self.display: Optional[Union[FileDisplay, NoninteractiveDisplay]] = None\n\n\n_SERVICE = _DisplayService()\n\nT = TypeVar(\"T\")\n\n\nclass FileDisplay:\n    \"\"\"File-based display.\"\"\"\n    # see https://github.com/certbot/certbot/issues/3915\n\n    def __init__(self, outfile: TextIO, force_interactive: bool) -> None:\n        super().__init__()\n        self.outfile = outfile\n        self.force_interactive = force_interactive\n        self.skipped_interaction = False\n\n    def notification(self, message: str, pause: bool = True, wrap: bool = True,\n                     force_interactive: bool = False, decorate: bool = True) -> None:\n        \"\"\"Displays a notification and waits for user acceptance.\n\n        :param str message: Message to display\n        :param bool pause: Whether or not the program should pause for the\n            user's confirmation\n        :param bool wrap: Whether or not the application should wrap text\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n        :param bool decorate: Whether to surround the message with a\n            decorated frame\n\n        \"\"\"\n        if wrap:\n            message = util.wrap_lines(message)\n\n        logger.debug(\"Notifying user: %s\", message)\n\n        self.outfile.write(\n            ((\"{line}{frame}{line}\" if decorate else \"\") +\n             \"{msg}{line}\" +\n             (\"{frame}{line}\" if decorate else \"\"))\n                .format(line=os.linesep, frame=SIDE_FRAME, msg=message)\n        )\n        self.outfile.flush()\n\n        if pause:\n            if self._can_interact(force_interactive):\n                util.input_with_timeout(\"Press Enter to Continue\")\n            else:\n                logger.debug(\"Not pausing for user confirmation\")\n\n    def menu(self, message: str, choices: Union[list[tuple[str, str]], list[str]],\n             ok_label: Optional[str] = None, cancel_label: Optional[str] = None,  # pylint: disable=unused-argument\n             help_label: Optional[str] = None, default: Optional[int] = None,  # pylint: disable=unused-argument\n             cli_flag: Optional[str] = None, force_interactive: bool = False,\n             **unused_kwargs: Any) -> tuple[str, int]:\n        \"\"\"Display a menu.\n\n        .. todo:: This doesn't enable the help label/button (I wasn't sold on\n           any interface I came up with for this). It would be a nice feature\n\n        :param str message: title of menu\n        :param choices: Menu lines, len must be > 0\n        :type choices: list of tuples (tag, item) or\n            list of descriptions (tags will be enumerated)\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n\n        :returns: tuple of (`code`, `index`) where\n            `code` - str display exit code\n            `index` - int index of the user's selection\n\n        :rtype: tuple\n\n        \"\"\"\n        return_default = self._return_default(message, default, cli_flag, force_interactive)\n        if return_default is not None:\n            return OK, return_default\n\n        self._print_menu(message, choices)\n\n        code, selection = self._get_valid_int_ans(len(choices))\n\n        return code, selection - 1\n\n    def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,\n              force_interactive: bool = False, **unused_kwargs: Any) -> tuple[str, str]:\n        \"\"\"Accept input from the user.\n\n        :param str message: message to display to the user\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n\n        :returns: tuple of (`code`, `input`) where\n            `code` - str display exit code\n            `input` - str of the user's input\n        :rtype: tuple\n\n        \"\"\"\n        return_default = self._return_default(message, default, cli_flag, force_interactive)\n        if return_default is not None:\n            return OK, return_default\n\n        # Trailing space must be added outside of util.wrap_lines to\n        # be preserved\n        message = util.wrap_lines(\"%s (Enter 'c' to cancel):\" % message) + \" \"\n        ans = util.input_with_timeout(message)\n\n        if ans in (\"c\", \"C\"):\n            return CANCEL, \"-1\"\n        return OK, ans\n\n    def yesno(self, message: str, yes_label: str = \"Yes\", no_label: str = \"No\",\n              default: Optional[bool] = None, cli_flag: Optional[str] = None,\n              force_interactive: bool = False, **unused_kwargs: Any) -> bool:\n        \"\"\"Query the user with a yes/no question.\n\n        Yes and No label must begin with different letters, and must contain at\n        least one letter each.\n\n        :param str message: question for the user\n        :param str yes_label: Label of the \"Yes\" parameter\n        :param str no_label: Label of the \"No\" parameter\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n\n        :returns: True for \"Yes\", False for \"No\"\n        :rtype: bool\n\n        \"\"\"\n        return_default = self._return_default(message, default, cli_flag, force_interactive)\n        if return_default is not None:\n            return return_default\n\n        message = util.wrap_lines(message)\n\n        self.outfile.write(\"{0}{frame}{msg}{0}{frame}\".format(\n            os.linesep, frame=SIDE_FRAME + os.linesep, msg=message))\n        self.outfile.flush()\n\n        while True:\n            ans = util.input_with_timeout(\"{yes}/{no}: \".format(\n                yes=util.parens_around_char(yes_label),\n                no=util.parens_around_char(no_label)))\n\n            # Couldn't get pylint indentation right with elif\n            # elif doesn't matter in this situation\n            if (ans.startswith(yes_label[0].lower()) or\n                ans.startswith(yes_label[0].upper())):\n                return True\n            if (ans.startswith(no_label[0].lower()) or\n                ans.startswith(no_label[0].upper())):\n                return False\n\n    def checklist(self, message: str, tags: list[str], default: Optional[list[str]] = None,\n                  cli_flag: Optional[str] = None, force_interactive: bool = False,\n                  **unused_kwargs: Any) -> tuple[str, list[str]]:\n        \"\"\"Display a checklist.\n\n        :param str message: Message to display to user\n        :param list tags: `str` tags to select, len(tags) > 0\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n\n        :returns: tuple of (`code`, `tags`) where\n            `code` - str display exit code\n            `tags` - list of selected tags\n        :rtype: tuple\n\n        \"\"\"\n        return_default = self._return_default(message, default, cli_flag, force_interactive)\n        if return_default is not None:\n            return OK, return_default\n\n        while True:\n            self._print_menu(message, tags)\n\n            code, ans = self.input(\"Select the appropriate numbers separated \"\n                                   \"by commas and/or spaces, or leave input \"\n                                   \"blank to select all options shown\",\n                                   force_interactive=True)\n\n            if code == OK:\n                if not ans.strip():\n                    ans = \" \".join(str(x) for x in range(1, len(tags)+1))\n                indices = util.separate_list_input(ans)\n                selected_tags = self._scrub_checklist_input(indices, tags)\n                if selected_tags:\n                    return code, selected_tags\n                self.outfile.write(\n                    \"** Error - Invalid selection **%s\" % os.linesep)\n                self.outfile.flush()\n            else:\n                return code, []\n\n    def _return_default(self, prompt: str, default: Optional[T],\n                        cli_flag: Optional[str], force_interactive: bool) -> Optional[T]:\n        \"\"\"Should we return the default instead of prompting the user?\n\n        :param str prompt: prompt for the user\n        :param T default: default answer to prompt\n        :param str cli_flag: command line option for setting an answer\n            to this question\n        :param bool force_interactive: if interactivity is forced\n\n        :returns: The default value if we should return it else `None`\n        :rtype: T or `None`\n\n        \"\"\"\n        # assert_valid_call(prompt, default, cli_flag, force_interactive)\n        if self._can_interact(force_interactive):\n            return None\n        if default is None:\n            msg = \"Unable to get an answer for the question:\\n{0}\".format(prompt)\n            if cli_flag:\n                msg += (\n                    \"\\nYou can provide an answer on the \"\n                    \"command line with the {0} flag.\".format(cli_flag))\n            raise errors.Error(msg)\n        logger.debug(\n            \"Falling back to default %s for the prompt:\\n%s\",\n            default, prompt)\n        return default\n\n    def _can_interact(self, force_interactive: bool) -> bool:\n        \"\"\"Can we safely interact with the user?\n\n        :param bool force_interactive: if interactivity is forced\n\n        :returns: True if the display can interact with the user\n        :rtype: bool\n\n        \"\"\"\n        if (self.force_interactive or force_interactive or\n            sys.stdin.isatty() and self.outfile.isatty()):\n            return True\n        if not self.skipped_interaction:\n            logger.warning(\n                \"Skipped user interaction because Certbot doesn't appear to \"\n                \"be running in a terminal. You should probably include \"\n                \"--non-interactive or %s on the command line.\",\n                constants.FORCE_INTERACTIVE_FLAG)\n            self.skipped_interaction = True\n        return False\n\n    def directory_select(self, message: str, default: Optional[str] = None,\n                         cli_flag: Optional[str] = None, force_interactive: bool = False,\n                         **unused_kwargs: Any) -> tuple[str, str]:\n        \"\"\"Display a directory selection screen.\n\n        :param str message: prompt to give the user\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n        :param bool force_interactive: True if it's safe to prompt the user\n            because it won't cause any workflow regressions\n\n        :returns: tuple of the form (`code`, `string`) where\n            `code` - display exit code\n            `string` - input entered by the user\n\n        \"\"\"\n        with completer.Completer():\n            return self.input(message, default, cli_flag, force_interactive)\n\n    def _scrub_checklist_input(self, indices: Iterable[Union[str, int]],\n                               tags: list[str]) -> list[str]:\n        \"\"\"Validate input and transform indices to appropriate tags.\n\n        :param list indices: input\n        :param list tags: Original tags of the checklist\n\n        :returns: valid tags the user selected\n        :rtype: :class:`list` of :class:`str`\n\n        \"\"\"\n        # They should all be of type int\n        try:\n            indices_int = [int(index) for index in indices]\n        except ValueError:\n            return []\n\n        # Remove duplicates. dict is used instead of set, since dict preserves\n        # insertion order as of Python 3.7\n        indices_int = list(dict.fromkeys(indices_int).keys())\n\n        # Check all input is within range\n        for index in indices_int:\n            if index < 1 or index > len(tags):\n                return []\n        # Transform indices_int to appropriate tags\n        return [tags[index - 1] for index in indices_int]\n\n    def _print_menu(self, message: str,\n                    choices: Union[list[tuple[str, str]], list[str]]) -> None:\n        \"\"\"Print a menu on the screen.\n\n        :param str message: title of menu\n        :param choices: Menu lines\n        :type choices: list of tuples (tag, item) or\n            list of descriptions (tags will be enumerated)\n\n        \"\"\"\n        # Can take either tuples or single items in choices list\n        if choices and isinstance(choices[0], tuple):\n            choices = [f\"{c[0]} - {c[1]}\" for c in choices]\n\n        # Write out the message to the user\n        self.outfile.write(f\"{os.linesep}{message}{os.linesep}\")\n        self.outfile.write(SIDE_FRAME + os.linesep)\n\n        # Write out the menu choices\n        for i, desc in enumerate(choices, 1):\n            msg = f\"{i}: {desc}\"\n            self.outfile.write(util.wrap_lines(msg))\n\n            # Keep this outside of the textwrap\n            self.outfile.write(os.linesep)\n\n        self.outfile.write(SIDE_FRAME + os.linesep)\n        self.outfile.flush()\n\n    def _get_valid_int_ans(self, max_: int) -> tuple[str, int]:\n        \"\"\"Get a numerical selection.\n\n        :param int max: The maximum entry (len of choices), must be positive\n\n        :returns: tuple of the form (`code`, `selection`) where\n            `code` - str display exit code ('ok' or cancel')\n            `selection` - int user's selection\n        :rtype: tuple\n\n        \"\"\"\n        selection = -1\n        if max_ > 1:\n            input_msg = (\"Select the appropriate number \"\n                         \"[1-{max_}] then [enter] (press 'c' to \"\n                         \"cancel): \".format(max_=max_))\n        else:\n            input_msg = (\"Press 1 [enter] to confirm the selection \"\n                         \"(press 'c' to cancel): \")\n        while selection < 1:\n            ans = util.input_with_timeout(input_msg)\n            if ans.startswith(\"c\") or ans.startswith(\"C\"):\n                return CANCEL, -1\n            try:\n                selection = int(ans)\n                if selection < 1 or selection > max_:\n                    selection = -1\n                    raise ValueError\n\n            except ValueError:\n                self.outfile.write(\n                    \"{0}** Invalid input **{0}\".format(os.linesep))\n                self.outfile.flush()\n\n        return OK, selection\n\n\nclass NoninteractiveDisplay:\n    \"\"\"A display utility implementation that never asks for interactive user input\"\"\"\n\n    def __init__(self, outfile: TextIO, *unused_args: Any, **unused_kwargs: Any) -> None:\n        super().__init__()\n        self.outfile = outfile\n\n    def _interaction_fail(self, message: str, cli_flag: Optional[str],\n                          extra: str = \"\") -> errors.MissingCommandlineFlag:\n        \"\"\"Return error to raise in case of an attempt to interact in noninteractive mode\"\"\"\n        msg = \"Missing command line flag or config entry for this setting:\\n\"\n        msg += message\n        if extra:\n            msg += \"\\n\" + extra\n        if cli_flag:\n            msg += \"\\n\\n(You can set this with the {0} flag)\".format(cli_flag)\n        return errors.MissingCommandlineFlag(msg)\n\n    def notification(self, message: str, pause: bool = False, wrap: bool = True,  # pylint: disable=unused-argument\n                     decorate: bool = True, **unused_kwargs: Any) -> None:\n        \"\"\"Displays a notification without waiting for user acceptance.\n\n        :param str message: Message to display to stdout\n        :param bool pause: The NoninteractiveDisplay waits for no keyboard\n        :param bool wrap: Whether or not the application should wrap text\n        :param bool decorate: Whether to apply a decorated frame to the message\n\n        \"\"\"\n        if wrap:\n            message = util.wrap_lines(message)\n\n        logger.debug(\"Notifying user: %s\", message)\n\n        self.outfile.write(\n            ((\"{line}{frame}{line}\" if decorate else \"\") +\n             \"{msg}{line}\" +\n             (\"{frame}{line}\" if decorate else \"\"))\n                .format(line=os.linesep, frame=SIDE_FRAME, msg=message)\n        )\n        self.outfile.flush()\n\n    def menu(self, message: str, choices: Union[list[tuple[str, str]], list[str]],\n             ok_label: Optional[str] = None, cancel_label: Optional[str] = None,\n             help_label: Optional[str] = None, default: Optional[int] = None,\n             cli_flag: Optional[str] = None, **unused_kwargs: Any) -> tuple[str, int]:\n        # pylint: disable=unused-argument\n        \"\"\"Avoid displaying a menu.\n\n        :param str message: title of menu\n        :param choices: Menu lines, len must be > 0\n        :type choices: list of tuples (tag, item) or\n            list of descriptions (tags will be enumerated)\n        :param int default: the default choice\n        :param dict kwargs: absorbs various irrelevant labelling arguments\n\n        :returns: tuple of (`code`, `index`) where\n            `code` - str display exit code\n            `index` - int index of the user's selection\n        :rtype: tuple\n        :raises errors.MissingCommandlineFlag: if there was no default\n\n        \"\"\"\n        if default is None:\n            raise self._interaction_fail(message, cli_flag, \"Choices: \" + repr(choices))\n\n        return OK, default\n\n    def input(self, message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,\n              **unused_kwargs: Any) -> tuple[str, str]:\n        \"\"\"Accept input from the user.\n\n        :param str message: message to display to the user\n\n        :returns: tuple of (`code`, `input`) where\n            `code` - str display exit code\n            `input` - str of the user's input\n        :rtype: tuple\n        :raises errors.MissingCommandlineFlag: if there was no default\n\n        \"\"\"\n        if default is None:\n            raise self._interaction_fail(message, cli_flag)\n        return OK, default\n\n    def yesno(self, message: str, yes_label: Optional[str] = None, no_label: Optional[str] = None,  # pylint: disable=unused-argument\n              default: Optional[bool] = None, cli_flag: Optional[str] = None,\n              **unused_kwargs: Any) -> bool:\n        \"\"\"Decide Yes or No, without asking anybody\n\n        :param str message: question for the user\n        :param dict kwargs: absorbs yes_label, no_label\n\n        :raises errors.MissingCommandlineFlag: if there was no default\n        :returns: True for \"Yes\", False for \"No\"\n        :rtype: bool\n\n        \"\"\"\n        if default is None:\n            raise self._interaction_fail(message, cli_flag)\n        return default\n\n    def checklist(self, message: str, tags: Iterable[str], default: Optional[list[str]] = None,\n                  cli_flag: Optional[str] = None, **unused_kwargs: Any) -> tuple[str, list[str]]:\n        \"\"\"Display a checklist.\n\n        :param str message: Message to display to user\n        :param list tags: `str` tags to select, len(tags) > 0\n        :param dict kwargs: absorbs default_status arg\n\n        :returns: tuple of (`code`, `tags`) where\n            `code` - str display exit code\n            `tags` - list of selected tags\n        :rtype: tuple\n\n        \"\"\"\n        if default is None:\n            raise self._interaction_fail(message, cli_flag, \"? \".join(tags) + \"?\")\n        return OK, default\n\n    def directory_select(self, message: str, default: Optional[str] = None,\n                         cli_flag: Optional[str] = None, **unused_kwargs: Any) -> tuple[str, str]:\n        \"\"\"Simulate prompting the user for a directory.\n\n        This function returns default if it is not ``None``, otherwise,\n        an exception is raised explaining the problem. If cli_flag is\n        not ``None``, the error message will include the flag that can\n        be used to set this value with the CLI.\n\n        :param str message: prompt to give the user\n        :param default: default value to return (if one exists)\n        :param str cli_flag: option used to set this value with the CLI\n\n        :returns: tuple of the form (`code`, `string`) where\n            `code` - int display exit code\n            `string` - input entered by the user\n\n        \"\"\"\n        return self.input(message, default, cli_flag)\n\n\ndef get_display() -> Union[FileDisplay, NoninteractiveDisplay]:\n    \"\"\"Get the display utility.\n\n    :return: the display utility\n    :rtype: Union[FileDisplay, NoninteractiveDisplay]\n    :raise: ValueError if the display utility is not configured yet.\n\n    \"\"\"\n    if not _SERVICE.display:\n        raise ValueError(\"This function was called too early in Certbot's execution \"\n                         \"as the display utility hasn't been configured yet.\")\n    return _SERVICE.display\n\n\ndef set_display(display: Union[FileDisplay, NoninteractiveDisplay]) -> None:\n    \"\"\"Set the display service.\n\n    :param Union[FileDisplay, NoninteractiveDisplay] display: the display service\n\n    \"\"\"\n    _SERVICE.display = display\n"
  },
  {
    "path": "certbot/src/certbot/_internal/display/util.py",
    "content": "\"\"\"Internal Certbot display utilities.\"\"\"\nimport sys\nimport textwrap\nfrom typing import Optional\n\nfrom acme import messages as acme_messages\nfrom certbot._internal import san\nfrom certbot.compat import misc\n\n\ndef wrap_lines(msg: str) -> str:\n    \"\"\"Format lines nicely to 80 chars.\n\n    :param str msg: Original message\n\n    :returns: Formatted message respecting newlines in message\n    :rtype: str\n\n    \"\"\"\n    lines = msg.splitlines()\n    fixed_l = []\n\n    for line in lines:\n        fixed_l.append(textwrap.fill(\n            line,\n            80,\n            break_long_words=False,\n            break_on_hyphens=False))\n\n    return '\\n'.join(fixed_l)\n\n\ndef parens_around_char(label: str) -> str:\n    \"\"\"Place parens around first character of label.\n\n    :param str label: Must contain at least one character\n\n    \"\"\"\n    return \"({first}){rest}\".format(first=label[0], rest=label[1:])\n\n\ndef input_with_timeout(prompt: Optional[str] = None, timeout: float = 36000.0) -> str:\n    \"\"\"Get user input with a timeout.\n\n    Behaves the same as the builtin input, however, an error is raised if\n    a user doesn't answer after timeout seconds. The default timeout\n    value was chosen to place it just under 12 hours for users following\n    our advice and running Certbot twice a day.\n\n    :param str prompt: prompt to provide for input\n    :param float timeout: maximum number of seconds to wait for input\n\n    :returns: user response\n    :rtype: str\n\n    :raises errors.Error if no answer is given before the timeout\n\n    \"\"\"\n    # use of sys.stdin and sys.stdout to mimic the builtin input based on\n    # https://github.com/python/cpython/blob/baf7bb30a02aabde260143136bdf5b3738a1d409/Lib/getpass.py#L129\n    if prompt:\n        sys.stdout.write(prompt)\n        sys.stdout.flush()\n\n    line = misc.readline_with_timeout(timeout, prompt)\n\n    if not line:\n        raise EOFError\n    return line.rstrip('\\n')\n\n\ndef separate_list_input(input_: str) -> list[str]:\n    \"\"\"Separate a comma or space separated list.\n\n    :param str input_: input from the user\n\n    :returns: strings\n    :rtype: list\n\n    \"\"\"\n    no_commas = input_.replace(\",\", \" \")\n    # Each string is naturally unicode, this causes problems with M2Crypto SANs\n    # TODO: check if above is still true when M2Crypto is gone ^\n    return [str(string) for string in no_commas.split()]\n\n\ndef summarize_sans(sans: list[san.SAN]) -> str:\n    \"\"\"Summarizes a list of identifiers in the format of:\n        example.com.com and N more\n    or if there are only two identifiers:\n        example.com and 192.0.2.77\n    or if there is only one identifier:\n        example.com\n\n    :param list sans: `san.SAN` list of domains and/or IP addresses\n    :returns: a summary\n    :rtype: str\n    \"\"\"\n    if not sans:\n        return \"\"\n\n    length = len(sans)\n    if length == 1:\n        return str(sans[0])\n    elif length == 2:\n        return f\"{sans[0]} and {sans[1]}\"\n    else:\n        return f\"{sans[0]} and {length - 1} more\"\n\n\ndef describe_acme_error(error: acme_messages.Error) -> str:\n    \"\"\"Returns a human-readable description of an RFC7807 error.\n\n    :param error: The ACME error\n    :returns: a string describing the error, suitable for human consumption.\n    :rtype: str\n    \"\"\"\n    parts = (error.title, error.detail)\n    if any(parts):\n        return ' :: '.join(part for part in parts if part is not None)\n    if error.description:\n        return error.description\n    return error.typ\n"
  },
  {
    "path": "certbot/src/certbot/_internal/eff.py",
    "content": "\"\"\"Subscribes users to the EFF newsletter.\"\"\"\nimport logging\nfrom typing import Optional\n\nimport requests\n\nfrom certbot import configuration\nfrom certbot._internal import constants\nfrom certbot._internal.account import Account\nfrom certbot._internal.account import AccountFileStorage\nfrom certbot.display import util as display_util\n\nlogger = logging.getLogger(__name__)\n\n\ndef prepare_subscription(config: configuration.NamespaceConfig, acc: Account) -> None:\n    \"\"\"High level function to store potential EFF newsletter subscriptions.\n\n    The user may be asked if they want to sign up for the newsletter if\n    they have not given their explicit approval or refusal using --eff-mail\n    or --no-eff-mail flag.\n\n    Decision about EFF subscription will be stored in the account metadata.\n\n    :param configuration.NamespaceConfig config: Client configuration.\n    :param Account acc: Current client account.\n\n    \"\"\"\n    if config.eff_email is False:\n        return\n    if config.eff_email is True:\n        if config.email is None:\n            _report_failure(\"you didn't provide an e-mail address\")\n        else:\n            acc.meta = acc.meta.update(register_to_eff=config.email)\n    elif config.email and _want_subscription():\n        acc.meta = acc.meta.update(register_to_eff=config.email)\n\n    if acc.meta.register_to_eff:\n        storage = AccountFileStorage(config)\n        storage.update_meta(acc)\n\n\ndef handle_subscription(config: configuration.NamespaceConfig, acc: Optional[Account]) -> None:\n    \"\"\"High level function to take care of EFF newsletter subscriptions.\n\n    Once subscription is handled, it will not be handled again.\n\n    :param configuration.NamespaceConfig config: Client configuration.\n    :param Account acc: Current client account.\n\n    \"\"\"\n    if config.dry_run or not acc:\n        return\n    if acc.meta.register_to_eff:\n        subscribe(acc.meta.register_to_eff)\n\n        acc.meta = acc.meta.update(register_to_eff=None)\n        storage = AccountFileStorage(config)\n        storage.update_meta(acc)\n\n\ndef _want_subscription() -> bool:\n    \"\"\"Does the user want to be subscribed to the EFF newsletter?\n\n    :returns: True if we should subscribe the user, otherwise, False\n    :rtype: bool\n\n    \"\"\"\n    prompt = (\n        'Would you be willing, once your first certificate is successfully issued, '\n        'to share your email address with the Electronic Frontier Foundation, a '\n        \"founding partner of the Let's Encrypt project and the non-profit organization \"\n        \"that develops Certbot? We'd like to send you email about our work encrypting \"\n        \"the web, EFF news, campaigns, and ways to support digital freedom. \")\n    return display_util.yesno(prompt, default=False)\n\n\ndef subscribe(email: str) -> None:\n    \"\"\"Subscribe the user to the EFF mailing list.\n\n    :param str email: the e-mail address to subscribe\n\n    \"\"\"\n    url = constants.EFF_SUBSCRIBE_URI\n    data = {'data_type': 'json',\n            'email': email,\n            'form_id': 'eff_supporters_library_subscribe_form'}\n    logger.info('Subscribe to the EFF mailing list (email: %s).', email)\n    logger.debug('Sending POST request to %s:\\n%s', url, data)\n    _check_response(requests.post(url, data=data, timeout=60))\n\n\ndef _check_response(response: requests.Response) -> None:\n    \"\"\"Check for errors in the server's response.\n\n    If an error occurred, it will be reported to the user.\n\n    :param requests.Response response: the server's response to the\n        subscription request\n\n    \"\"\"\n    logger.debug('Received response:\\n%s', response.content)\n    try:\n        response.raise_for_status()\n        if not response.json()['status']:\n            _report_failure('your e-mail address appears to be invalid')\n    except requests.exceptions.HTTPError:\n        _report_failure()\n    except (ValueError, KeyError):\n        _report_failure('there was a problem with the server response')\n\n\ndef _report_failure(reason: Optional[str] = None) -> None:\n    \"\"\"Notify the user of failing to sign them up for the newsletter.\n\n    :param reason: a phrase describing what the problem was\n        beginning with a lowercase letter and no closing punctuation\n    :type reason: `str` or `None`\n\n    \"\"\"\n    msg = ['We were unable to subscribe you the EFF mailing list']\n    if reason is not None:\n        msg.append(' because ')\n        msg.append(reason)\n    msg.append('. You can try again later by visiting https://act.eff.org.')\n    display_util.notify(''.join(msg))\n"
  },
  {
    "path": "certbot/src/certbot/_internal/error_handler.py",
    "content": "\"\"\"Registers functions to be called if an exception or signal occurs.\"\"\"\nimport functools\nimport logging\nimport signal\nimport traceback\nfrom types import TracebackType\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot import errors\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n\n# _SIGNALS stores the signals that will be handled by the ErrorHandler. These\n# signals were chosen as their default handler terminates the process and could\n# potentially occur from inside Python. Signals such as SIGILL were not\n# included as they could be a sign of something devious and we should terminate\n# immediately.\nif os.name != \"nt\":\n    _SIGNALS = [signal.SIGTERM]\n    for signal_code in [signal.SIGHUP, signal.SIGQUIT,\n                        signal.SIGXCPU, signal.SIGXFSZ]:\n        # Adding only those signals that their default action is not Ignore.\n        # This is platform-dependent, so we check it dynamically.\n        if signal.getsignal(signal_code) != signal.SIG_IGN:\n            _SIGNALS.append(signal_code)\nelse:\n    # POSIX signals are not implemented natively in Windows, but emulated from the C runtime.\n    # As consumed by CPython, most of handlers on these signals are useless, in particular\n    # SIGTERM: for instance, os.kill(pid, signal.SIGTERM) will call TerminateProcess, that stops\n    # immediately the process without calling the attached handler. Besides, non-POSIX signals\n    # (CTRL_C_EVENT and CTRL_BREAK_EVENT) are implemented in a console context to handle the\n    # CTRL+C event to a process launched from the console. Only CTRL_C_EVENT has a reliable\n    # behavior in fact, and maps to the handler to SIGINT. However in this case, a\n    # KeyboardInterrupt is raised, that will be handled by ErrorHandler through the context manager\n    # protocol. Finally, no signal on Windows is electable to be handled using ErrorHandler.\n    #\n    # Refs: https://stackoverflow.com/a/35792192, https://maruel.ca/post/python_windows_signal,\n    # https://docs.python.org/2/library/os.html#os.kill,\n    # https://www.reddit.com/r/Python/comments/1dsblt/windows_command_line_automation_ctrlc_question\n    _SIGNALS = []\n\n\nclass ErrorHandler:\n    \"\"\"Context manager for running code that must be cleaned up on failure.\n\n    The context manager allows you to register functions that will be called\n    when an exception (excluding SystemExit) or signal is encountered.\n    Usage::\n\n        handler = ErrorHandler(cleanup1_func, *cleanup1_args, **cleanup1_kwargs)\n        handler.register(cleanup2_func, *cleanup2_args, **cleanup2_kwargs)\n\n        with handler:\n            do_something()\n\n    Or for one cleanup function::\n\n        with ErrorHandler(func, args, kwargs):\n            do_something()\n\n    If an exception is raised out of do_something, the cleanup functions will\n    be called in last in first out order. Then the exception is raised.\n    Similarly, if a signal is encountered, the cleanup functions are called\n    followed by the previously received signal handler.\n\n    Each registered cleanup function is called exactly once. If a registered\n    function raises an exception, it is logged and the next function is called.\n    Signals received while the registered functions are executing are\n    deferred until they finish.\n\n    \"\"\"\n    def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:\n        self.call_on_regular_exit = False\n        self.body_executed = False\n        self.funcs: list[Callable[[], Any]] = []\n        self.prev_handlers: dict[int, Union[int, None, Callable]] = {}\n        self.received_signals: list[int] = []\n        if func is not None:\n            self.register(func, *args, **kwargs)\n\n    def __enter__(self) -> None:\n        self.body_executed = False\n        self._set_signal_handlers()\n\n    def __exit__(self, exec_type: Optional[type[BaseException]],\n                 exec_value: Optional[BaseException],\n                 trace: Optional[TracebackType]) -> bool:\n        self.body_executed = True\n        retval = False\n        # SystemExit is ignored to properly handle forks that don't exec\n        if exec_type is SystemExit:\n            return retval\n        if exec_type is None:\n            if not self.call_on_regular_exit:\n                return retval\n        elif exec_type is errors.SignalExit:\n            logger.debug(\"Encountered signals: %s\", self.received_signals)\n            retval = True\n        else:\n            logger.debug(\"Encountered exception:\\n%s\", \"\".join(\n                traceback.format_exception(exec_type, exec_value, trace)))\n\n        self._call_registered()\n        self._reset_signal_handlers()\n        self._call_signals()\n        return retval\n\n    def register(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:\n        \"\"\"Sets func to be run with the given arguments during cleanup.\n\n        :param function func: function to be called in case of an error\n\n        \"\"\"\n        self.funcs.append(functools.partial(func, *args, **kwargs))\n\n    def _call_registered(self) -> None:\n        \"\"\"Calls all registered functions\"\"\"\n        logger.debug(\"Calling registered functions\")\n        while self.funcs:\n            try:\n                self.funcs[-1]()\n            except Exception as exc:  # pylint: disable=broad-except\n                output = traceback.format_exception_only(type(exc), exc)\n                logger.error(\"Encountered exception during recovery: %s\",\n                             ''.join(output).rstrip())\n            self.funcs.pop()\n\n    def _set_signal_handlers(self) -> None:\n        \"\"\"Sets signal handlers for signals in _SIGNALS.\"\"\"\n        for signum in _SIGNALS:\n            prev_handler = signal.getsignal(signum)\n            # If prev_handler is None, the handler was set outside of Python\n            if prev_handler is not None:\n                self.prev_handlers[signum] = prev_handler\n                signal.signal(signum, self._signal_handler)\n\n    def _reset_signal_handlers(self) -> None:\n        \"\"\"Resets signal handlers for signals in _SIGNALS.\"\"\"\n        for signum, handler in self.prev_handlers.items():\n            signal.signal(signum, handler)\n        self.prev_handlers.clear()\n\n    def _signal_handler(self, signum: int, unused_frame: Any) -> None:\n        \"\"\"Replacement function for handling received signals.\n\n        Store the received signal. If we are executing the code block in\n        the body of the context manager, stop by raising signal exit.\n\n        :param int signum: number of current signal\n\n        \"\"\"\n        self.received_signals.append(signum)\n        if not self.body_executed:\n            raise errors.SignalExit\n\n    def _call_signals(self) -> None:\n        \"\"\"Finally call the deferred signals.\"\"\"\n        for signum in self.received_signals:\n            logger.debug(\"Calling signal %s\", signum)\n            os.kill(os.getpid(), signum)\n\n\nclass ExitHandler(ErrorHandler):\n    \"\"\"Context manager for running code that must be cleaned up.\n\n    Subclass of ErrorHandler, with the same usage and parameters.\n    In addition to cleaning up on all signals, also cleans up on\n    regular exit.\n    \"\"\"\n    def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:\n        super().__init__(func, *args, **kwargs)\n        self.call_on_regular_exit = True\n"
  },
  {
    "path": "certbot/src/certbot/_internal/hooks.py",
    "content": "\"\"\"Facilities for implementing hooks that call shell commands.\"\"\"\n\nimport logging\nfrom typing import Optional\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import san\nfrom certbot.compat import filesystem\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.display import ops as display_ops\nfrom certbot.plugins import util as plug_util\n\nlogger = logging.getLogger(__name__)\n\n\ndef validate_hooks(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Check hook commands are executable.\"\"\"\n    validate_hook(config.pre_hook, \"pre\")\n    validate_hook(config.post_hook, \"post\")\n    validate_hook(config.deploy_hook, \"deploy\")\n\n\ndef _prog(shell_cmd: str) -> Optional[str]:\n    \"\"\"Extract the program run by a shell command.\n\n    :param str shell_cmd: command to be executed\n\n    :returns: basename of command or None if the command isn't found\n    :rtype: str or None\n\n    \"\"\"\n    if not util.exe_exists(shell_cmd):\n        plug_util.path_surgery(shell_cmd)\n        if not util.exe_exists(shell_cmd):\n            return None\n\n    return os.path.basename(shell_cmd)\n\n\ndef validate_hook(shell_cmd: str, hook_name: str) -> None:\n    \"\"\"Check that a command provided as a hook is plausibly executable.\n\n    :raises .errors.HookCommandNotFound: if the command is not found\n    \"\"\"\n    if shell_cmd:\n        cmd = shell_cmd.split(None, 1)[0]\n        if not _prog(cmd):\n            path = os.environ[\"PATH\"]\n            if os.path.exists(cmd):\n                msg = f\"{cmd}-hook command {hook_name} exists, but is not executable.\"\n            else:\n                msg = (\n                    f\"Unable to find {hook_name}-hook command {cmd} in the PATH.\\n(PATH is \"\n                    f\"{path})\\nSee also the --disable-hook-validation option.\"\n                )\n\n            raise errors.HookCommandNotFound(msg)\n\n\ndef pre_hook(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Run pre-hooks if they exist and haven't already been run.\n\n    When Certbot is running with the renew subcommand, this function\n    runs any hooks found in the config.renewal_pre_hooks_dir (if they\n    have not already been run) followed by any pre-hook in the config.\n    If hooks in config.renewal_pre_hooks_dir are run and the pre-hook in\n    the config is a path to one of these scripts, it is not run twice.\n\n    :param configuration.NamespaceConfig config: Certbot settings\n\n    \"\"\"\n    all_hooks: list[str] = (list_hooks(config.renewal_pre_hooks_dir) if config.directory_hooks\n        else [])\n    all_hooks += [config.pre_hook] if config.pre_hook else []\n    for hook in all_hooks:\n        _run_pre_hook_if_necessary(hook)\n\n\nexecuted_pre_hooks: set[str] = set()\n\n\ndef _run_pre_hook_if_necessary(command: str) -> None:\n    \"\"\"Run the specified pre-hook if we haven't already.\n\n    If we've already run this exact command before, a message is logged\n    saying the pre-hook was skipped.\n\n    :param str command: pre-hook to be run\n\n    \"\"\"\n    if command in executed_pre_hooks:\n        logger.info(\"Pre-hook command already run, skipping: %s\", command)\n    else:\n        _run_hook(\"pre-hook\", command)\n        executed_pre_hooks.add(command)\n\n\ndef post_hook(\n    config: configuration.NamespaceConfig,\n    renewed_sans: list[san.SAN]\n) -> None:\n\n    \"\"\"Run post-hooks if defined.\n\n    This function also registers any executables found in\n    config.renewal_post_hooks_dir to be run when Certbot is used with\n    the renew subcommand.\n\n    If the verb is renew, we delay executing any post-hooks until\n    :func:`run_saved_post_hooks` is called. In this case, this function\n    registers all hooks found in config.renewal_post_hooks_dir to be\n    called followed by any post-hook in the config. If the post-hook in\n    the config is a path to an executable in the post-hook directory, it\n    is not scheduled to be run twice.\n\n    :param configuration.NamespaceConfig config: Certbot settings\n\n    \"\"\"\n\n    all_hooks: list[str] = (list_hooks(config.renewal_post_hooks_dir) if config.directory_hooks\n        else [])\n    all_hooks += [config.post_hook] if config.post_hook else []\n    # In the \"renew\" case, we save these up to run at the end\n    if config.verb == \"renew\":\n        for hook in all_hooks:\n            _run_eventually(hook)\n    # certonly / run\n    else:\n        renewed_sans_str = ' '.join(map(str, renewed_sans))\n        # 32k is reasonable on Windows and likely quite conservative on other platforms\n        if len(renewed_sans_str) > 32_000:\n            logger.warning(\"Limiting RENEWED_DOMAINS environment variable to 32k characters\")\n            renewed_sans_str = renewed_sans_str[:32_000]\n        for hook in all_hooks:\n            _run_hook(\n                \"post-hook\",\n                hook,\n                {\n                    'RENEWED_DOMAINS': renewed_sans_str,\n                    # Since other commands stop certbot execution on failure,\n                    # it doesn't make sense to have a FAILED_DOMAINS variable\n                    'FAILED_DOMAINS': \"\"\n                }\n            )\n\n\npost_hooks: list[str] = []\n\n\ndef _run_eventually(command: str) -> None:\n    \"\"\"Registers a post-hook to be run eventually.\n\n    All commands given to this function will be run exactly once in the\n    order they were given when :func:`run_saved_post_hooks` is called.\n\n    :param str command: post-hook to register to be run\n\n    \"\"\"\n    if command not in post_hooks:\n        post_hooks.append(command)\n\n\ndef run_saved_post_hooks(renewed_sans: list[san.SAN], failed_sans: list[san.SAN]) -> None:\n    \"\"\"Run any post hooks that were saved up in the course of the 'renew' verb\"\"\"\n\n    renewed_sans_str = ' '.join(map(str, renewed_sans))\n    failed_sans_str = ' '.join(map(str, failed_sans))\n\n    # 32k combined is reasonable on Windows and likely quite conservative on other platforms\n    if len(renewed_sans_str) > 16_000:\n        logger.warning(\"Limiting RENEWED_DOMAINS environment variable to 16k characters\")\n        renewed_sans_str = renewed_sans_str[:16_000]\n\n    if len(failed_sans_str) > 16_000:\n        logger.warning(\"Limiting FAILED_DOMAINS environment variable to 16k characters\")\n        renewed_sans_str = failed_sans_str[:16_000]\n\n    for cmd in post_hooks:\n        _run_hook(\n            \"post-hook\",\n            cmd,\n            {\n                'RENEWED_DOMAINS': renewed_sans_str,\n                'FAILED_DOMAINS': failed_sans_str\n            }\n        )\n\n\ndef deploy_hook(config: configuration.NamespaceConfig, sans: list[san.SAN],\n                lineage_path: str) -> None:\n    \"\"\"Run post-renewal/post-issuance hooks.\n\n    This function runs any hooks found in\n    config.renewal_deploy_hooks_dir followed by any deploy-hook in the\n    config. If the deploy-hook in the config is a path to a script in\n    config.renewal_deploy_hooks_dir, it is not run twice.\n\n    If Certbot is doing a dry run, no hooks are run and messages are\n    logged saying that they were skipped.\n\n    :param configuration.NamespaceConfig config: Certbot settings\n    :param sans: domains and/or IP addresses in the obtained certificate\n    :type sans: `list` of `san.SAN`\n    :param str lineage_path: live directory path for the new cert\n\n    \"\"\"\n    executed_hooks = set()\n    all_hooks: list[str] = (list_hooks(config.renewal_deploy_hooks_dir) if config.directory_hooks\n        else [])\n    all_hooks += [config.deploy_hook] if config.deploy_hook else []\n    for hook in all_hooks:\n        if hook in executed_hooks:\n            logger.info(\"Skipping deploy-hook '%s' as it was already run.\", hook)\n        else:\n            _run_deploy_hook(hook, sans, lineage_path, config.dry_run, config.run_deploy_hooks)\n            executed_hooks.add(hook)\n\n\ndef _run_deploy_hook(command: str, sans: list[san.SAN], lineage_path: str, dry_run: bool,\n                     run_deploy_hooks: bool) -> None:\n    \"\"\"Run the specified deploy-hook (if not doing a dry run).\n\n    If dry_run is True, command is not run and a message is logged\n    saying that it was skipped. If dry_run is False, the hook is run\n    after setting the appropriate environment variables.\n\n    :param str command: command to run as a deploy-hook\n    :param sans: domains and/or IP addresses in the obtained certificate\n    :type sans: `list` of `san.SAN`\n    :param str lineage_path: live directory path for the new cert\n    :param bool dry_run: True iff Certbot is doing a dry run\n    :param bool run_deploy_hooks: True if deploy hooks should run despite Certbot doing a dry run\n\n    \"\"\"\n    if dry_run and not run_deploy_hooks:\n        logger.info(\"Dry run: skipping deploy hook command: %s\",\n                       command)\n        return\n\n    os.environ[\"RENEWED_DOMAINS\"] = \" \".join(map(str, sans))\n    os.environ[\"RENEWED_LINEAGE\"] = lineage_path\n    _run_hook(\"deploy-hook\", command)\n\n\ndef _run_hook(cmd_name: str, shell_cmd: str, extra_env: Optional[dict[str, str]] = None) -> str:\n    \"\"\"Run a hook command.\n\n    :param str cmd_name: the user facing name of the hook being run\n    :param shell_cmd: shell command to execute\n    :type shell_cmd: `list` of `str` or `str`\n    :param dict extra_env: extra environment variables to set\n    :type extra_env: `dict` of `str` to `str`\n\n    :returns: stderr if there was any\"\"\"\n    env = util.env_no_snap_for_external_calls()\n    env.update(extra_env or {})\n    returncode, err, out = misc.execute_command_status(\n        cmd_name, shell_cmd, env=env)\n    display_ops.report_executed_command(f\"Hook '{cmd_name}'\", returncode, out, err)\n    return err\n\n\ndef list_hooks(dir_path: str) -> list[str]:\n    \"\"\"List paths to all hooks found in dir_path in sorted order.\n\n    :param str dir_path: directory to search\n\n    :returns: `list` of `str`\n    :rtype: sorted list of absolute paths to executables in dir_path\n\n    \"\"\"\n    allpaths = (os.path.join(dir_path, f) for f in os.listdir(dir_path))\n    hooks = [path for path in allpaths if filesystem.is_executable(path) and not path.endswith('~')]\n    return sorted(hooks)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/lock.py",
    "content": "\"\"\"Implements file locks compatible with Linux and Windows for locking files and directories.\"\"\"\nimport errno\nimport logging\nfrom typing import Optional\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\n\ntry:\n    import fcntl\nexcept ImportError:\n    import msvcrt\n    POSIX_MODE = False\nelse:\n    POSIX_MODE = True\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass LockFile:\n    \"\"\"\n    Platform independent file lock system.\n    LockFile accepts a parameter, the path to a file acting as a lock. Once the LockFile,\n    instance is created, the associated file is 'locked from the point of view of the OS,\n    meaning that if another instance of Certbot try at the same time to acquire the same lock,\n    it will raise an Exception. Calling release method will release the lock, and make it\n    available to every other instance.\n    Upon exit, Certbot will also release all the locks.\n    This allows us to protect a file or directory from being concurrently accessed\n    or modified by two Certbot instances.\n    LockFile is platform independent: it will proceed to the appropriate OS lock mechanism\n    depending on Linux or Windows.\n    \"\"\"\n    def __init__(self, path: str) -> None:\n        \"\"\"\n        Create a LockFile instance on the given file path, and acquire lock.\n        :param str path: the path to the file that will hold a lock\n        \"\"\"\n        self._path = path\n        mechanism = _UnixLockMechanism if POSIX_MODE else _WindowsLockMechanism\n        self._lock_mechanism = mechanism(path)\n\n        self.acquire()\n\n    def __repr__(self) -> str:\n        repr_str = '{0}({1}) <'.format(self.__class__.__name__, self._path)\n        if self.is_locked():\n            repr_str += 'acquired>'\n        else:\n            repr_str += 'released>'\n        return repr_str\n\n    def acquire(self) -> None:\n        \"\"\"\n        Acquire the lock on the file, forbidding any other Certbot instance to acquire it.\n        :raises errors.LockError: if unable to acquire the lock\n        \"\"\"\n        self._lock_mechanism.acquire()\n\n    def release(self) -> None:\n        \"\"\"\n        Release the lock on the file, allowing any other Certbot instance to acquire it.\n        \"\"\"\n        self._lock_mechanism.release()\n\n    def is_locked(self) -> bool:\n        \"\"\"\n        Check if the file is currently locked.\n        :return: True if the file is locked, False otherwise\n        \"\"\"\n        return self._lock_mechanism.is_locked()\n\n\nclass _BaseLockMechanism:\n    def __init__(self, path: str) -> None:\n        \"\"\"\n        Create a lock file mechanism for Unix.\n        :param str path: the path to the lock file\n        \"\"\"\n        self._path = path\n        self._fd: Optional[int] = None\n\n    def is_locked(self) -> bool:\n        \"\"\"Check if lock file is currently locked.\n        :return: True if the lock file is locked\n        :rtype: bool\n        \"\"\"\n        return self._fd is not None\n\n    def acquire(self) -> None:  # pylint: disable=missing-function-docstring\n        pass  # pragma: no cover\n\n    def release(self) -> None:  # pylint: disable=missing-function-docstring\n        pass  # pragma: no cover\n\n\nclass _UnixLockMechanism(_BaseLockMechanism):\n    \"\"\"\n    A UNIX lock file mechanism.\n    This lock file is released when the locked file is closed or the\n    process exits. It cannot be used to provide synchronization between\n    threads. It is based on the lock_file package by Martin Horcicka.\n    \"\"\"\n    def acquire(self) -> None:\n        \"\"\"Acquire the lock.\"\"\"\n        while self._fd is None:\n            # Open the file\n            fd = filesystem.open(self._path, os.O_CREAT | os.O_WRONLY, 0o600)\n            try:\n                self._try_lock(fd)\n                if self._lock_success(fd):\n                    self._fd = fd\n            finally:\n                # Close the file if it is not the required one\n                if self._fd is None:\n                    os.close(fd)\n\n    def _try_lock(self, fd: int) -> None:\n        \"\"\"\n        Try to acquire the lock file without blocking.\n        :param int fd: file descriptor of the opened file to lock\n        \"\"\"\n        try:\n            fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)\n        except OSError as err:\n            if err.errno in (errno.EACCES, errno.EAGAIN):\n                logger.debug('A lock on %s is held by another process.', self._path)\n                raise errors.LockError('Another instance of Certbot is already running.')\n            raise\n\n    def _lock_success(self, fd: int) -> bool:\n        \"\"\"\n        Did we successfully grab the lock?\n        Because this class deletes the locked file when the lock is\n        released, it is possible another process removed and recreated\n        the file between us opening the file and acquiring the lock.\n        :param int fd: file descriptor of the opened file to lock\n        :returns: True if the lock was successfully acquired\n        :rtype: bool\n        \"\"\"\n        # Normally os module should not be imported in certbot codebase except in certbot.compat\n        # for the sake of compatibility over Windows and Linux.\n        # We make an exception here, since _lock_success is private and called only on Linux.\n        from os import fstat  # pylint: disable=os-module-forbidden\n        from os import stat  # pylint: disable=os-module-forbidden\n        try:\n            stat1 = stat(self._path)\n        except OSError as err:\n            if err.errno == errno.ENOENT:\n                return False\n            raise\n\n        stat2 = fstat(fd)\n        # If our locked file descriptor and the file on disk refer to\n        # the same device and inode, they're the same file.\n        return stat1.st_dev == stat2.st_dev and stat1.st_ino == stat2.st_ino\n\n    def release(self) -> None:\n        \"\"\"Remove, close, and release the lock file.\"\"\"\n        # It is important the lock file is removed before it's released,\n        # otherwise:\n        #\n        # process A: open lock file\n        # process B: release lock file\n        # process A: lock file\n        # process A: check device and inode\n        # process B: delete file\n        # process C: open and lock a different file at the same path\n        try:\n            os.remove(self._path)\n        finally:\n            # Following check is done to make mypy happy: it ensure that self._fd, marked\n            # as Optional[int] is effectively int to make it compatible with os.close signature.\n            if self._fd is None:  # pragma: no cover\n                raise TypeError('Error, self._fd is None.')\n            try:\n                os.close(self._fd)\n            finally:\n                self._fd = None\n\n\nclass _WindowsLockMechanism(_BaseLockMechanism):\n    \"\"\"\n    A Windows lock file mechanism.\n    By default on Windows, acquiring a file handler gives exclusive access to the process\n    and results in an effective lock. However, it is possible to explicitly acquire the\n    file handler in shared access in terms of read and write, and this is done by os.open\n    in Python. So an explicit lock needs to be done through the call of\n    msvcrt.locking, that will lock the first byte of the file. In theory, it is also\n    possible to access a file in shared delete access, allowing other processes to delete an\n    opened file. But this needs also to be done explicitly by all processes using the Windows\n    low level APIs, and Python does not do it. As of Python 3.7 and below, Python developers\n    state that deleting a file opened by a process from another process is not possible with\n    os.open.\n    Consequently, msvcrt.locking is sufficient to obtain an effective lock, and the race\n    condition encountered on Linux is not possible on Windows, leading to a simpler workflow.\n    \"\"\"\n    def acquire(self) -> None:\n        \"\"\"Acquire the lock\"\"\"\n        open_mode = os.O_RDWR | os.O_CREAT | os.O_TRUNC\n\n        fd = None\n        try:\n            # Under Windows, filesystem.open will raise directly an EACCES error\n            # if the lock file is already locked.\n            fd = filesystem.open(self._path, open_mode, 0o600)\n            # This \"type: ignore\" is currently needed because msvcrt methods\n            # are only defined on Windows. See\n            # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi.\n            msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)  # type: ignore # pylint: disable=used-before-assignment\n        except OSError as err:\n            if fd:\n                os.close(fd)\n            # Anything except EACCES is unexpected. Raise directly the error in that case.\n            if err.errno != errno.EACCES:\n                raise\n            logger.debug('A lock on %s is held by another process.', self._path)\n            raise errors.LockError('Another instance of Certbot is already running.')\n\n        self._fd = fd\n\n    def release(self) -> None:\n        \"\"\"Release the lock.\"\"\"\n        try:\n            if not self._fd:\n                raise errors.Error(\"The lock has not been acquired first.\")\n            # This \"type: ignore\" is currently needed because msvcrt methods\n            # are only defined on Windows. See\n            # https://github.com/python/typeshed/blob/16ae4c61201cd8b96b8b22cdfb2ab9e89ba5bcf2/stdlib/msvcrt.pyi.\n            msvcrt.locking(self._fd, msvcrt.LK_UNLCK, 1)  # type: ignore # pylint: disable=used-before-assignment\n            os.close(self._fd)\n\n            try:\n                os.remove(self._path)\n            except OSError as e:\n                # If the lock file cannot be removed, it is not a big deal.\n                # Likely another instance is acquiring the lock we just released.\n                logger.debug(str(e))\n        finally:\n            self._fd = None\n\n\ndef lock_dir(dir_path: str) -> LockFile:\n    \"\"\"Place a lock file on the directory at dir_path.\n\n    The lock file is placed in the root of dir_path with the name\n    .certbot.lock.\n\n    :param str dir_path: path to directory\n\n    :returns: the locked LockFile object\n    :rtype: LockFile\n\n    :raises errors.LockError: if unable to acquire the lock\n\n    \"\"\"\n    return LockFile(os.path.join(dir_path, '.certbot.lock'))\n"
  },
  {
    "path": "certbot/src/certbot/_internal/log.py",
    "content": "\"\"\"Logging utilities for Certbot.\n\nThe best way to use this module is through `pre_arg_parse_setup` and\n`post_arg_parse_setup`. `pre_arg_parse_setup` configures a minimal\nterminal logger and ensures a detailed log is written to a secure\ntemporary file if Certbot exits before `post_arg_parse_setup` is called.\n`post_arg_parse_setup` relies on the parsed command line arguments and\ndoes the full logging setup with terminal and rotating file handling as\nconfigured by the user. Any logged messages before\n`post_arg_parse_setup` is called are sent to the rotating file handler.\nSpecial care is taken by both methods to ensure all errors are logged\nand properly flushed before program exit.\n\nThe `logging` module is useful for recording messages about about what\nCertbot is doing under the hood, but do not necessarily need to be shown\nto the user on the terminal. The default verbosity is WARNING.\n\nThe preferred method to display important information to the user is to\nuse `certbot.display.util` and `certbot.display.ops`.\n\n\"\"\"\n\n\nimport functools\nimport logging\nimport logging.handlers\nimport shutil\nimport sys\nimport tempfile\nimport traceback\nfrom types import TracebackType\nfrom typing import Any\nfrom typing import cast\nfrom typing import IO\nfrom typing import Optional\n\nfrom acme import messages\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot._internal.display import util as display_util\nfrom certbot.compat import os\n\n# Logging format\nCLI_FMT = \"%(message)s\"\nFILE_FMT = \"%(asctime)s:%(levelname)s:%(name)s:%(message)s\"\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef pre_arg_parse_setup() -> None:\n    \"\"\"Setup logging before command line arguments are parsed.\n\n    Terminal logging is setup using\n    `certbot._internal.constants.QUIET_LOGGING_LEVEL` so Certbot is as quiet as\n    possible. File logging is setup so that logging messages are\n    buffered in memory. If Certbot exits before `post_arg_parse_setup`\n    is called, these buffered messages are written to a temporary file.\n    If Certbot doesn't exit, `post_arg_parse_setup` writes the messages\n    to the normal log files.\n\n    This function also sets `logging.shutdown` to be called on program\n    exit which automatically flushes logging handlers and\n    `sys.excepthook` to properly log/display fatal exceptions.\n\n    \"\"\"\n    temp_handler = TempHandler()\n    temp_handler.setFormatter(logging.Formatter(FILE_FMT))\n    temp_handler.setLevel(logging.DEBUG)\n    memory_handler = MemoryHandler(temp_handler)\n\n    stream_handler = ColoredStreamHandler()\n    stream_handler.setFormatter(logging.Formatter(CLI_FMT))\n    # The pre-argparse logging level is set to WARNING here. This is to ensure that\n    # deprecated flags (see DeprecatedArgumentAction) print something to the terminal.\n    # See https://github.com/certbot/certbot/issues/9618.\n    stream_handler.setLevel(logging.WARNING)\n\n    root_logger = logging.getLogger()\n    root_logger.setLevel(logging.DEBUG)  # send all records to handlers\n    root_logger.addHandler(memory_handler)\n    root_logger.addHandler(stream_handler)\n\n    # logging.shutdown will flush the memory handler because flush() and\n    # close() are explicitly called\n    util.atexit_register(logging.shutdown)\n    sys.excepthook = functools.partial(\n        pre_arg_parse_except_hook, memory_handler,\n        debug='--debug' in sys.argv,\n        quiet='--quiet' in sys.argv or '-q' in sys.argv,\n        log_path=temp_handler.path)\n\n\ndef post_arg_parse_setup(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Setup logging after command line arguments are parsed.\n\n    This function assumes `pre_arg_parse_setup` was called earlier and\n    the root logging configuration has not been modified. A rotating\n    file logging handler is created and the buffered log messages are\n    sent to that handler. Terminal logging output is set to the level\n    requested by the user.\n\n    :param certbot.configuration.NamespaceConfig config: Configuration object\n\n    \"\"\"\n    file_handler, file_path = setup_log_file_handler(\n        config, 'letsencrypt.log', FILE_FMT)\n\n    root_logger = logging.getLogger()\n    memory_handler = stderr_handler = None\n    for handler in root_logger.handlers:\n        if isinstance(handler, ColoredStreamHandler):\n            stderr_handler = handler\n        elif isinstance(handler, MemoryHandler):\n            memory_handler = handler\n    msg = 'Previously configured logging handlers have been removed!'\n    assert memory_handler is not None and stderr_handler is not None, msg\n\n    root_logger.addHandler(file_handler)\n    root_logger.removeHandler(memory_handler)\n    temp_handler = getattr(memory_handler, 'target', None)\n    memory_handler.setTarget(file_handler)  # pylint: disable=no-member\n    memory_handler.flush(force=True)  # pylint: disable=unexpected-keyword-arg\n    memory_handler.close()\n    if temp_handler:\n        temp_handler.close()\n\n    if config.quiet:\n        level = constants.QUIET_LOGGING_LEVEL\n    elif config.verbose_level is not None:\n        level = constants.DEFAULT_LOGGING_LEVEL - int(config.verbose_level) * 10\n    else:\n        level = constants.DEFAULT_LOGGING_LEVEL - config.verbose_count * 10\n\n    stderr_handler.setLevel(level)\n    logger.debug('Root logging level set at %d', level)\n\n    if not config.quiet:\n        print(f'Saving debug log to {file_path}', file=sys.stderr)\n\n    sys.excepthook = functools.partial(\n        post_arg_parse_except_hook,\n        debug=config.debug, quiet=config.quiet, log_path=file_path)\n\n\ndef setup_log_file_handler(config: configuration.NamespaceConfig, logfile: str,\n                           fmt: str) -> tuple[logging.Handler, str]:\n    \"\"\"Setup file debug logging.\n\n    :param certbot.configuration.NamespaceConfig config: Configuration object\n    :param str logfile: basename for the log file\n    :param str fmt: logging format string\n\n    :returns: file handler and absolute path to the log file\n    :rtype: tuple\n\n    \"\"\"\n    # TODO: logs might contain sensitive data such as contents of the\n    # private key! #525\n    util.set_up_core_dir(config.logs_dir, 0o700, config.strict_permissions)\n    log_file_path = os.path.join(config.logs_dir, logfile)\n    try:\n        handler = logging.handlers.RotatingFileHandler(\n            log_file_path, maxBytes=2 ** 20,\n            backupCount=config.max_log_backups)\n    except OSError as error:\n        raise errors.Error(util.PERM_ERR_FMT.format(error))\n    # rotate on each invocation, rollover only possible when maxBytes\n    # is nonzero and backupCount is nonzero, so we set maxBytes as big\n    # as possible not to overrun in single CLI invocation (1MB).\n    handler.doRollover()  # TODO: creates empty letsencrypt.log.1 file\n    handler.setLevel(logging.DEBUG)\n    handler_formatter = logging.Formatter(fmt=fmt)\n    handler.setFormatter(handler_formatter)\n    return handler, log_file_path\n\n\nclass ColoredStreamHandler(logging.StreamHandler):\n    \"\"\"Sends colored logging output to a stream.\n\n    If the specified stream is not a tty, the class works like the\n    standard `logging.StreamHandler`. Default red_level is\n    `logging.WARNING`.\n\n    :ivar bool colored: True if output should be colored\n    :ivar bool red_level: The level at which to output\n\n    \"\"\"\n    def __init__(self, stream: Optional[IO] = None) -> None:\n        super().__init__(stream)\n        self.colored = (sys.stderr.isatty() if stream is None else\n                        stream.isatty())\n        self.red_level = logging.WARNING\n\n    def format(self, record: logging.LogRecord) -> str:\n        \"\"\"Formats the string representation of record.\n\n        :param logging.LogRecord record: Record to be formatted\n\n        :returns: Formatted, string representation of record\n        :rtype: str\n\n        \"\"\"\n        out = super().format(record)\n        if self.colored and record.levelno >= self.red_level:\n            return ''.join((util.ANSI_SGR_RED, out, util.ANSI_SGR_RESET))\n        return out\n\n\nclass MemoryHandler(logging.handlers.MemoryHandler):\n    \"\"\"Buffers logging messages in memory until the buffer is flushed.\n\n    This differs from `logging.handlers.MemoryHandler` in that flushing\n    only happens when flush(force=True) is called.\n\n    \"\"\"\n    def __init__(self, target: Optional[logging.Handler] = None,\n                 capacity: int = 10000) -> None:\n        # capacity doesn't matter because should_flush() is overridden\n        super().__init__(capacity, target=target)\n\n    def close(self) -> None:\n        \"\"\"Close the memory handler, but don't set the target to None.\"\"\"\n        # This allows the logging module which may only have a weak\n        # reference to the target handler to properly flush and close it.\n        target = getattr(self, 'target')\n        super().close()\n        self.target = target\n\n    def flush(self, force: bool = False) -> None:  # pylint: disable=arguments-differ\n        \"\"\"Flush the buffer if force=True.\n\n        If force=False, this call is a noop.\n\n        :param bool force: True if the buffer should be flushed.\n\n        \"\"\"\n        # This method allows flush() calls in logging.shutdown to be a\n        # noop so we can control when this handler is flushed.\n        if force:\n            super().flush()\n\n    def shouldFlush(self, record: logging.LogRecord) -> bool:\n        \"\"\"Should the buffer be automatically flushed?\n\n        :param logging.LogRecord record: log record to be considered\n\n        :returns: False because the buffer should never be auto-flushed\n        :rtype: bool\n\n        \"\"\"\n        return False\n\n\nclass TempHandler(logging.StreamHandler):\n    \"\"\"Safely logs messages to a temporary file.\n\n    The file is created with permissions 600. If no log records are sent\n    to this handler, the temporary file is deleted when the handler is\n    closed.\n\n    :ivar str path: file system path to the temporary log file\n\n    \"\"\"\n    def __init__(self) -> None:\n        self._workdir = tempfile.mkdtemp(prefix=\"certbot-log-\")\n        self.path = os.path.join(self._workdir, 'log')\n        stream = util.safe_open(self.path, mode='w', chmod=0o600)\n        super().__init__(stream)\n        # Super constructor assigns the provided stream object to self.stream.\n        # Let's help mypy be aware of this by giving a type hint.\n        self.stream: IO[str]\n        self._delete = True\n\n    def emit(self, record: logging.LogRecord) -> None:\n        \"\"\"Log the specified logging record.\n\n        :param logging.LogRecord record: Record to be formatted\n\n        \"\"\"\n        self._delete = False\n        super().emit(record)\n\n    def close(self) -> None:\n        \"\"\"Close the handler and the temporary log file.\n\n        The temporary log file is deleted if it wasn't used.\n\n        \"\"\"\n        self.acquire()\n        try:\n            # StreamHandler.close() doesn't close the stream to allow a\n            # stream like stderr to be used\n            self.stream.close()\n            if self._delete:\n                shutil.rmtree(self._workdir)\n            self._delete = False\n            super().close()\n        finally:\n            self.release()\n\n\ndef pre_arg_parse_except_hook(memory_handler: MemoryHandler,\n                              *args: Any, **kwargs: Any) -> None:\n    \"\"\"A simple wrapper around post_arg_parse_except_hook.\n\n    The additional functionality provided by this wrapper is the memory\n    handler will be flushed before Certbot exits. This allows us to\n    write logging messages to a temporary file if we crashed before\n    logging was fully configured.\n\n    Since sys.excepthook isn't called on SystemExit exceptions, the\n    memory handler will not be flushed in this case which prevents us\n    from creating temporary log files when argparse exits because a\n    command line argument was invalid or -h, --help, or --version was\n    provided on the command line.\n\n    :param MemoryHandler memory_handler: memory handler to flush\n    :param tuple args: args for post_arg_parse_except_hook\n    :param dict kwargs: kwargs for post_arg_parse_except_hook\n\n    \"\"\"\n    try:\n        post_arg_parse_except_hook(*args, **kwargs)\n    finally:\n        # flush() is called here so messages logged during\n        # post_arg_parse_except_hook are also flushed.\n        memory_handler.flush(force=True)\n\n\ndef post_arg_parse_except_hook(exc_type: type[BaseException], exc_value: BaseException,\n                               trace: TracebackType, debug: bool, quiet: bool,\n                               log_path: str) -> None:\n    \"\"\"Logs fatal exceptions and reports them to the user.\n\n    If debug is True, the full exception and traceback is shown to the\n    user, otherwise, it is suppressed. sys.exit is always called with a\n    nonzero status.\n\n    :param type exc_type: type of the raised exception\n    :param BaseException exc_value: raised exception\n    :param traceback trace: traceback of where the exception was raised\n    :param bool debug: True if the traceback should be shown to the user\n    :param bool quiet: True if Certbot is running in quiet mode\n    :param str log_path: path to file or directory containing the log\n\n    \"\"\"\n    exc_info = (exc_type, exc_value, trace)\n    # Only print human advice if not running under --quiet\n    def exit_func() -> None:\n        if quiet:\n            sys.exit(1)\n        else:\n            exit_with_advice(log_path)\n    # constants.QUIET_LOGGING_LEVEL or higher should be used to\n    # display message the user, otherwise, a lower level like\n    # logger.DEBUG should be used\n    if debug or not issubclass(exc_type, Exception):\n        assert constants.QUIET_LOGGING_LEVEL <= logging.ERROR\n        if exc_type is KeyboardInterrupt:\n            logger.error('Exiting due to user request.')\n            sys.exit(1)\n        logger.error('Exiting abnormally:', exc_info=exc_info)\n    else:\n        logger.debug('Exiting abnormally:', exc_info=exc_info)\n        # Use logger to print the error message to take advantage of\n        # our logger printing warnings and errors in red text.\n        if issubclass(exc_type, errors.Error):\n            logger.error(str(exc_value))\n            exit_func()\n        logger.error('An unexpected error occurred:')\n        acme_error = getattr(exc_value, \"error\", exc_value)\n        if messages.is_acme_error(acme_error):\n            logger.error(display_util.describe_acme_error(cast(messages.Error, acme_error)))\n        else:\n            output = traceback.format_exception_only(exc_type, exc_value)\n            # format_exception_only returns a list of strings each\n            # terminated by a newline. We combine them into one string\n            # and remove the final newline before passing it to\n            # logger.error.\n            logger.error(''.join(output).rstrip())\n    exit_func()\n\n\ndef exit_with_advice(log_path: str) -> None:\n    \"\"\"Print a link to the community forums, the debug log path, and exit\n\n    The message is printed to stderr and the program will exit with a\n    nonzero status.\n\n    :param str log_path: path to file or directory containing the log\n\n    \"\"\"\n    msg = (\"Ask for help or search for solutions at https://community.letsencrypt.org. \"\n           \"See the \")\n    if os.path.isdir(log_path):\n        msg += f'logfiles in {log_path} '\n    else:\n        msg += f\"logfile {log_path} \"\n    msg += 'or re-run Certbot with -v for more details.'\n    sys.exit(msg)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/main.py",
    "content": "\"\"\"Certbot main entry point.\"\"\"\n# pylint: disable=too-many-lines\n\nfrom contextlib import contextmanager\nimport copy\nimport functools\nimport logging.handlers\nimport sys\nfrom typing import cast\nfrom typing import Generator\nfrom typing import IO\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TypeVar\nfrom typing import Union\n\nimport configobj\nfrom cryptography import x509\nimport josepy as jose\nfrom josepy import b64\n\nfrom acme import client as acme_client\nfrom acme import errors as acme_errors\nfrom acme import messages as acme_messages\nimport certbot\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import account\nfrom certbot._internal import cert_manager\nfrom certbot._internal import cli\nfrom certbot._internal import client\nfrom certbot._internal import constants\nfrom certbot._internal import eff\nfrom certbot._internal import hooks\nfrom certbot._internal import log\nfrom certbot._internal import renewal\nfrom certbot._internal import san\nfrom certbot._internal import snap_config\nfrom certbot._internal import storage\nfrom certbot._internal import updater\nfrom certbot._internal.display import obj as display_obj\nfrom certbot._internal.display import util as internal_display_util\nfrom certbot._internal.plugins import disco as plugins_disco\nfrom certbot._internal.plugins import selection as plug_sel\nfrom certbot.compat import filesystem\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.display import ops as display_ops\nfrom certbot.display import util as display_util\nfrom certbot.plugins import enhancements\n\nUSER_CANCELLED = (\"User chose to cancel the operation and may \"\n                  \"reinvoke the client.\")\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef _suggest_donation_if_appropriate(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Potentially suggest a donation to support Certbot.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    # don't prompt for donation if:\n    # - renewing\n    # - using the staging server (--staging or --dry-run)\n    # - running with --quiet (display fd won't be available during atexit calls #8995)\n    assert config.verb != \"renew\"\n    if config.staging or config.quiet:\n        return\n    util.atexit_register(\n        display_util.notification,\n        \"If you like Certbot, please consider supporting our work by:\\n\"\n        \" * Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate\\n\"\n        \" * Donating to EFF:                    https://eff.org/donate-le\",\n        pause=False\n    )\n\n\ndef _get_and_save_cert(le_client: client.Client, config: configuration.NamespaceConfig,\n                       sans: Optional[list[san.SAN]] = None, certname: Optional[str] = None,\n                       lineage: Optional[storage.RenewableCert] = None\n                       ) -> Optional[storage.RenewableCert]:\n    \"\"\"Authenticate and enroll certificate.\n\n    This method finds the relevant lineage, figures out what to do with it,\n    then performs that action. Includes calls to hooks, various reports,\n    checks, and requests for user input.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param sans: List of domain names and/or IP addresses for which to get a certificate.\n        Defaults to `None`\n    :type sans: `list` of `san.SAN`\n\n    :param certname: Name of new certificate. Defaults to `None`\n    :type certname: str\n\n    :param lineage: Certificate lineage object. Defaults to `None`\n    :type lineage: storage.RenewableCert\n\n    :returns: the issued certificate or `None` if doing a dry run\n    :rtype: storage.RenewableCert or None\n\n    :raises errors.Error: if certificate could not be obtained\n\n    \"\"\"\n    hooks.pre_hook(config)\n    renewed_sans: list[san.SAN] = []\n\n    try:\n        if lineage is not None:\n            # Renewal, where we already know the specific lineage we're\n            # interested in\n            display_util.notify(\n                \"{action} for {identifiers}\".format(\n                    action=\"Simulating renewal of an existing certificate\"\n                    if config.dry_run else \"Renewing an existing certificate\",\n                    identifiers=internal_display_util.summarize_sans(sans or lineage.sans())\n                )\n            )\n            renewal.renew_cert(config, sans, le_client, lineage)\n        else:\n            # TREAT AS NEW REQUEST\n            if sans is None:\n                raise errors.Error(\"Domain list cannot be none if the lineage is not set.\")\n            display_util.notify(\n                \"{action} for {identifiers}\".format(\n                    action=\"Simulating a certificate request\" if config.dry_run else\n                           \"Requesting a certificate\",\n                    identifiers=internal_display_util.summarize_sans(sans)\n                )\n            )\n            lineage = le_client.obtain_and_enroll_certificate(sans, certname)\n            if lineage is not None:\n                hooks.deploy_hook(config, lineage.sans(), lineage.live_dir)\n                renewed_sans.extend(sans)\n    finally:\n        hooks.post_hook(config, renewed_sans)\n\n    return lineage\n\n\ndef _handle_unexpected_key_type_migration(config: configuration.NamespaceConfig,\n                                          cert: storage.RenewableCert) -> bool:\n    \"\"\"\n    This function ensures that the user will not implicitly migrate an existing key\n    from one type to another in the situation where a certificate for that lineage\n    already exist and they have not provided explicitly --key-type and --cert-name.\n    :param config: Current configuration provided by the client\n    :param cert: Matching certificate that could be renewed\n    :returns: Whether a key type migration is going ahead.\n    :rtype: `bool`\n    \"\"\"\n    new_key_type = config.key_type.upper()\n    cur_key_type = cert.private_key_type.upper()\n\n    if new_key_type == cur_key_type:\n        return False\n\n    # If both --key-type and --cert-name are provided, we consider the user's intent to\n    # be unambiguous: to change the key type of this lineage.\n    is_confirmed_via_cli = config.set_by_user(\"key_type\") and config.set_by_user(\"certname\")\n\n    # Failing that, we interactively prompt the user to confirm the change.\n    if is_confirmed_via_cli or display_util.yesno(\n        f'An {cur_key_type} certificate named {cert.lineagename} already exists. Do you want to '\n        f'update its key type to {new_key_type}?',\n        yes_label='Update key type', no_label='Keep existing key type',\n        default=False, force_interactive=False,\n    ):\n        return True\n\n    # If --key-type was set on the CLI but the user did not confirm the key type change using\n    # one of the two above methods, their intent is ambiguous. Error out.\n    if config.set_by_user(\"key_type\"):\n        raise errors.Error(\n            'Are you trying to change the key type of the certificate named '\n            f'{cert.lineagename} from {cur_key_type} to {new_key_type}? Please provide '\n            'both --cert-name and --key-type on the command line to confirm the change '\n            'you are trying to make.'\n        )\n\n    # The mismatch between the lineage's key type and config.key_type is caused by Certbot's\n    # default value. The user is not asking for a key change: keep the key type of the existing\n    # lineage.\n    config.key_type = cur_key_type.lower()\n    return False\n\n\ndef _handle_subset_cert_request(config: configuration.NamespaceConfig,\n                                sans: Iterable[san.SAN],\n                                cert: storage.RenewableCert\n                                ) -> tuple[str, Optional[storage.RenewableCert]]:\n    \"\"\"Figure out what to do if a previous cert had a subset of the names now requested\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param sans: List of domain names and/or IP addresses\n    :type sans: `list` of `san.SAN`\n\n    :param cert: Certificate object\n    :type cert: storage.RenewableCert\n\n    :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_sans_and_certname\n              action can be: \"newcert\" | \"renew\" | \"reinstall\"\n    :rtype: `tuple` of `str`\n\n    \"\"\"\n    _handle_unexpected_key_type_migration(config, cert)\n\n    question = (\n        \"You have an existing certificate that contains a portion of \"\n        \"the domains you requested (ref: {0}){br}{br}It contains these \"\n        \"names: {1}{br}{br}You requested these names for the new \"\n        \"certificate: {2}.{br}{br}Do you want to expand and replace this existing \"\n        \"certificate with the new certificate?\"\n    ).format(cert.configfile.filename,\n             san.display(cert.sans()),\n             san.display(sans),\n             br=os.linesep)\n    if config.expand or config.renew_by_default or display_util.yesno(\n        question, \"Expand\", \"Cancel\", cli_flag=\"--expand\", force_interactive=True):\n        return \"renew\", cert\n    display_util.notify(\n        \"To obtain a new certificate that contains these names without \"\n        \"replacing your existing certificate for {0}, you must use the \"\n        \"--duplicate option.{br}{br}\"\n        \"For example:{br}{br}{1} --duplicate {2}\".format(\n            san.display(cert.sans()),\n            cli.cli_command, \" \".join(sys.argv[1:]),\n            br=os.linesep\n        ))\n    raise errors.Error(USER_CANCELLED)\n\n\ndef _handle_identical_cert_request(config: configuration.NamespaceConfig,\n                                   lineage: storage.RenewableCert,\n                                   ) -> tuple[str, Optional[storage.RenewableCert]]:\n    \"\"\"Figure out what to do if a lineage has the same names as a previously obtained one\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_sans_and_certname\n              action can be: \"newcert\" | \"renew\" | \"reinstall\"\n    :rtype: `tuple` of `str`\n\n    \"\"\"\n    is_key_type_changing = _handle_unexpected_key_type_migration(config, lineage)\n\n    if not lineage.ensure_deployed():\n        return \"reinstall\", lineage\n    if is_key_type_changing:\n        return \"renew\", lineage\n\n    ari_clients = renewal.AriClientPool(config)\n\n    if renewal.should_renew(config, lineage, ari_clients):\n        return \"renew\", lineage\n\n    if config.reinstall:\n        # Set with --reinstall, force an identical certificate to be\n        # reinstalled without further prompting.\n        return \"reinstall\", lineage\n    question = (\n        \"You have an existing certificate that has exactly the same \"\n        \"domains or certificate name you requested and isn't close to expiry.\"\n        \"{br}(ref: {0}){br}{br}What would you like to do?\"\n    ).format(lineage.configfile.filename, br=os.linesep)\n\n    if config.verb == \"run\":\n        keep_opt = \"Attempt to reinstall this existing certificate\"\n    else:\n        assert config.verb == \"certonly\", \"Unexpected Certbot subcommand\"\n        keep_opt = \"Keep the existing certificate for now\"\n    choices = [keep_opt,\n               \"Renew & replace the certificate (may be subject to CA rate limits)\"]\n\n    response = display_util.menu(question, choices,\n                                    default=0, force_interactive=True)\n    if response[0] == display_util.CANCEL:\n        # TODO: Add notification related to command-line options for\n        #       skipping the menu for this case.\n        raise errors.Error(\n            \"Operation canceled. You may re-run the client.\")\n    if response[1] == 0:\n        return \"reinstall\", lineage\n    elif response[1] == 1:\n        return \"renew\", lineage\n    raise AssertionError('This is impossible')\n\n\ndef _find_lineage_for_sans(config: configuration.NamespaceConfig, sans: list[san.SAN]\n                           ) -> tuple[Optional[str], Optional[storage.RenewableCert]]:\n    \"\"\"Determine whether there are duplicated names and how to handle\n    them (renew, reinstall, newcert, or raising an error to stop\n    the client run if the user chooses to cancel the operation when\n    prompted).\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param sans: List of domain names and/or IP addresses\n    :type sans: `list` of `san.SAN`\n\n    :returns: Two-element tuple containing desired new-certificate behavior as\n              a string token (\"reinstall\", \"renew\", or \"newcert\"), plus either\n              a RenewableCert instance or `None` if renewal shouldn't occur.\n    :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None`\n\n    :raises errors.Error: If the user would like to rerun the client again.\n\n    \"\"\"\n    # Considering the possibility that the requested certificate is\n    # related to an existing certificate.  (config.duplicate, which\n    # is set with --duplicate, skips all of this logic and forces any\n    # kind of certificate to be obtained with renewal = False.)\n    if config.duplicate:\n        return \"newcert\", None\n    # TODO: Also address superset case\n    ident_names_cert, subset_names_cert = cert_manager.find_duplicative_certs(config, sans)\n    # XXX ^ schoen is not sure whether that correctly reads the systemwide\n    # configuration file.\n    if ident_names_cert is None and subset_names_cert is None:\n        return \"newcert\", None\n\n    if ident_names_cert is not None:\n        return _handle_identical_cert_request(config, ident_names_cert)\n    elif subset_names_cert is not None:\n        return _handle_subset_cert_request(config, sans, subset_names_cert)\n    return None, None\n\n\ndef _find_cert(config: configuration.NamespaceConfig, sans: list[san.SAN], certname: str\n               ) -> tuple[bool, Optional[storage.RenewableCert]]:\n    \"\"\"Finds an existing certificate object given domains and/or a certificate name.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param sans: List of domain names and/or IP addresses\n    :type sans: `list` of `san.SAN`\n\n    :param certname: Name of certificate\n    :type certname: str\n\n    :returns: Two-element tuple of a boolean that indicates if this function should be\n              followed by a call to fetch a certificate from the server, and either a\n              RenewableCert instance or None.\n    :rtype: `tuple` of `bool` and :class:`storage.RenewableCert` or `None`\n\n    \"\"\"\n    action, lineage = _find_lineage_for_sans_and_certname(config, sans, certname)\n    if action == \"reinstall\":\n        logger.info(\"Keeping the existing certificate\")\n    return (action != \"reinstall\"), lineage\n\n\ndef _find_lineage_for_sans_and_certname(\n        config: configuration.NamespaceConfig, sans: list[san.SAN],\n        certname: str) -> tuple[Optional[str], Optional[storage.RenewableCert]]:\n    \"\"\"Find appropriate lineage based on given domains and/or certname.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param sans: List of domain names and/or IP addresses\n    :type sans: `list` of `san.SAN`\n\n    :param certname: Name of certificate\n    :type certname: str\n\n    :returns: Two-element tuple containing desired new-certificate behavior as\n              a string token (\"reinstall\", \"renew\", or \"newcert\"), plus either\n              a RenewableCert instance or None if renewal should not occur.\n\n    :rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None`\n\n    :raises errors.Error: If the user would like to rerun the client again.\n\n    \"\"\"\n    if not certname:\n        return _find_lineage_for_sans(config, sans)\n    lineage = cert_manager.lineage_for_certname(config, certname)\n    if lineage:\n        if sans:\n            computed_domains = cert_manager.sans_for_certname(config, certname)\n            if computed_domains and set(computed_domains) != set(sans):\n                _handle_unexpected_key_type_migration(config, lineage)\n                _ask_user_to_confirm_new_sans(config, sans, certname,\n                                              lineage.sans())  # raises if no\n                return \"renew\", lineage\n        # unnecessarily specified domains or no domains specified\n        return _handle_identical_cert_request(config, lineage)\n    elif sans:\n        return \"newcert\", None\n    raise errors.ConfigurationError(\"No certificate with name {0} found. \"\n                                    \"Use -d to specify domains, or run certbot certificates to see \"\n                                    \"possible certificate names.\".format(certname))\n\n\nT = TypeVar(\"T\")\n\n\ndef _get_added_removed(after: Iterable[T], before: Iterable[T]) -> tuple[list[T], list[T]]:\n    \"\"\"Get lists of items removed from `before`\n    and a lists of items added to `after`\n    \"\"\"\n    added = list(set(after) - set(before))\n    removed = list(set(before) - set(after))\n    added.sort()\n    removed.sort()\n    return added, removed\n\n\ndef _format_list(character: str, strings: Iterable[str]) -> str:\n    \"\"\"Format list with given character\n    \"\"\"\n    if not strings:\n        formatted = \"{br}(None)\"\n    else:\n        formatted = \"{br}{ch} \" + \"{br}{ch} \".join(strings)\n    return formatted.format(\n        ch=character,\n        br=os.linesep\n    )\n\n\ndef _ask_user_to_confirm_new_sans(config: configuration.NamespaceConfig,\n                                  new_sans: Iterable[san.SAN],\n                                  certname: str,\n                                  old_sans: Iterable[san.SAN]) -> None:\n    \"\"\"Ask user to confirm update cert certname to contain new_sans.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param new_sans: List of new domain names and/or IP addresses\n    :type new_sans: `list` of `san.SAN`\n\n    :param certname: Name of certificate\n    :type certname: str\n\n    :param old_sans: List of old domain names and/or IP addresses\n    :type old_sans: `list` of `san.SAN`\n\n    :returns: None\n    :rtype: None\n\n    :raises errors.ConfigurationError: if cert name and domains mismatch\n\n    \"\"\"\n    if config.renew_with_new_domains:\n        return\n\n    added, removed = _get_added_removed(map(str, new_sans), map(str, old_sans))\n\n    msg = (\"You are updating certificate {0} to include new domain(s): {1}{br}{br}\"\n           \"You are also removing previously included domain(s): {2}{br}{br}\"\n           \"Did you intend to make this change?\".format(\n               certname,\n               _format_list(\"+\", added),\n               _format_list(\"-\", removed),\n               br=os.linesep))\n    if not display_util.yesno(msg, \"Update certificate\", \"Cancel\", default=True):\n        raise errors.ConfigurationError(\"Specified mismatched certificate name and domains.\")\n\n\ndef _find_sans_or_certname(config: configuration.NamespaceConfig,\n                           installer: Optional[interfaces.Installer],\n                           question: Optional[str] = None) -> tuple[list[san.SAN], str]:\n    \"\"\"Retrieve domains and certname from config or user input.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :param `str` question: Overriding default question to ask the user if asked\n        to choose from domain names.\n\n    :returns: Two-part tuple of domains and certname\n    :rtype: `tuple` of list of `str` and `str`\n\n    :raises errors.Error: Usage message, if parameters are not used correctly\n\n    \"\"\"\n    domains: list[san.SAN] = []\n    ip_addresses: list[san.SAN] = []\n    certname = config.certname\n\n    # first, try to get domains from the config\n    if config.domains:\n        domains = config.domains\n    if config.ip_addresses:\n        ip_addresses = config.ip_addresses\n\n    sans: Optional[list[san.SAN]] = domains + ip_addresses\n\n    # if we can't do that but we have a certname, get the sans\n    # by loading the latest certificate with that certname\n    if certname and not sans:\n        sans = cert_manager.sans_for_certname(config, certname)\n\n    # that certname might not have existed, or there was a problem.\n    # try to get domains from the user.\n    if not sans:\n        sans = san.guess(display_ops.choose_names(installer, question))\n\n    if not sans:\n        raise errors.Error(\"Please specify --domains, --ip-address, or --installer that \"\n                           \"will help in domain names autodiscovery, or \"\n                           \"--cert-name for an existing certificate name.\")\n\n    return sans, certname\n\n\ndef _report_next_steps(config: configuration.NamespaceConfig, installer_err: Optional[errors.Error],\n                       lineage: Optional[storage.RenewableCert],\n                       new_or_renewed_cert: bool = True) -> None:\n    \"\"\"Displays post-run/certonly advice to the user about renewal and installation.\n\n    The output varies by runtime configuration and any errors encountered during installation.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param installer_err: The installer/enhancement error encountered, if any.\n    :type error: Optional[errors.Error]\n\n    :param lineage: The resulting certificate lineage from the issuance, if any.\n    :type lineage: Optional[storage.RenewableCert]\n\n    :param bool new_or_renewed_cert: Whether the verb execution resulted in a certificate\n                                     being saved (created or renewed).\n\n    \"\"\"\n    steps: list[str] = []\n\n    # If the installation or enhancement raised an error, show advice on trying again\n    if installer_err:\n        # Special case where either --nginx or --apache were used, causing us to\n        # run the \"installer\" (i.e. reloading the nginx/apache config)\n        if config.verb == 'certonly':\n            steps.append(\n                \"The certificate was saved, but was not successfully loaded by the installer \"\n                f\"({config.installer}) due to the installer failing to reload. \"\n                f\"After fixing the error shown below, try reloading {config.installer} manually.\"\n            )\n        else:\n            steps.append(\n                \"The certificate was saved, but could not be installed (installer: \"\n                f\"{config.installer}). After fixing the error shown below, try installing it again \"\n                f\"by running:\\n  {cli.cli_command} install --cert-name \"\n                f\"{_cert_name_from_config_or_lineage(config, lineage)}\"\n            )\n\n    # If a certificate was obtained or renewed, show applicable renewal advice\n    if new_or_renewed_cert:\n        if config.csr:\n            steps.append(\n                \"Certificates created using --csr will not be renewed automatically by Certbot. \"\n                \"You will need to renew the certificate before it expires, by running the same \"\n                \"Certbot command again.\")\n        elif _is_interactive_only_auth(config):\n            steps.append(\n                \"This certificate will not be renewed automatically. Autorenewal of \"\n                \"--manual certificates requires the use of an authentication hook script \"\n                \"(--manual-auth-hook) but one was not provided. To renew this certificate, repeat \"\n                f\"this same {cli.cli_command} command before the certificate's expiry date.\"\n            )\n        elif not config.preconfigured_renewal:\n            steps.append(\n                \"The certificate will need to be renewed before it expires. Certbot can \"\n                \"automatically renew the certificate in the background, but you may need \"\n                \"to take steps to enable that functionality. \"\n                \"See https://certbot.org/renewal-setup for instructions.\")\n\n    if not steps:\n        return\n\n    # TODO: refactor ANSI escapes during https://github.com/certbot/certbot/issues/8848\n    (bold_on, nl, bold_off) = [c if sys.stdout.isatty() and not config.quiet else '' \\\n                               for c in (util.ANSI_SGR_BOLD, '\\n', util.ANSI_SGR_RESET)]\n    print(bold_on, end=nl)\n    display_util.notify(\"NEXT STEPS:\")\n    print(bold_off, end='')\n\n    for step in steps:\n        display_util.notify(f\"- {step}\")\n\n    # If there was an installer error, segregate the error output with a trailing newline\n    if installer_err:\n        print()\n\n\ndef _report_new_cert(config: configuration.NamespaceConfig, cert_path: Optional[str],\n                     fullchain_path: Optional[str], key_path: Optional[str] = None) -> None:\n    \"\"\"Reports the creation of a new certificate to the user.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param cert_path: path to certificate\n    :type cert_path: str\n\n    :param fullchain_path: path to full chain\n    :type fullchain_path: str\n\n    :param key_path: path to private key, if available\n    :type key_path: str\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    if config.dry_run:\n        display_util.notify(\"The dry run was successful.\")\n        return\n\n    assert cert_path and fullchain_path, \"No certificates saved to report.\"\n\n    renewal_msg = \"\"\n    if config.preconfigured_renewal and not _is_interactive_only_auth(config):\n        renewal_msg = (\"\\nCertbot has set up a scheduled task to automatically renew this \"\n                       \"certificate in the background.\")\n\n    display_util.notify(\n        (\"\\nSuccessfully received certificate.\\n\"\n        \"Certificate is saved at: {cert_path}\\n{key_msg}\"\n        \"This certificate expires on {expiry}.\\n\"\n        \"These files will be updated when the certificate renews.{renewal_msg}{nl}\").format(\n            cert_path=fullchain_path,\n            expiry=crypto_util.notAfter(cert_path).date(),\n            key_msg=\"Key is saved at:         {}\\n\".format(key_path) if key_path else \"\",\n            renewal_msg=renewal_msg,\n            nl=\"\\n\" if config.verb == \"run\" else \"\" # Normalize spacing across verbs\n        )\n    )\n\n\ndef _is_interactive_only_auth(config: configuration.NamespaceConfig) -> bool:\n    \"\"\" Whether the current authenticator params only support interactive renewal.\n    \"\"\"\n    # --manual without --manual-auth-hook can never autorenew\n    if config.authenticator == \"manual\" and config.manual_auth_hook is None:\n        return True\n\n    return False\n\n\ndef _csr_report_new_cert(config: configuration.NamespaceConfig, cert_path: Optional[str],\n                         chain_path: Optional[str], fullchain_path: Optional[str]) -> None:\n    \"\"\" --csr variant of _report_new_cert.\n\n    Until --csr is overhauled (#8332) this is transitional function to report the creation\n    of a new certificate using --csr.\n    TODO: remove this function and just call _report_new_cert when --csr is overhauled.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param str cert_path: path to cert.pem\n\n    :param str chain_path: path to chain.pem\n\n    :param str fullchain_path: path to fullchain.pem\n\n    \"\"\"\n    if config.dry_run:\n        display_util.notify(\"The dry run was successful.\")\n        return\n\n    assert cert_path and fullchain_path, \"No certificates saved to report.\"\n\n    expiry = crypto_util.notAfter(cert_path).date()\n\n    display_util.notify(\n        (\"\\nSuccessfully received certificate.\\n\"\n        \"Certificate is saved at:            {cert_path}\\n\"\n        \"Intermediate CA chain is saved at:  {chain_path}\\n\"\n        \"Full certificate chain is saved at: {fullchain_path}\\n\"\n        \"This certificate expires on {expiry}.\").format(\n            cert_path=cert_path, chain_path=chain_path,\n            fullchain_path=fullchain_path, expiry=expiry,\n        )\n    )\n\n\ndef _determine_account(config: configuration.NamespaceConfig\n                       ) -> tuple[account.Account,\n                                  Optional[acme_client.ClientV2]]:\n    \"\"\"Determine which account to use.\n\n    If ``config.account`` is ``None``, it will be updated based on the\n    user input. Same for ``config.email``.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :returns: Account and optionally ACME client API (biproduct of new\n        registration).\n    :rtype: tuple of :class:`certbot._internal.account.Account` and :class:`acme.client.Client`\n\n    :raises errors.Error: If unable to register an account with ACME server\n\n    \"\"\"\n    def _tos_cb(terms_of_service: str) -> None:\n        if config.tos:\n            return\n        msg = (\"Please read the Terms of Service at: {0}\\n\"\n               \"You must agree in order to register with the ACME \"\n               \"server. Do you agree?\".format(terms_of_service))\n        result = display_util.yesno(msg, cli_flag=\"--agree-tos\", force_interactive=True)\n        if not result:\n            raise errors.Error(\n                \"Registration cannot proceed without accepting \"\n                \"Terms of Service.\")\n\n    account_storage = account.AccountFileStorage(config)\n    acme: Optional[acme_client.ClientV2] = None\n\n    if config.account is not None:\n        acc = account_storage.load(config.account)\n    else:\n        accounts = account_storage.find_all()\n        if len(accounts) > 1:\n            potential_acc = display_ops.choose_account(accounts)\n            if not potential_acc:\n                raise errors.Error(\"No account has been chosen.\")\n            acc = potential_acc\n        elif len(accounts) == 1:\n            acc = accounts[0]\n        else:  # no account registered yet\n            if config.email is None and not config.register_unsafely_without_email:\n                config.email = display_ops.get_email()\n            try:\n                acc, acme = client.register(\n                    config, account_storage, tos_cb=_tos_cb)\n                display_util.notify(\"Account registered.\")\n            except errors.MissingCommandlineFlag:\n                raise\n            except (errors.Error, acme_messages.Error) as err:\n                logger.debug(\"\", exc_info=True)\n                if acme_messages.is_acme_error(err):\n                    err_msg = internal_display_util.describe_acme_error(\n                        cast(acme_messages.Error, err))\n                    err_msg = f\"Error returned by the ACME server: {err_msg}\"\n                else:\n                    err_msg = str(err)\n                raise errors.Error(\n                    f\"Unable to register an account with ACME server. {err_msg}\")\n\n    config.account = acc.id\n    return acc, acme\n\n\ndef _delete_if_appropriate(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Does the user want to delete their now-revoked certs? If run in non-interactive mode,\n    deleting happens automatically.\n\n    :param config: parsed command line arguments\n    :type config: configuration.NamespaceConfig\n\n    :returns: `None`\n    :rtype: None\n\n    :raises errors.Error: If anything goes wrong, including bad user input, if an overlapping\n        archive dir is found for the specified lineage, etc ...\n    \"\"\"\n    attempt_deletion = config.delete_after_revoke\n    if attempt_deletion is None:\n        msg = (\"Would you like to delete the certificate(s) you just revoked, \"\n               \"along with all earlier and later versions of the certificate?\")\n        attempt_deletion = display_util.yesno(msg, yes_label=\"Yes (recommended)\", no_label=\"No\",\n                                              force_interactive=True, default=True)\n\n    if not attempt_deletion:\n        return\n\n    # config.cert_path must have been set\n    # config.certname may have been set\n    assert config.cert_path\n\n    if not config.certname:\n        config.certname = cert_manager.cert_path_to_lineage(config)\n\n    # don't delete if the archive_dir is used by some other lineage\n    archive_dir = storage.full_archive_path(\n            configobj.ConfigObj(\n                storage.renewal_file_for_certname(config, config.certname),\n                encoding='utf-8', default_encoding='utf-8'),\n            config, config.certname)\n    try:\n        cert_manager.match_and_check_overlaps(config, [lambda x: archive_dir],\n                                              lambda x: x.archive_dir, lambda x: x.lineagename)\n    except errors.OverlappingMatchFound:\n        logger.warning(\"Not deleting revoked certificates due to overlapping archive dirs. \"\n                       \"More than one certificate is using %s\", archive_dir)\n        return\n    except Exception as e:\n        msg = ('config.default_archive_dir: {0}, config.live_dir: {1}, archive_dir: {2},'\n               'original exception: {3}')\n        msg = msg.format(config.default_archive_dir, config.live_dir, archive_dir, e)\n        raise errors.Error(msg)\n\n    cert_manager.delete(config)\n\n\ndef _init_le_client(config: configuration.NamespaceConfig,\n                    authenticator: Optional[interfaces.Authenticator],\n                    installer: Optional[interfaces.Installer]) -> client.Client:\n    \"\"\"Initialize Let's Encrypt Client\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param authenticator: Acme authentication handler\n    :type authenticator: Optional[interfaces.Authenticator]\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :returns: client: Client object\n    :rtype: client.Client\n\n    \"\"\"\n    acc: Optional[account.Account]\n    if authenticator is not None:\n        # if authenticator was given, then we will need account...\n        acc, acme = _determine_account(config)\n        logger.debug(\"Picked account: %r\", acc)\n    else:\n        acc, acme = None, None\n\n    return client.Client(config, acc, authenticator, installer, acme=acme)\n\n\ndef unregister(config: configuration.NamespaceConfig,\n               unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Deactivate account on server\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or a string indicating an error\n    :rtype: None or str\n\n    \"\"\"\n    account_storage = account.AccountFileStorage(config)\n    accounts = account_storage.find_all()\n\n    if not accounts:\n        return f\"Could not find existing account for server {config.server}.\"\n    prompt = (\"Are you sure you would like to irrevocably deactivate \"\n              \"your account?\")\n    wants_deactivate = display_util.yesno(prompt, yes_label='Deactivate', no_label='Abort',\n                                          default=True)\n\n    if not wants_deactivate:\n        return \"Deactivation aborted.\"\n\n    acc, acme = _determine_account(config)\n    cb_client = client.Client(config, acc, None, None, acme=acme)\n\n    if not cb_client.acme:\n        raise errors.Error(\"ACME client is not set.\")\n\n    # delete on boulder\n    cb_client.acme.deactivate_registration(acc.regr)\n    account_files = account.AccountFileStorage(config)\n    # delete local account files\n    account_files.delete(config.account)\n\n    display_util.notify(\"Account deactivated.\")\n    return None\n\n\ndef register(config: configuration.NamespaceConfig,\n             unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Create accounts on the server.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or a string indicating an error\n    :rtype: None or str\n\n    \"\"\"\n    # Portion of _determine_account logic to see whether accounts already\n    # exist or not.\n    account_storage = account.AccountFileStorage(config)\n    accounts = account_storage.find_all()\n\n    if accounts:\n        # TODO: add a flag to register a duplicate account (this will\n        #       also require extending _determine_account's behavior\n        #       or else extracting the registration code from there)\n        return (\"There is an existing account; registration of a \"\n                \"duplicate account with this command is currently \"\n                \"unsupported.\")\n    # _determine_account will register an account\n    _determine_account(config)\n    return None\n\n\ndef update_account(config: configuration.NamespaceConfig,\n                   unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Modify accounts on the server.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or a string indicating an error\n    :rtype: None or str\n\n    \"\"\"\n    # Portion of _determine_account logic to see whether accounts already\n    # exist or not.\n    account_storage = account.AccountFileStorage(config)\n    accounts = account_storage.find_all()\n\n    if not accounts:\n        return f\"Could not find an existing account for server {config.server}.\"\n    if config.email is None and not config.register_unsafely_without_email:\n        config.email = display_ops.get_email()\n\n    acc, acme = _determine_account(config)\n    cb_client = client.Client(config, acc, None, None, acme=acme)\n\n    if not cb_client.acme:\n        raise errors.Error(\"ACME client is not set.\")\n\n    # Empty list of contacts in case the user is removing all emails\n    acc_contacts: Iterable[str] = ()\n    if config.email:\n        acc_contacts = ['mailto:' + email for email in config.email.split(',')]\n    # We rely on an exception to interrupt this process if it didn't work.\n    prev_regr_uri = acc.regr.uri\n    acc.regr = cb_client.acme.update_registration(acc.regr.update(\n        body=acc.regr.body.update(contact=acc_contacts)))\n    # A v1 account being used as a v2 account will result in changing the uri to\n    # the v2 uri. Since it's the same object on disk, put it back to the v1 uri\n    # so that we can also continue to use the account object with acmev1.\n    acc.regr = acc.regr.update(uri=prev_regr_uri)\n    account_storage.update_regr(acc)\n\n    if not config.email:\n        display_util.notify(\"Any contact information associated \"\n                            \"with this account has been removed.\")\n    else:\n        eff.prepare_subscription(config, acc)\n        display_util.notify(\"Your e-mail address was updated to {0}.\".format(config.email))\n\n    return None\n\n\ndef show_account(config: configuration.NamespaceConfig,\n                   unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Fetch account info from the ACME server and show it to the user.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or a string indicating an error\n    :rtype: None or str\n\n    \"\"\"\n    # Portion of _determine_account logic to see whether accounts already\n    # exist or not.\n    account_storage = account.AccountFileStorage(config)\n    accounts = account_storage.find_all()\n\n    if not accounts:\n        return f\"Could not find an existing account for server {config.server}.\"\n\n    acc, acme = _determine_account(config)\n    cb_client = client.Client(config, acc, None, None, acme=acme)\n\n    if not cb_client.acme:\n        raise errors.Error(\"ACME client is not set.\")\n\n    regr = cb_client.acme.query_registration(acc.regr)\n    output = [f\"Account details for server {config.server}:\",\n              f\"  Account URL: {regr.uri}\"]\n\n    thumbprint = b64.b64encode(acc.key.thumbprint()).decode()\n    output.append(f\"  Account Thumbprint: {thumbprint}\")\n\n    emails = []\n\n    for contact in regr.body.contact:\n        if contact.startswith('mailto:'):\n            emails.append(contact[7:])\n\n    output.append(\"  Email contact{}: {}\".format(\n                            \"s\" if len(emails) > 1 else \"\",\n                            \", \".join(emails) if len(emails) > 0 else \"none\"))\n\n    display_util.notify(\"\\n\".join(output))\n\n    return None\n\n\ndef _cert_name_from_config_or_lineage(config: configuration.NamespaceConfig,\n                                      lineage: Optional[storage.RenewableCert]) -> Optional[str]:\n    if lineage:\n        return lineage.lineagename\n    elif config.certname:\n        return config.certname\n    try:\n        cert_name = cert_manager.cert_path_to_lineage(config)\n        return cert_name\n    except errors.Error:\n        pass\n\n    return None\n\n\ndef _install_cert(config: configuration.NamespaceConfig, le_client: client.Client,\n                  sans: list[san.SAN], lineage: Optional[storage.RenewableCert] = None) -> None:\n    \"\"\"Install a cert\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param le_client: Client object\n    :type le_client: client.Client\n\n    :param sans: List of domains\n    :type sans: `list` of `str`\n\n    :param lineage: Certificate lineage object. Defaults to `None`\n    :type lineage: storage.RenewableCert\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    path_provider: Union[storage.RenewableCert,\n                         configuration.NamespaceConfig] = lineage if lineage else config\n    assert path_provider.cert_path is not None\n\n    domains, ip_addresses = san.split(sans)\n    if len(ip_addresses) > 0:\n        # Our apache and nginx plugins are currently relying on this check for a user friendly error\n        # message about their lack of support for IP certificates. If you're removing this check,\n        # please check that the plugins can process IP addresses.\n        raise errors.ConfigurationError(\"Enhancements not supported for IP address certificates\")\n\n    le_client.deploy_certificate(domains, path_provider.key_path, path_provider.cert_path,\n                                 path_provider.chain_path, path_provider.fullchain_path)\n\n    le_client.enhance_config(domains, path_provider.chain_path)\n\n\ndef install(config: configuration.NamespaceConfig,\n            plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Install a previously obtained cert in a server.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or the error message\n    :rtype: None or str\n\n    \"\"\"\n    # XXX: Update for renewer/RenewableCert\n    # FIXME: be consistent about whether errors are raised or returned from\n    # this function ...\n\n    try:\n        installer, _ = plug_sel.choose_configurator_plugins(config, plugins, \"install\")\n    except errors.PluginSelectionError as e:\n        return str(e)\n\n    custom_cert = (config.key_path and config.cert_path)\n    if not config.certname and not custom_cert:\n        certname_question = \"Which certificate would you like to install?\"\n        config.certname = cert_manager.get_certnames(\n            config, \"install\", allow_multiple=False,\n            custom_prompt=certname_question)[0]\n\n    if not enhancements.are_supported(config, installer):\n        raise errors.NotSupportedError(\"One or more of the requested enhancements \"\n                                       \"are not supported by the selected installer\")\n    # If cert-path is defined, populate missing (ie. not overridden) values.\n    # Unfortunately this can't be done in argument parser, as certificate\n    # manager needs the access to renewal directory paths\n    if config.certname:\n        config = _populate_from_certname(config)\n    elif enhancements.are_requested(config):\n        # Preflight config check\n        raise errors.ConfigurationError(\"One or more of the requested enhancements \"\n                                        \"require --cert-name to be provided\")\n\n    if config.key_path and config.cert_path:\n        _check_certificate_and_key(config)\n        sans, _ = _find_sans_or_certname(config, installer)\n        le_client = _init_le_client(config, authenticator=None, installer=installer)\n        _install_cert(config, le_client, sans)\n    else:\n        raise errors.ConfigurationError(\"Path to certificate or key was not defined. \"\n            \"If your certificate is managed by Certbot, please use --cert-name \"\n            \"to define which certificate you would like to install.\")\n\n    if enhancements.are_requested(config):\n        # In the case where we don't have certname, we have errored out already\n        lineage = cert_manager.lineage_for_certname(config, config.certname)\n        domains, ip_addresses = san.split(sans)\n        if ip_addresses:\n            raise TypeError(\"enhancements not supported for IP address certificates\")\n        domains_str = [d.dns_name for d in domains]\n        enhancements.enable(lineage, domains_str, installer, config)\n\n    return None\n\n\ndef _populate_from_certname(config: configuration.NamespaceConfig) -> configuration.NamespaceConfig:\n    \"\"\"Helper function for install to populate missing config values from lineage\n    defined by --cert-name.\"\"\"\n\n    lineage = cert_manager.lineage_for_certname(config, config.certname)\n    if not lineage:\n        return config\n    if not config.key_path:\n        config.key_path = lineage.key_path\n    if not config.cert_path:\n        config.cert_path = lineage.cert_path\n    if not config.chain_path:\n        config.chain_path = lineage.chain_path\n    if not config.fullchain_path:\n        config.fullchain_path = lineage.fullchain_path\n    return config\n\n\ndef _check_certificate_and_key(config: configuration.NamespaceConfig) -> None:\n    if not os.path.isfile(filesystem.realpath(config.cert_path)):\n        raise errors.ConfigurationError(\"Error while reading certificate from path \"\n                                        \"{0}\".format(config.cert_path))\n    if not os.path.isfile(filesystem.realpath(config.key_path)):\n        raise errors.ConfigurationError(\"Error while reading private key from path \"\n                                        \"{0}\".format(config.key_path))\n\n\ndef plugins_cmd(config: configuration.NamespaceConfig,\n                plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"List server software plugins.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    logger.debug(\"Expected interfaces: %s\", config.ifaces)\n\n    ifaces = [] if config.ifaces is None else config.ifaces\n    filtered = plugins.visible().ifaces(ifaces)\n    logger.debug(\"Filtered plugins: %r\", filtered)\n\n    notify = functools.partial(display_util.notification, pause=False)\n    if not config.init and not config.prepare:\n        notify(str(filtered))\n        return\n\n    filtered.init(config)\n    logger.debug(\"Filtered plugins: %r\", filtered)\n\n    if not config.prepare:\n        notify(str(filtered))\n        return\n\n    filtered.prepare()\n    available = filtered.available()\n    logger.debug(\"Prepared plugins: %s\", available)\n    notify(str(available))\n\n\ndef enhance(config: configuration.NamespaceConfig,\n            plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Add security enhancements to existing configuration\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or a string indicating an error\n    :rtype: None or str\n\n    \"\"\"\n    supported_enhancements = [\"hsts\", \"redirect\", \"uir\", \"staple\"]\n    # Check that at least one enhancement was requested on command line\n    oldstyle_enh = any(getattr(config, enh) for enh in supported_enhancements)\n    if not enhancements.are_requested(config) and not oldstyle_enh:\n        msg = (\"Please specify one or more enhancement types to configure. To list \"\n               \"the available enhancement types, run:\\n\\n%s --help enhance\\n\")\n        logger.error(msg, cli.cli_command)\n        raise errors.MisconfigurationError(\"No enhancements requested, exiting.\")\n\n    try:\n        installer, _ = plug_sel.choose_configurator_plugins(config, plugins, \"enhance\")\n    except errors.PluginSelectionError as e:\n        return str(e)\n\n    if not enhancements.are_supported(config, installer):\n        raise errors.NotSupportedError(\"One or more of the requested enhancements \"\n                                       \"are not supported by the selected installer\")\n\n    certname_question = (\"Which certificate would you like to use to enhance \"\n                         \"your configuration?\")\n    config.certname = cert_manager.get_certnames(\n        config, \"enhance\", allow_multiple=False,\n        custom_prompt=certname_question)[0]\n    cert_sans = cert_manager.sans_for_certname(config, config.certname)\n    if cert_sans is None:\n        raise errors.Error(\"Could not find the list of domains for the given certificate name.\")\n    cert_domains, ip_addresses = san.split(cert_sans)\n    if len(ip_addresses) > 0:\n        # Our apache and nginx plugins are currently relying on this check for a user friendly error\n        # message about their lack of support for IP certificates. If you're removing this check,\n        # please check that the plugins can process IP addresses.\n        raise errors.ConfigurationError(\"Enhancements not supported for IP address certificates\")\n\n    if config.noninteractive_mode:\n        domains = cert_domains\n    else:\n        domain_question = (\"Which domain names would you like to enable the \"\n                           \"selected enhancements for?\")\n        domain_strs = display_ops.choose_values(list(map(str, cert_domains)), domain_question)\n        if not domain_strs:\n            raise errors.Error(\"User cancelled the domain selection. No domains \"\n                               \"defined, exiting.\")\n        domains = list(map(san.DNSName, domain_strs))\n\n    lineage = cert_manager.lineage_for_certname(config, config.certname)\n    if not lineage:\n        raise errors.Error(\"Could not find the lineage for the given certificate name.\")\n    if not config.chain_path:\n        config.chain_path = lineage.chain_path\n    if oldstyle_enh:\n        le_client = _init_le_client(config, authenticator=None, installer=installer)\n        le_client.enhance_config(domains, config.chain_path, redirect_default=False)\n    if enhancements.are_requested(config):\n        enhancements.enable(lineage, [d.dns_name for d in domains], installer, config)\n\n    return None\n\n\ndef rollback(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Rollback server configuration changes made during install.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    client.rollback(config.installer, config.checkpoints, config, plugins)\n\n\ndef delete(config: configuration.NamespaceConfig,\n           unused_plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Delete a certificate\n\n    Use the information in the config file to delete an existing\n    lineage.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    cert_manager.delete(config)\n\n\ndef certificates(config: configuration.NamespaceConfig,\n                 unused_plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Display information about certs configured with Certbot\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    cert_manager.certificates(config)\n\n\ndef revoke(config: configuration.NamespaceConfig,\n           unused_plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Revoke a previously obtained certificate.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None` or string indicating error in case of error\n    :rtype: None or str\n\n    \"\"\"\n    # For user-agent construction\n    config.installer = config.authenticator = None\n\n    if config.cert_path is None and config.certname:\n        # When revoking via --cert-name, take the cert path and server from renewalparams\n        lineage = storage.RenewableCert(\n            storage.renewal_file_for_certname(config, config.certname), config)\n        config.cert_path = lineage.cert_path\n        # --server takes priority over lineage.server\n        if lineage.server and not config.set_by_user(\"server\"):\n            config.server = lineage.server\n    elif not config.cert_path or (config.cert_path and config.certname):\n        # intentionally not supporting --cert-path & --cert-name together,\n        # to avoid dealing with mismatched values\n        raise errors.Error(\"Error! Exactly one of --cert-path or --cert-name must be specified!\")\n\n    if config.key_path is not None:  # revocation by cert key\n        logger.debug(\"Revoking %s using certificate key %s\",\n                     config.cert_path, config.key_path)\n        crypto_util.verify_cert_matches_priv_key(config.cert_path, config.key_path)\n        with open(config.key_path, 'rb') as f:\n            key = jose.JWK.load(f.read())\n        acme = client.create_acme_client(config, key)\n    else:  # revocation by account key\n        logger.debug(\"Revoking %s using Account Key\", config.cert_path)\n        acc, _ = _determine_account(config)\n        acme = client.create_acme_client(config, acc.key, acc.regr)\n\n    with open(config.cert_path, 'rb') as f:\n        cert = x509.load_pem_x509_certificate(f.read())\n    logger.debug(\"Reason code for revocation: %s\", config.reason)\n    try:\n        acme.revoke(cert, config.reason)\n        _delete_if_appropriate(config)\n    except acme_errors.ClientError as e:\n        return str(e)\n\n    display_ops.success_revocation(config.cert_path)\n    return None\n\n\ndef run(config: configuration.NamespaceConfig,\n        plugins: plugins_disco.PluginsRegistry) -> Optional[str]:\n    \"\"\"Obtain a certificate and install.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    # TODO: Make run as close to auth + install as possible\n    # Possible difficulties: config.csr was hacked into auth\n    try:\n        installer, authenticator = plug_sel.choose_configurator_plugins(config, plugins, \"run\")\n    except errors.PluginSelectionError as e:\n        return str(e)\n\n    if config.must_staple and installer and \"staple-ocsp\" not in installer.supported_enhancements():\n        raise errors.NotSupportedError(\n            \"Must-Staple extension requested, but OCSP stapling is not supported by the selected \"\n            f\"installer ({config.installer})\\n\\n\"\n            \"You can either:\\n\"\n            \" * remove the --must-staple option from the command line and obtain a certificate \"\n            \"without the Must-Staple extension, or;\\n\"\n            \" * use the `certonly` subcommand and manually install the certificate into the  \"\n            \"intended service (e.g. webserver). You must also then manually enable OCSP stapling, \"\n            \"as it is required for certificates with the Must-Staple extension to \"\n            \"function properly.\\n\"\n            \" * choose a different installer plugin (such as --nginx or --apache), if possible.\"\n        )\n\n    # Preflight check for enhancement support by the selected installer\n    if not enhancements.are_supported(config, installer):\n        raise errors.NotSupportedError(\"One or more of the requested enhancements \"\n                                       \"are not supported by the selected installer\")\n\n    # TODO: Handle errors from _init_le_client?\n    le_client = _init_le_client(config, authenticator, installer)\n\n    sans, certname = _find_sans_or_certname(config, installer)\n\n    domains, ip_addresses = san.split(sans)\n    if ip_addresses:\n        raise errors.Error(\"installation of IP address certificate not supported\")\n\n    should_get_cert, lineage = _find_cert(config, sans, certname)\n\n    new_lineage = lineage\n    if should_get_cert:\n        new_lineage = _get_and_save_cert(le_client, config, sans,\n            certname, lineage)\n\n    cert_path = new_lineage.cert_path if new_lineage else None\n    fullchain_path = new_lineage.fullchain_path if new_lineage else None\n    key_path = new_lineage.key_path if new_lineage else None\n\n    if should_get_cert:\n        _report_new_cert(config, cert_path, fullchain_path, key_path)\n\n    # The installer error, if any, is being stored as a value here, in order to first print\n    # relevant advice in a nice way, before re-raising the error for normal processing.\n    installer_err: Optional[errors.Error] = None\n    try:\n        _install_cert(config, le_client, sans, new_lineage)\n\n        if enhancements.are_requested(config) and new_lineage:\n            enhancements.enable(new_lineage, [d.dns_name for d in domains], installer, config)\n\n        sans_strs = list(map(str, sans))\n        if lineage is None or not should_get_cert:\n            display_ops.success_installation(sans_strs)\n        else:\n            display_ops.success_renewal(sans_strs)\n    except errors.Error as e:\n        installer_err = e\n    finally:\n        _report_next_steps(config, installer_err, new_lineage,\n                           new_or_renewed_cert=should_get_cert)\n        # If the installer did fail, re-raise the error to bail out\n        if installer_err:\n            raise installer_err\n\n    _suggest_donation_if_appropriate(config)\n    eff.handle_subscription(config, le_client.account)\n    return None\n\n\ndef _csr_get_and_save_cert(config: configuration.NamespaceConfig,\n                           le_client: client.Client) -> tuple[\n                           Optional[str], Optional[str], Optional[str]]:\n    \"\"\"Obtain a cert using a user-supplied CSR\n\n    This works differently in the CSR case (for now) because we don't\n    have the privkey, and therefore can't construct the files for a lineage.\n    So we just save the cert & chain to disk :/\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param client: Client object\n    :type client: client.Client\n\n    :returns: `cert_path`, `chain_path` and `fullchain_path` as absolute\n              paths to the actual files, or None for each if it's a dry-run.\n    :rtype: `tuple` of `str`\n\n    \"\"\"\n    util_csr = config.actual_csr\n    x509_req = x509.load_pem_x509_csr(util_csr.data)\n    domains, ip_addresses = san.from_x509(x509_req.subject, x509_req.extensions)\n    display_util.notify(\n        \"{action} for {sans}\".format(\n            action=\"Simulating a certificate request\" if config.dry_run else\n                    \"Requesting a certificate\",\n            sans=internal_display_util.summarize_sans(san.join(domains, ip_addresses))\n        )\n    )\n    cert, chain = le_client.obtain_certificate_from_csr(util_csr)\n    if config.dry_run:\n        logger.debug(\n            \"Dry run: skipping saving certificate to %s\", config.cert_path)\n        return None, None, None\n    cert_path, chain_path, fullchain_path = le_client.save_certificate(\n        cert, chain, os.path.normpath(config.cert_path),\n        os.path.normpath(config.chain_path), os.path.normpath(config.fullchain_path))\n    return cert_path, chain_path, fullchain_path\n\n\ndef renew_cert(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry,\n               lineage: storage.RenewableCert) -> None:\n    \"\"\"Renew & save an existing cert. Do not install it.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :returns: `None`\n    :rtype: None\n\n    :raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass\n\n    \"\"\"\n    # installers are used in auth mode to determine domain names\n    installer, auth = plug_sel.choose_configurator_plugins(config, plugins, \"certonly\")\n    le_client = _init_le_client(config, auth, installer)\n\n    renewed_lineage = _get_and_save_cert(le_client, config, lineage=lineage)\n\n    if not renewed_lineage:\n        raise errors.Error(\"An existing certificate for the given name could not be found.\")\n\n    if installer and not config.dry_run:\n        # In case of a renewal, reload server to pick up new certificate.\n        updater.run_renewal_deployer(config, renewed_lineage, installer)\n        display_util.notify(f\"Reloading {config.installer} server after certificate renewal\")\n        installer.restart()\n\n\ndef certonly(config: configuration.NamespaceConfig, plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Authenticate & obtain cert, but do not install it.\n\n    This implements the 'certonly' subcommand.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    :raises errors.Error: If specified plugin could not be used\n\n    \"\"\"\n    # SETUP: Select plugins and construct a client instance\n    # installers are used in auth mode to determine domain names\n    installer, auth = plug_sel.choose_configurator_plugins(config, plugins, \"certonly\")\n    le_client = _init_le_client(config, auth, installer)\n\n    if config.csr:\n        cert_path, chain_path, fullchain_path = _csr_get_and_save_cert(config, le_client)\n        _csr_report_new_cert(config, cert_path, chain_path, fullchain_path)\n        _report_next_steps(config, None, None, new_or_renewed_cert=not config.dry_run)\n        _suggest_donation_if_appropriate(config)\n        eff.handle_subscription(config, le_client.account)\n        return\n\n    sans, certname = _find_sans_or_certname(config, installer)\n    should_get_cert, lineage = _find_cert(config, sans, certname)\n\n    if not should_get_cert:\n        display_util.notification(\"Certificate not yet due for renewal; no action taken.\",\n                                     pause=False)\n        return\n\n    lineage = _get_and_save_cert(le_client, config, sans, certname, lineage)\n\n    # If a new cert was issued and we were passed an installer, we can safely\n    # run `installer.restart()` to load the newly issued certificate\n    installer_err: Optional[errors.Error] = None\n    if lineage and installer and not config.dry_run:\n        logger.info(\"Reloading %s server after certificate issuance\", config.installer)\n        try:\n            installer.restart()\n        except errors.Error as e:\n            installer_err = e\n\n    cert_path = lineage.cert_path if lineage else None\n    fullchain_path = lineage.fullchain_path if lineage else None\n    key_path = lineage.key_path if lineage else None\n    _report_new_cert(config, cert_path, fullchain_path, key_path)\n    _report_next_steps(config, installer_err, lineage,\n                       new_or_renewed_cert=should_get_cert and not config.dry_run)\n    if installer_err:\n        raise installer_err\n    _suggest_donation_if_appropriate(config)\n    eff.handle_subscription(config, le_client.account)\n\n\ndef renew(config: configuration.NamespaceConfig,\n          unused_plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Renew previously-obtained certificates.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param unused_plugins: List of plugins (deprecated)\n    :type unused_plugins: plugins_disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    renewal.handle_renewal_request(config)\n\n\ndef make_or_verify_needed_dirs(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Create or verify existence of config, work, and hook directories.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions)\n\n    # Ensure the working directory has the expected mode, even under stricter umask settings\n    with filesystem.temp_umask(0o022):\n        util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE, config.strict_permissions)\n\n    hook_dirs = (config.renewal_pre_hooks_dir,\n                 config.renewal_deploy_hooks_dir,\n                 config.renewal_post_hooks_dir,)\n    for hook_dir in hook_dirs:\n        util.make_or_verify_dir(hook_dir, strict=config.strict_permissions)\n\n\ndef _report_reconfigure_results(renewal_file: str, orig_renewal_conf: configobj.ConfigObj) -> None:\n    \"\"\"Reports the outcome of certificate renewal reconfiguration to the user.\n\n    :param renewal_file: Path to the cert's renewal file\n    :type renewal_file: str\n\n    :param orig_renewal_conf: Loaded original renewal configuration\n    :type orig_renewal_conf: configobj.ConfigObj\n\n    :returns: `None`\n    :rtype: None\n\n    \"\"\"\n    try:\n        final_renewal_conf = configobj.ConfigObj(\n            renewal_file, encoding='utf-8', default_encoding='utf-8')\n    except configobj.ConfigObjError:\n        raise errors.CertStorageError(\n            f'error parsing {renewal_file}')\n\n    orig_renewal_params = orig_renewal_conf['renewalparams']\n    final_renewal_params = final_renewal_conf['renewalparams']\n\n    if final_renewal_params == orig_renewal_params:\n        success_message = '\\nNo changes were made to the renewal configuration.'\n    else:\n        success_message = '\\nSuccessfully updated configuration.' + \\\n                          '\\nChanges will apply when the certificate renews.'\n\n    display_util.notify(success_message)\n\n\ndef reconfigure(config: configuration.NamespaceConfig,\n          plugins: plugins_disco.PluginsRegistry) -> None:\n    \"\"\"Allow the user to set new configuration options for an existing certificate without\n       forcing renewal. This can be used for things like authenticator, installer, and hooks,\n       but not for the domains on the cert, since those are only saved in the cert.\n\n    :param config: Configuration object\n    :type config: configuration.NamespaceConfig\n\n    :param plugins: List of plugins\n    :type plugins: plugins_disco.PluginsRegistry\n\n    :raises errors.Error: if the dry run fails\n    :raises errors.ConfigurationError: if certificate could not be loaded\n\n    \"\"\"\n\n    if config.domains:\n        raise errors.ConfigurationError(\"You have specified domains, but this function cannot \"\n            \"be used to modify the domains in a certificate. If you would like to do so, follow \"\n            \"the instructions at https://certbot.org/change-cert-domain. Otherwise, remove the \"\n            \"domains from the command to continue reconfiguring. You can specify which certificate \"\n            \"you want on the command line with flag --cert-name instead.\")\n    # While we could technically allow domains to be used to specify the certificate in addition to\n    # --cert-name, there's enough complexity with matching certs to domains that it's not worth it,\n    # to say nothing of the difficulty in explaining what exactly this subcommand can modify\n\n\n    # To make sure that the requested changes work, we're going to do a dry run, and only save\n    # upon success. First, modify the config as the user requested.\n    if not config.certname:\n        certname_question = \"Which certificate would you like to reconfigure?\"\n        config.certname = cert_manager.get_certnames(\n            config, \"reconfigure\", allow_multiple=False,\n            custom_prompt=certname_question)[0]\n\n    certname = config.certname\n\n    try:\n        renewal_file = storage.renewal_file_for_certname(config, certname)\n    except errors.CertStorageError:\n        raise errors.ConfigurationError(f\"An existing certificate with name {certname} could not \"\n            \"be found. Run `certbot certificates` to list available certificates.\")\n\n    # figure this out before we modify config\n    if config.deploy_hook and not config.run_deploy_hooks:\n        msg = (\"You are attempting to set a --deploy-hook. Would you like Certbot to run deploy \"\n               \"hooks when it performs a dry run with the new settings? This will run all \"\n               \"relevant deploy hooks, including directory hooks, unless --no-directory-hooks \"\n               \"is set. This will use the current active certificate, and not the temporary test \"\n               \"certificate acquired during the dry run.\")\n        config.run_deploy_hooks = display_util.yesno(msg,\"Run deploy hooks\",\n            \"Do not run deploy hooks\", default=False)\n\n    # cache previous version for later comparison\n    try:\n        orig_renewal_conf = configobj.ConfigObj(\n            renewal_file, encoding='utf-8', default_encoding='utf-8')\n    except configobj.ConfigObjError:\n        raise errors.CertStorageError(\n            f\"error parsing {renewal_file}\")\n\n    lineage_config = copy.deepcopy(config)\n    try:\n        renewal_candidate = renewal.reconstitute(lineage_config, renewal_file)\n    except Exception as e:  # pylint: disable=broad-except\n        raise errors.ConfigurationError(f\"Renewal configuration file {renewal_file} \"\n            f\"(cert: {certname}) produced an unexpected error: {e}.\")\n    if not renewal_candidate:\n        raise errors.ConfigurationError(\"Could not load certificate. See logs for errors.\")\n\n    renewalparams = orig_renewal_conf['renewalparams']\n    # If server was set but hasn't changed and no account is loaded,\n    # load the old account because reconstitute won't have\n    if lineage_config.set_by_user('server') and lineage_config.server == renewalparams['server']\\\n        and lineage_config.account is None:\n        lineage_config.account = renewalparams['account']\n    for param in ('account', 'server',):\n        if getattr(lineage_config, param) != renewalparams.get(param):\n            msg = (\"Using reconfigure to change the ACME account or server is not supported. \"\n                   \"If you would like to do so, use renew with the --force-renewal flag instead \"\n                   \"of reconfigure. Note that doing so will count against any rate limits. For \"\n                   \"more information on this method, see \"\n                   \"https://certbot.org/renew-reconfiguration\")\n            raise errors.ConfigurationError(msg)\n\n    # this is where lineage_config gets fully filled out (e.g. --apache will set auth and installer)\n    installer, auth = plug_sel.choose_configurator_plugins(lineage_config, plugins, \"certonly\")\n\n    # make a deep copy of lineage_config because we're about to modify it for a test dry run\n    dry_run_lineage_config = copy.deepcopy(lineage_config)\n\n    # we also set noninteractive_mode to more accurately simulate renewal (since `certbot renew`\n    # implies noninteractive mode) and to avoid prompting the user as changes made to\n    # dry_run_lineage_config beyond this point will not be applied to the original lineage_config\n    dry_run_lineage_config.noninteractive_mode = True\n    dry_run_lineage_config.dry_run = True\n    cli.set_test_server_options(\"reconfigure\", dry_run_lineage_config)\n\n    le_client = _init_le_client(dry_run_lineage_config, auth, installer)\n\n    # renews cert as dry run to test that the new values are ok\n    # at this point, renewal_candidate.configuration has the old values, but will use\n    # the values from lineage_config when doing the dry run\n    _get_and_save_cert(le_client, dry_run_lineage_config, certname=certname,\n        lineage=renewal_candidate)\n\n    # this function will update lineage.configuration with the new values, and save it to disk\n    # use the pre-dry-run version\n    renewal_candidate.save_new_config_values(lineage_config)\n\n    _report_reconfigure_results(renewal_file, orig_renewal_conf)\n\n\n@contextmanager\ndef make_displayer(config: configuration.NamespaceConfig\n                   ) -> Generator[Union[display_obj.NoninteractiveDisplay,\n                                        display_obj.FileDisplay], None, None]:\n    \"\"\"Creates a display object appropriate to the flags in the supplied config.\n\n    :param config: Configuration object\n\n    :returns: Display object\n\n    \"\"\"\n    displayer: Union[None, display_obj.NoninteractiveDisplay,\n                     display_obj.FileDisplay] = None\n    devnull: Optional[IO] = None\n\n    if config.quiet:\n        config.noninteractive_mode = True\n        devnull = open(os.devnull, \"w\")  # pylint: disable=consider-using-with\n        displayer = display_obj.NoninteractiveDisplay(devnull)\n    elif config.noninteractive_mode:\n        displayer = display_obj.NoninteractiveDisplay(sys.stdout)\n    else:\n        displayer = display_obj.FileDisplay(\n            sys.stdout, config.force_interactive)\n\n    try:\n        yield displayer\n    finally:\n        if devnull:\n            devnull.close()\n\n\ndef main(cli_args: Optional[list[str]] = None) -> Optional[Union[str, int]]:\n    \"\"\"Run Certbot.\n\n    :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]``\n    :type cli_args: `list` of `str`\n\n    :returns: value for `sys.exit` about the exit status of Certbot\n    :rtype: `str` or `int` or `None`\n\n    \"\"\"\n    if not cli_args:\n        cli_args = sys.argv[1:]\n\n    log.pre_arg_parse_setup()\n\n    if os.environ.get('CERTBOT_SNAPPED') == 'True':\n        cli_args = snap_config.prepare_env(cli_args)\n\n    plugins = plugins_disco.PluginsRegistry.find_all()\n    logger.debug(\"certbot version: %s\", certbot.__version__)\n    logger.debug(\"Location of certbot entry point: %s\", sys.argv[0])\n    # do not log `config`, as it contains sensitive data (e.g. revoke --key)!\n    logger.debug(\"Arguments: %r\", cli_args)\n    logger.debug(\"Discovered plugins: %r\", plugins)\n\n    # Some releases of Windows require escape sequences to be enable explicitly\n    misc.prepare_virtual_console()\n\n    # note: arg parser internally handles --help (and exits afterwards)\n    config = cli.prepare_and_parse_args(plugins, cli_args)\n\n    # On windows, shell without administrative right cannot create symlinks required by certbot.\n    # So we check the rights before continuing.\n    misc.raise_for_non_administrative_windows_rights()\n\n    try:\n        log.post_arg_parse_setup(config)\n        make_or_verify_needed_dirs(config)\n    except errors.Error:\n        # Let plugins_cmd be run as un-privileged user.\n        if config.func != plugins_cmd:  # pylint: disable=comparison-with-callable\n            raise\n\n    with make_displayer(config) as displayer:\n        display_obj.set_display(displayer)\n\n        return config.func(config, plugins)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/ocsp.py",
    "content": "\"\"\"Tools for checking certificate revocation.\"\"\"\nfrom datetime import datetime\nfrom datetime import timedelta\nfrom datetime import timezone\nimport logging\nfrom typing import Optional\n\nfrom cryptography import x509\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.exceptions import UnsupportedAlgorithm\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.x509 import ocsp\nimport requests\n\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot.interfaces import RenewableCert\n\nlogger = logging.getLogger(__name__)\n\n\nclass RevocationChecker:\n    \"\"\"This class figures out OCSP checking on this system, and performs it.\"\"\"\n    def ocsp_revoked(self, cert: RenewableCert) -> bool:\n        \"\"\"Get revoked status for a particular cert version.\n\n        .. todo:: Make this a non-blocking call\n\n        :param `.interfaces.RenewableCert` cert: Certificate object\n        :returns: True if revoked; False if valid or the check failed or cert is expired.\n        :rtype: bool\n\n        \"\"\"\n        return self.ocsp_revoked_by_paths(cert.cert_path, cert.chain_path)\n\n    def ocsp_revoked_by_paths(self, cert_path: str, chain_path: str, timeout: int = 10) -> bool:\n        \"\"\"Performs the OCSP revocation check\n\n        :param str cert_path: Certificate filepath\n        :param str chain_path: Certificate chain\n        :param int timeout: Timeout (in seconds) for the OCSP query\n\n        :returns: True if revoked; False if valid or the check failed or cert is expired.\n        :rtype: bool\n\n        \"\"\"\n        # Let's Encrypt doesn't update OCSP for expired certificates,\n        # so don't check OCSP if the cert is expired.\n        # https://github.com/certbot/certbot/issues/7152\n        now = datetime.now(timezone.utc)\n        if crypto_util.notAfter(cert_path) <= now:\n            return False\n\n        url, host = _determine_ocsp_server(cert_path)\n        if not host or not url:\n            return False\n\n        return _check_ocsp_cryptography(cert_path, chain_path, url, timeout)\n\n\ndef _determine_ocsp_server(cert_path: str) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Extract the OCSP server host from a certificate.\n\n    :param str cert_path: Path to the cert we're checking OCSP for\n    :rtype tuple:\n    :returns: (OCSP server URL or None, OCSP server host or None)\n\n    \"\"\"\n    with open(cert_path, 'rb') as file_handler:\n        cert = x509.load_pem_x509_certificate(file_handler.read(), default_backend())\n    try:\n        extension = cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)\n        ocsp_oid = x509.AuthorityInformationAccessOID.OCSP\n        descriptions = [description for description in extension.value\n                        if description.access_method == ocsp_oid]\n\n        url = descriptions[0].access_location.value\n    except (x509.ExtensionNotFound, IndexError):\n        logger.info(\"Cannot extract OCSP URI from %s\", cert_path)\n        return None, None\n\n    url = url.rstrip()\n    host = url.partition(\"://\")[2].rstrip(\"/\")\n\n    if host:\n        return url, host\n    logger.info(\"Cannot process OCSP host from URL (%s) in certificate at %s\", url, cert_path)\n    return None, None\n\n\ndef _check_ocsp_cryptography(cert_path: str, chain_path: str, url: str, timeout: int) -> bool:\n    # Retrieve OCSP response\n    with open(chain_path, 'rb') as file_handler:\n        issuer = x509.load_pem_x509_certificate(file_handler.read(), default_backend())\n    with open(cert_path, 'rb') as file_handler:\n        cert = x509.load_pem_x509_certificate(file_handler.read(), default_backend())\n    builder = ocsp.OCSPRequestBuilder()\n    builder = builder.add_certificate(cert, issuer, hashes.SHA1())\n    request = builder.build()\n    request_binary = request.public_bytes(serialization.Encoding.DER)\n    try:\n        response = requests.post(url, data=request_binary,\n                                 headers={'Content-Type': 'application/ocsp-request'},\n                                 timeout=timeout)\n    except requests.exceptions.RequestException:\n        logger.info(\"OCSP check failed for %s (are we offline?)\", cert_path, exc_info=True)\n        return False\n    if response.status_code != 200:\n        logger.info(\"OCSP check failed for %s (HTTP status: %d)\", cert_path, response.status_code)\n        return False\n\n    response_ocsp = ocsp.load_der_ocsp_response(response.content)\n\n    # Check OCSP response validity\n    if response_ocsp.response_status != ocsp.OCSPResponseStatus.SUCCESSFUL:\n        logger.warning(\"Invalid OCSP response status for %s: %s\",\n                     cert_path, response_ocsp.response_status)\n        return False\n\n    # Check OCSP signature\n    try:\n        _check_ocsp_response(response_ocsp, request, issuer, cert_path)\n    except UnsupportedAlgorithm as e:\n        logger.warning(str(e))\n    except errors.Error as e:\n        logger.warning(str(e))\n    except InvalidSignature:\n        logger.warning('Invalid signature on OCSP response for %s', cert_path)\n    except AssertionError as error:\n        logger.warning('Invalid OCSP response for %s: %s.', cert_path, str(error))\n    else:\n        # Check OCSP certificate status\n        logger.debug(\"OCSP certificate status for %s is: %s\",\n                     cert_path, response_ocsp.certificate_status)\n        return response_ocsp.certificate_status == ocsp.OCSPCertStatus.REVOKED\n\n    return False\n\n\ndef _check_ocsp_response(response_ocsp: 'ocsp.OCSPResponse', request_ocsp: 'ocsp.OCSPRequest',\n                         issuer_cert: x509.Certificate, cert_path: str) -> None:\n    \"\"\"Verify that the OCSP is valid for several criteria\"\"\"\n    # Assert OCSP response corresponds to the certificate we are talking about\n    if response_ocsp.serial_number != request_ocsp.serial_number:\n        raise AssertionError('the certificate in response does not correspond '\n                             'to the certificate in request')\n\n    # Assert signature is valid\n    _check_ocsp_response_signature(response_ocsp, issuer_cert, cert_path)\n\n    # Assert issuer in response is the expected one\n    if (not isinstance(response_ocsp.hash_algorithm, type(request_ocsp.hash_algorithm))\n            or response_ocsp.issuer_key_hash != request_ocsp.issuer_key_hash\n            or response_ocsp.issuer_name_hash != request_ocsp.issuer_name_hash):\n        raise AssertionError('the issuer does not correspond to issuer of the certificate.')\n\n    # In following checks, two situations can occur:\n    #   * nextUpdate is set, and requirement is thisUpdate < now < nextUpdate\n    #   * nextUpdate is not set, and requirement is thisUpdate < now\n    # NB1: We add a validity period tolerance to handle clock time inconsistencies,\n    #      value is 5 min like for OpenSSL.\n    # NB2: Another check is to verify that thisUpdate is not too old, it is optional\n    #      for OpenSSL, so we do not do it here.\n    # See OpenSSL implementation as a reference:\n    # https://github.com/openssl/openssl/blob/ef45aa14c5af024fcb8bef1c9007f3d1c115bd85/crypto/ocsp/ocsp_cl.c#L338-L391\n    # thisUpdate/nextUpdate are expressed in UTC/GMT time zone\n    now = datetime.now(timezone.utc)\n    if not response_ocsp.this_update_utc:\n        raise AssertionError('param thisUpdate is not set.')\n    if response_ocsp.this_update_utc > now + timedelta(minutes=5):\n        raise AssertionError('param thisUpdate is in the future.')\n    if response_ocsp.next_update_utc and response_ocsp.next_update_utc < now - timedelta(minutes=5):\n        raise AssertionError('param nextUpdate is in the past.')\n\n\ndef _check_ocsp_response_signature(response_ocsp: 'ocsp.OCSPResponse',\n                                   issuer_cert: x509.Certificate, cert_path: str) -> None:\n    \"\"\"Verify an OCSP response signature against certificate issuer or responder\"\"\"\n    def _key_hash(cert: x509.Certificate) -> bytes:\n        return x509.SubjectKeyIdentifier.from_public_key(cert.public_key()).digest\n\n    if (response_ocsp.responder_name == issuer_cert.subject\n            or response_ocsp.responder_key_hash == _key_hash(issuer_cert)):\n        # Case where the OCSP responder is also the certificate issuer\n        logger.debug('OCSP response for certificate %s is signed by the certificate\\'s issuer.',\n                     cert_path)\n        responder_cert = issuer_cert\n    else:\n        # Case where the OCSP responder is not the certificate issuer\n        logger.debug('OCSP response for certificate %s is delegated to an external responder.',\n                     cert_path)\n\n        responder_certs = [cert for cert in response_ocsp.certificates\n                           if response_ocsp.responder_name == cert.subject or \\\n                              response_ocsp.responder_key_hash == _key_hash(cert)]\n        if not responder_certs:\n            raise AssertionError('no matching responder certificate could be found')\n\n        # We suppose here that the ACME server support only one certificate in the OCSP status\n        # request. This is currently the case for LetsEncrypt servers.\n        # See https://github.com/letsencrypt/boulder/issues/2331\n        responder_cert = responder_certs[0]\n\n        if responder_cert.issuer != issuer_cert.subject:\n            raise AssertionError('responder certificate is not signed '\n                                 'by the certificate\\'s issuer')\n\n        try:\n            extension = responder_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)\n            delegate_authorized = x509.oid.ExtendedKeyUsageOID.OCSP_SIGNING in extension.value\n        except (x509.ExtensionNotFound, IndexError):\n            delegate_authorized = False\n        if not delegate_authorized:\n            raise AssertionError('responder is not authorized by issuer to sign OCSP responses')\n\n        # Following line may raise UnsupportedAlgorithm\n        chosen_cert_hash = responder_cert.signature_hash_algorithm\n        assert chosen_cert_hash # always present for RSA and ECDSA certificates.\n        # For a delegate OCSP responder, we need first check that its certificate is effectively\n        # signed by the certificate issuer.\n        crypto_util.verify_signed_payload(issuer_cert.public_key(), responder_cert.signature,\n                                          responder_cert.tbs_certificate_bytes, chosen_cert_hash)\n\n    # Following line may raise UnsupportedAlgorithm\n    chosen_response_hash = response_ocsp.signature_hash_algorithm\n    # We check that the OSCP response is effectively signed by the responder\n    # (an authorized delegate one or the certificate issuer itself).\n    if not chosen_response_hash:\n        raise AssertionError(\"no signature hash algorithm defined\")\n    crypto_util.verify_signed_payload(responder_cert.public_key(), response_ocsp.signature,\n                                      response_ocsp.tbs_response_bytes, chosen_response_hash)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/__init__.py",
    "content": "\"\"\"Certbot plugins.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/disco.py",
    "content": "\"\"\"Utilities for plugins discovery and selection.\"\"\"\nimport logging\nimport sys\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Iterator\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal import constants\nfrom certbot.compat import os\nfrom certbot.errors import Error\n\nif sys.version_info >= (3, 10):  # pragma: no cover\n    import importlib.metadata as importlib_metadata\nelse:\n    import importlib_metadata\n\nlogger = logging.getLogger(__name__)\n\n\nPLUGIN_INTERFACES = [interfaces.Authenticator, interfaces.Installer, interfaces.Plugin]\n\"\"\"Interfaces that should be listed in `certbot plugins` output\"\"\"\n\n\nclass PluginEntryPoint:\n    \"\"\"Plugin entry point.\"\"\"\n\n    # this object is mutable, don't allow it to be hashed!\n    __hash__ = None  # type: ignore\n\n    def __init__(self, entry_point: importlib_metadata.EntryPoint) -> None:\n        self.name = self.entry_point_to_plugin_name(entry_point)\n        self.plugin_cls: type[interfaces.Plugin] = entry_point.load()\n        self.entry_point = entry_point\n        self.warning_message: Optional[str] = None\n        self._initialized: Optional[interfaces.Plugin] = None\n        self._prepared: Optional[Union[bool, Error]] = None\n\n    def check_name(self, name: Optional[str]) -> bool:\n        \"\"\"Check if the name refers to this plugin.\"\"\"\n        if name == self.name:\n            return True\n        return False\n\n    @classmethod\n    def entry_point_to_plugin_name(cls, entry_point: importlib_metadata.EntryPoint) -> str:\n        \"\"\"Unique plugin name for an ``entry_point``\"\"\"\n        return entry_point.name\n\n    @property\n    def description(self) -> str:\n        \"\"\"Description of the plugin.\"\"\"\n        return self.plugin_cls.description\n\n    @property\n    def description_with_name(self) -> str:\n        \"\"\"Description with name. Handy for UI.\"\"\"\n        return \"{0} ({1})\".format(self.description, self.name)\n\n    @property\n    def long_description(self) -> str:\n        \"\"\"Long description of the plugin.\"\"\"\n        return getattr(self.plugin_cls, \"long_description\", self.description)\n\n    @property\n    def hidden(self) -> bool:\n        \"\"\"Should this plugin be hidden from UI?\"\"\"\n        return getattr(self.plugin_cls, \"hidden\", False)\n\n    def ifaces(self, *ifaces_groups: Iterable[type]) -> bool:\n        \"\"\"Does plugin implement specified interface groups?\"\"\"\n        return not ifaces_groups or any(\n            all(issubclass(self.plugin_cls, iface)\n                for iface in ifaces)\n            for ifaces in ifaces_groups)\n\n    @property\n    def initialized(self) -> bool:\n        \"\"\"Has the plugin been initialized already?\"\"\"\n        return self._initialized is not None\n\n    def init(self, config: Optional[configuration.NamespaceConfig] = None) -> interfaces.Plugin:\n        \"\"\"Memoized plugin initialization.\"\"\"\n        if not self._initialized:\n            # For plugins implementing ABCs Plugin, Authenticator or Installer, the following\n            # line will raise an exception if some implementations of abstract methods are missing.\n            self._initialized = self.plugin_cls(config, self.name)\n        return self._initialized\n\n    @property\n    def prepared(self) -> bool:\n        \"\"\"Has the plugin been prepared already?\"\"\"\n        if not self.initialized:\n            logger.debug(\".prepared called on uninitialized %r\", self)\n        return self._prepared is not None\n\n    def prepare(self) -> Union[bool, Error]:\n        \"\"\"Memoized plugin preparation.\"\"\"\n        if self._initialized is None:\n            raise ValueError(\"Plugin is not initialized.\")\n        if self._prepared is None:\n            try:\n                self._initialized.prepare()\n            except errors.MisconfigurationError as error:\n                logger.debug(\"Misconfigured %r: %s\", self, error, exc_info=True)\n                self._prepared = error\n            except errors.NoInstallationError as error:\n                logger.debug(\n                    \"No installation (%r): %s\", self, error, exc_info=True)\n                self._prepared = error\n            except errors.PluginError as error:\n                logger.debug(\"Other error:(%r): %s\", self, error, exc_info=True)\n                self._prepared = error\n            else:\n                self._prepared = True\n        # Mypy seems to fail to understand the actual type here, let's help it.\n        return cast(Union[bool, Error], self._prepared)\n\n    @property\n    def misconfigured(self) -> bool:\n        \"\"\"Is plugin misconfigured?\"\"\"\n        return isinstance(self._prepared, errors.MisconfigurationError)\n\n    @property\n    def problem(self) -> Optional[Exception]:\n        \"\"\"Return the Exception raised during plugin setup, or None if all is well\"\"\"\n        if isinstance(self._prepared, Exception):\n            return self._prepared\n        return None\n\n    @property\n    def available(self) -> bool:\n        \"\"\"Is plugin available, i.e. prepared or misconfigured?\"\"\"\n        return self._prepared is True or self.misconfigured\n\n    def __repr__(self) -> str:\n        return \"PluginEntryPoint#{0}\".format(self.name)\n\n    def __str__(self) -> str:\n        lines = [\n            \"* {0}\".format(self.name),\n            \"Description: {0}\".format(self.plugin_cls.description),\n            \"Interfaces: {0}\".format(\", \".join(\n                iface.__name__ for iface in PLUGIN_INTERFACES\n                if issubclass(self.plugin_cls, iface)\n            )),\n            \"Entry point: {0}\".format(self.entry_point),\n        ]\n\n        if self.initialized:\n            lines.append(\"Initialized: {0}\".format(self.init()))\n            if self.prepared:\n                lines.append(\"Prep: {0}\".format(self.prepare()))\n\n        return \"\\n\".join(lines)\n\n\nclass PluginsRegistry(Mapping):\n    \"\"\"Plugins registry.\"\"\"\n\n    def __init__(self, plugins: Mapping[str, PluginEntryPoint]) -> None:\n        # plugins are sorted so the same order is used between runs.\n        # This prevents deadlock caused by plugins acquiring a lock\n        # and ensures at least one concurrent Certbot instance will run\n        # successfully.\n        self._plugins = dict(sorted(plugins.items()))\n\n    @classmethod\n    def find_all(cls) -> 'PluginsRegistry':\n        \"\"\"Find plugins using Python package entry points.\n\n        See https://packaging.python.org/en/latest/specifications/entry-points/ for more info on\n        entry points.\n\n        \"\"\"\n        plugins: dict[str, PluginEntryPoint] = {}\n        plugin_paths_string = os.getenv('CERTBOT_PLUGIN_PATH')\n        plugin_paths = plugin_paths_string.split(':') if plugin_paths_string else []\n        # XXX should ensure this only happens once\n        sys.path.extend(plugin_paths)\n        entry_points = list(importlib_metadata.entry_points(  # pylint: disable=unexpected-keyword-arg\n            group=constants.SETUPTOOLS_PLUGINS_ENTRY_POINT))\n        old_entry_points = list(importlib_metadata.entry_points(  # pylint: disable=unexpected-keyword-arg\n            group=constants.OLD_SETUPTOOLS_PLUGINS_ENTRY_POINT))\n        for entry_point in entry_points + old_entry_points:\n            try:\n                cls._load_entry_point(entry_point, plugins)\n            except Exception as e:\n                raise errors.PluginError(\n                    f\"The '{entry_point.module}' plugin errored while loading: {e}. \"\n                    \"You may need to remove or update this plugin. The Certbot log will \"\n                    \"contain the full error details and this should be reported to the \"\n                    \"plugin developer.\") from e\n        return cls(plugins)\n\n    @classmethod\n    def _load_entry_point(cls, entry_point: importlib_metadata.EntryPoint,\n                          plugins: dict[str, PluginEntryPoint]) -> None:\n        plugin_ep = PluginEntryPoint(entry_point)\n        if plugin_ep.name in plugins:\n            other_ep = plugins[plugin_ep.name]\n            plugin1_dist = plugin_ep.entry_point.dist\n            plugin2_dist = other_ep.entry_point.dist\n            plugin1 = plugin1_dist.name.lower() if plugin1_dist else \"unknown\"\n            plugin2 = plugin2_dist.name.lower() if plugin2_dist else \"unknown\"\n            # pylint: disable=broad-exception-raised\n            raise Exception(\"Duplicate plugin name {0} from {1} and {2}.\".format(\n                plugin_ep.name, plugin1, plugin2))\n        if issubclass(plugin_ep.plugin_cls, interfaces.Plugin):\n            plugins[plugin_ep.name] = plugin_ep\n        else:  # pragma: no cover\n            logger.warning(\n                \"%r does not inherit from Plugin, skipping\", plugin_ep)\n\n    def __getitem__(self, name: str) -> PluginEntryPoint:\n        return self._plugins[name]\n\n    def __iter__(self) -> Iterator[str]:\n        return iter(self._plugins)\n\n    def __len__(self) -> int:\n        return len(self._plugins)\n\n    def init(self, config: configuration.NamespaceConfig) -> list[interfaces.Plugin]:\n        \"\"\"Initialize all plugins in the registry.\"\"\"\n        return [plugin_ep.init(config) for plugin_ep\n                in self._plugins.values()]\n\n    def filter(self, pred: Callable[[PluginEntryPoint], bool]) -> \"PluginsRegistry\":\n        \"\"\"Filter plugins based on predicate.\"\"\"\n        return type(self)({name: plugin_ep for name, plugin_ep\n                           in self._plugins.items() if pred(plugin_ep)})\n\n    def visible(self) -> \"PluginsRegistry\":\n        \"\"\"Filter plugins based on visibility.\"\"\"\n        return self.filter(lambda plugin_ep: not plugin_ep.hidden)\n\n    def ifaces(self, *ifaces_groups: Iterable[type]) -> \"PluginsRegistry\":\n        \"\"\"Filter plugins based on interfaces.\"\"\"\n        return self.filter(lambda p_ep: p_ep.ifaces(*ifaces_groups))\n\n    def prepare(self) -> list[Union[bool, Error]]:\n        \"\"\"Prepare all plugins in the registry.\"\"\"\n        return [plugin_ep.prepare() for plugin_ep in self._plugins.values()]\n\n    def available(self) -> \"PluginsRegistry\":\n        \"\"\"Filter plugins based on availability.\"\"\"\n        return self.filter(lambda p_ep: p_ep.available)\n        # successfully prepared + misconfigured\n\n    def find_init(self, plugin: interfaces.Plugin) -> Optional[PluginEntryPoint]:\n        \"\"\"Find an initialized plugin.\n\n        This is particularly useful for finding a name for the plugin::\n\n          # plugin is an instance providing Plugin, initialized\n          # somewhere else in the code\n          plugin_registry.find_init(plugin).name\n\n        Returns ``None`` if ``plugin`` is not found in the registry.\n\n        \"\"\"\n        # use list instead of set because PluginEntryPoint is not hashable\n        candidates = [plugin_ep for plugin_ep in self._plugins.values()\n                      if plugin_ep.initialized and plugin_ep.init() is plugin]\n        assert len(candidates) <= 1\n        if candidates:\n            return candidates[0]\n        return None\n\n    def __repr__(self) -> str:\n        return \"{0}({1})\".format(\n            self.__class__.__name__, ','.join(\n                repr(p_ep) for p_ep in self._plugins.values()))\n\n    def __str__(self) -> str:\n        if not self._plugins:\n            return \"No plugins\"\n        return \"\\n\\n\".join(str(p_ep) for p_ep in self._plugins.values())\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/manual.py",
    "content": "\"\"\"Manual authenticator plugin\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot import util\nfrom certbot._internal import hooks\nfrom certbot._internal.cli import cli_constants\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.display import ops as display_ops\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\n\nlogger = logging.getLogger(__name__)\n\n\nclass Authenticator(common.Plugin, interfaces.Authenticator):\n    \"\"\"Manual authenticator\n\n    This plugin allows the user to perform the validation\n    challenge(s) themselves. This either be done manually by the user or\n    through shell scripts provided to Certbot.\n\n    \"\"\"\n\n    description = 'Manual configuration or run your own shell scripts'\n    hidden = True\n    long_description = (\n        'Authenticate through manual configuration or custom shell scripts. '\n        'When using shell scripts, an authenticator script must be provided. '\n        'The environment variables available to this script depend on the '\n        'type of challenge. $CERTBOT_IDENTIFIER will always contain the domain or IP address '\n        'being authenticated. For HTTP-01 and DNS-01, $CERTBOT_VALIDATION '\n        'is the validation string, and $CERTBOT_TOKEN is the filename of the '\n        'resource requested when performing an HTTP-01 challenge. An additional '\n        'cleanup script can also be provided and can use the additional variable '\n        '$CERTBOT_AUTH_OUTPUT which contains the stdout output from the auth script. '\n        'For both authenticator and cleanup script, on HTTP-01 and DNS-01 challenges, '\n        '$CERTBOT_REMAINING_CHALLENGES will be equal to the number of challenges that '\n        'remain after the current one, and $CERTBOT_ALL_IDENTIFIERS contains a comma-separated '\n        'list of all identifiers that are challenged for the current certificate.')\n    # Include the full stop at the end of the FQDN in the instructions below for the null\n    # label of the DNS root, as stated in section 3.1 of RFC 1035. While not necessary\n    # for most day to day usage of hostnames, when adding FQDNs to a DNS zone editor, this\n    # full stop is often mandatory. Without a full stop, the entered name is often seen as\n    # relative to the DNS zone origin, which could lead to entries for, e.g.:\n    # _acme-challenge.example.com.example.com. For users unaware of this subtle detail,\n    # including the trailing full stop in the DNS instructions below might avert this issue.\n    _DNS_INSTRUCTIONS = \"\"\"\\\nPlease deploy a DNS TXT record under the name:\n\n{domain}.\n\nwith the following value:\n\n{validation}\n\"\"\"\n    _DNS_VERIFY_INSTRUCTIONS = \"\"\"\nBefore continuing, verify the TXT record has been deployed. Depending on the DNS\nprovider, this may take some time, from a few seconds to multiple minutes. You can\ncheck if it has finished deploying with aid of online tools, such as the Google\nAdmin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/{domain}.\nLook for one or more bolded line(s) below the line ';ANSWER'. It should show the\nvalue(s) you've just added.\n\"\"\"\n    _HTTP_INSTRUCTIONS = \"\"\"\\\nCreate a file containing just this data:\n\n{validation}\n\nAnd make it available on your web server at this URL:\n\n{uri}\n\"\"\"\n    _SUBSEQUENT_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet.)\n\"\"\"\n    _SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS = \"\"\"\n(This must be set up in addition to the previous challenges; do not remove,\nreplace, or undo the previous challenge tasks yet. Note that you might be\nasked to create multiple distinct TXT records with the same name. This is\npermitted by DNS standards.)\n\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.reverter = reverter.Reverter(self.config)\n        self.reverter.recovery_routine()\n        self.env: dict[achallenges.AnnotatedChallenge, dict[str, str]] = {}\n        self.subsequent_dns_challenge = False\n        self.subsequent_any_challenge = False\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        add('auth-hook',\n            help='Path or command to execute for the authentication script')\n        add('cleanup-hook',\n            help='Path or command to execute for the cleanup script')\n\n    def prepare(self) -> None:  # pylint: disable=missing-function-docstring\n        if self.config.noninteractive_mode and not self.conf('auth-hook'):\n            raise errors.PluginError(\n                'An authentication script must be provided with --{0} when '\n                'using the manual plugin non-interactively.'.format(\n                    self.option_name('auth-hook')))\n        self._validate_hooks()\n\n    def _validate_hooks(self) -> None:\n        if self.config.validate_hooks:\n            for name in ('auth-hook', 'cleanup-hook'):\n                hook = self.conf(name)\n                if hook is not None:\n                    hook_prefix = self.option_name(name)[:-len('-hook')]\n                    hooks.validate_hook(hook, hook_prefix)\n\n    def more_info(self) -> str:  # pylint: disable=missing-function-docstring\n        return (\n            'This plugin allows the user to customize setup for '\n            'validation challenges either through shell scripts provided by '\n            'the user or by performing the setup manually.')\n\n    def auth_hint(self, failed_achalls: Iterable[achallenges.AnnotatedChallenge]) -> str:\n        def has_chall(cls: type[challenges.Challenge]) -> bool:\n            return any(isinstance(achall.chall, cls) for achall in failed_achalls)\n\n        has_dns = has_chall(challenges.DNS01)\n        resource_names = {\n            challenges.DNS01: 'DNS TXT records',\n            challenges.HTTP01: 'challenge files',\n        }\n        resources = ' and '.join(sorted([v for k, v in resource_names.items() if has_chall(k)]))\n\n        if self.conf('auth-hook'):\n            return (\n                'The Certificate Authority failed to verify the {resources} created by the '\n                '--manual-auth-hook. Ensure that this hook is functioning correctly{dns_hint}. '\n                'Refer to \"{certbot} --help manual\" and the Certbot User Guide.'\n                .format(\n                    certbot=cli_constants.cli_command,\n                    resources=resources,\n                    dns_hint=(\n                        ' and that it waits a sufficient duration of time for DNS propagation'\n                    ) if has_dns else ''\n                )\n            )\n        else:\n            return (\n                'The Certificate Authority failed to verify the manually created {resources}. '\n                'Ensure that you created these in the correct location{dns_hint}.'\n                .format(\n                    resources=resources,\n                    dns_hint=(\n                        ', or try waiting longer for DNS propagation on the next attempt'\n                     ) if has_dns else ''\n                )\n            )\n\n    def get_chall_pref(self, identifier: str) -> Iterable[type[challenges.Challenge]]:\n        # pylint: disable=unused-argument,missing-function-docstring\n        return [challenges.HTTP01, challenges.DNS01]\n\n    def perform(self, achalls: list[achallenges.AnnotatedChallenge]\n                ) -> list[challenges.ChallengeResponse]:  # pylint: disable=missing-function-docstring\n        responses = []\n        last_dns_achall = 0\n        for i, achall in enumerate(achalls):\n            if isinstance(achall.chall, challenges.DNS01):\n                last_dns_achall = i\n        for i, achall in enumerate(achalls):\n            if self.conf('auth-hook'):\n                self._perform_achall_with_script(achall, achalls)\n            else:\n                self._perform_achall_manually(achall, i == last_dns_achall)\n            responses.append(achall.response(achall.account_key))\n        return responses\n\n    def _perform_achall_with_script(self, achall: achallenges.AnnotatedChallenge,\n                                    achalls: list[achallenges.AnnotatedChallenge]) -> None:\n        identifier_value = achall.identifier.value\n        env = {\n            \"CERTBOT_DOMAIN\": identifier_value,\n            \"CERTBOT_IDENTIFIER\": identifier_value,\n            \"CERTBOT_VALIDATION\": achall.validation(achall.account_key),\n            \"CERTBOT_ALL_DOMAINS\": ','.join(one_achall.identifier.value for one_achall in achalls),\n            \"CERTBOT_ALL_IDENTIFIERS\":\n                ','.join(one_achall.identifier.value for one_achall in achalls),\n            \"CERTBOT_REMAINING_CHALLENGES\": str(len(achalls) - achalls.index(achall) - 1),\n        }\n        if isinstance(achall.chall, challenges.HTTP01):\n            env['CERTBOT_TOKEN'] = achall.chall.encode('token')\n        else:\n            os.environ.pop('CERTBOT_TOKEN', None)\n        os.environ.update(env)\n        _, out = self._execute_hook('auth-hook', identifier_value)\n        env['CERTBOT_AUTH_OUTPUT'] = out.strip()\n        self.env[achall] = env\n\n    def _perform_achall_manually(self, achall: achallenges.AnnotatedChallenge,\n                                 last_dns_achall: bool = False) -> None:\n        identifier_value = achall.identifier.value\n        validation = achall.validation(achall.account_key)\n        if isinstance(achall.chall, challenges.HTTP01):\n            msg = self._HTTP_INSTRUCTIONS.format(\n                achall=achall, encoded_token=achall.chall.encode('token'),\n                port=self.config.http01_port,\n                uri=achall.chall.uri(identifier_value), validation=validation)\n        else:\n            assert isinstance(achall.chall, challenges.DNS01)\n            assert achall.identifier.typ == messages.IDENTIFIER_FQDN\n            msg = self._DNS_INSTRUCTIONS.format(\n                domain=achall.validation_domain_name(identifier_value),\n                validation=validation)\n        if isinstance(achall.chall, challenges.DNS01):\n            if self.subsequent_dns_challenge:\n                # 2nd or later dns-01 challenge\n                msg += self._SUBSEQUENT_DNS_CHALLENGE_INSTRUCTIONS\n            elif self.subsequent_any_challenge:\n                # 1st dns-01 challenge, but 2nd or later *any* challenge, so\n                # instruct user not to remove any previous http-01 challenge\n                msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n            self.subsequent_dns_challenge = True\n            if last_dns_achall:\n                # last dns-01 challenge\n                msg += self._DNS_VERIFY_INSTRUCTIONS.format(\n                    domain=achall.validation_domain_name(identifier_value))\n        elif self.subsequent_any_challenge:\n            # 2nd or later challenge of another type\n            msg += self._SUBSEQUENT_CHALLENGE_INSTRUCTIONS\n        display_util.notification(msg, wrap=False, force_interactive=True)\n        self.subsequent_any_challenge = True\n\n    def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None:  # pylint: disable=missing-function-docstring\n        if self.conf('cleanup-hook'):\n            for achall in achalls:\n                env = self.env.pop(achall)\n                if 'CERTBOT_TOKEN' not in env:\n                    os.environ.pop('CERTBOT_TOKEN', None)\n                os.environ.update(env)\n                self._execute_hook('cleanup-hook', achall.identifier.value)\n        self.reverter.recovery_routine()\n\n    def _execute_hook(self, hook_name: str, identifier_value: str) -> tuple[str, str]:\n        returncode, err, out = misc.execute_command_status(\n            self.option_name(hook_name), self.conf(hook_name),\n            env=util.env_no_snap_for_external_calls()\n        )\n\n        display_ops.report_executed_command(\n            f\"Hook '--manual-{hook_name}' for {identifier_value}\", returncode, out, err)\n\n        return err, out\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/null.py",
    "content": "\"\"\"Null plugin.\"\"\"\nimport logging\nfrom typing import Callable\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot import interfaces\nfrom certbot.plugins import common\n\nlogger = logging.getLogger(__name__)\n\n\nclass Installer(common.Plugin, interfaces.Installer):\n    \"\"\"Null installer.\"\"\"\n\n    description = \"Null Installer\"\n    hidden = True\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        pass\n\n    # pylint: disable=missing-function-docstring\n\n    def prepare(self) -> None:\n        pass  # pragma: no cover\n\n    def more_info(self) -> str:\n        return \"Installer that doesn't do anything (for testing).\"\n\n    def get_all_names(self) -> list[str]:\n        return []\n\n    def deploy_cert(self, domain: str, cert_path: str, key_path: str,\n                    chain_path: str, fullchain_path: str) -> None:\n        pass  # pragma: no cover\n\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[list[str], str]] = None) -> None:\n        pass  # pragma: no cover\n\n    def supported_enhancements(self) -> list[str]:\n        return []\n\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        pass  # pragma: no cover\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        pass  # pragma: no cover\n\n    def recovery_routine(self) -> None:\n        pass  # pragma: no cover\n\n    def config_test(self) -> None:\n        pass  # pragma: no cover\n\n    def restart(self) -> None:\n        pass  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/selection.py",
    "content": "\"\"\"Decide which plugins to use for authentication & installation\"\"\"\n\nimport logging\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TypeVar\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal.plugins import disco\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\n\nlogger = logging.getLogger(__name__)\n\n\ndef pick_configurator(config: configuration.NamespaceConfig, default: Optional[str],\n                      plugins: disco.PluginsRegistry,\n                      question: str = \"How would you like to authenticate and install \"\n                                      \"certificates?\") -> Optional[interfaces.Plugin]:\n    \"\"\"Pick configurator plugin.\"\"\"\n    return pick_plugin(\n        config, default, plugins, question,\n        (interfaces.Authenticator, interfaces.Installer))\n\n\ndef pick_installer(config: configuration.NamespaceConfig, default: Optional[str],\n                   plugins: disco.PluginsRegistry,\n                   question: str = \"How would you like to install certificates?\"\n                   ) -> Optional[interfaces.Installer]:\n    \"\"\"Pick installer plugin.\"\"\"\n    return pick_plugin(config, default, plugins, question, (interfaces.Installer,))\n\n\ndef pick_authenticator(config: configuration.NamespaceConfig, default: Optional[str],\n                       plugins: disco.PluginsRegistry,\n                       question: str = \"How would you \"\n                                       \"like to authenticate with the ACME CA?\"\n                       ) -> Optional[interfaces.Authenticator]:\n    \"\"\"Pick authentication plugin.\"\"\"\n    return pick_plugin(\n        config, default, plugins, question, (interfaces.Authenticator,))\n\n\ndef get_unprepared_installer(config: configuration.NamespaceConfig,\n                             plugins: disco.PluginsRegistry) -> Optional[interfaces.Installer]:\n    \"\"\"\n    Get an unprepared interfaces.Installer object.\n\n    :param certbot.configuration.NamespaceConfig config: Configuration\n    :param certbot._internal.plugins.disco.PluginsRegistry plugins:\n        All plugins registered as entry points.\n\n    :returns: Unprepared installer plugin or None\n    :rtype: Plugin or None\n    \"\"\"\n\n    _, req_inst = cli_plugin_requests(config)\n    if not req_inst:\n        return None\n    installers = plugins.filter(lambda p_ep: p_ep.check_name(req_inst))\n    installers.init(config)\n    if len(installers) > 1:\n        raise errors.PluginSelectionError(\n            \"Found multiple installers with the name %s, Certbot is unable to \"\n            \"determine which one to use. Skipping.\" % req_inst)\n    if installers:\n        inst = list(installers.values())[0]\n        logger.debug(\"Selecting plugin: %s\", inst)\n        return inst.init(config)\n    raise errors.PluginSelectionError(\n        \"Could not select or initialize the requested installer %s.\" % req_inst)\n\n\nP = TypeVar('P', bound=interfaces.Plugin)\n\n\ndef pick_plugin(config: configuration.NamespaceConfig, default: Optional[str],\n                plugins: disco.PluginsRegistry, question: str,\n                ifaces: Iterable[type]) -> Optional[P]:\n    \"\"\"Pick plugin.\n\n    :param certbot.configuration.NamespaceConfig config: Configuration\n    :param str default: Plugin name supplied by user or ``None``.\n    :param certbot._internal.plugins.disco.PluginsRegistry plugins:\n        All plugins registered as entry points.\n    :param str question: Question to be presented to the user in case\n        multiple candidates are found.\n    :param list ifaces: Interfaces that plugins must provide.\n\n    :returns: Initialized plugin.\n    :rtype: Plugin\n\n    \"\"\"\n    if default is not None:\n        # throw more UX-friendly error if default not in plugins\n        filtered = plugins.filter(lambda p_ep: p_ep.check_name(default))\n    else:\n        if config.noninteractive_mode:\n            # it's really bad to auto-select the single available plugin in\n            # non-interactive mode, because an update could later add a second\n            # available plugin\n            raise errors.MissingCommandlineFlag(\n                \"Missing command line flags. For non-interactive execution, \"\n                \"you will need to specify a plugin on the command line.  Run \"\n                \"with '--help plugins' to see a list of options, and see \"\n                \"https://eff.org/letsencrypt-plugins for more detail on what \"\n                \"the plugins do and how to use them.\")\n\n        filtered = plugins.visible()\n\n    filtered = filtered.ifaces(ifaces)\n    filtered.init(config)\n    filtered.prepare()\n    prepared = filtered.available()\n\n    if len(prepared) > 1:\n        logger.debug(\"Multiple candidate plugins: %s\", prepared)\n        plugin_ep1 = choose_plugin(list(prepared.values()), question)\n        if plugin_ep1 is None:\n            return None\n        return cast(P, plugin_ep1.init())\n    elif len(prepared) == 1:\n        plugin_ep2 = list(prepared.values())[0]\n        logger.debug(\"Single candidate plugin: %s\", plugin_ep2)\n        if plugin_ep2.misconfigured:\n            return None\n        return plugin_ep2.init()\n    else:\n        logger.debug(\"No candidate plugin\")\n        return None\n\n\ndef choose_plugin(prepared: list[disco.PluginEntryPoint],\n                  question: str) -> Optional[disco.PluginEntryPoint]:\n    \"\"\"Allow the user to choose their plugin.\n\n    :param list prepared: List of `~.PluginEntryPoint`.\n    :param str question: Question to be presented to the user.\n\n    :returns: Plugin entry point chosen by the user.\n    :rtype: `~.PluginEntryPoint`\n\n    \"\"\"\n    opts = [plugin_ep.description_with_name +\n            (\" [Misconfigured]\" if plugin_ep.misconfigured else \"\")\n            for plugin_ep in prepared]\n\n    while True:\n        code, index = display_util.menu(question, opts, force_interactive=True)\n\n        if code == display_util.OK:\n            plugin_ep = prepared[index]\n            if plugin_ep.misconfigured:\n                display_util.notification(\n                    \"The selected plugin encountered an error while parsing \"\n                    \"your server configuration and cannot be used. The error \"\n                    \"was:\\n\\n{0}\".format(plugin_ep.prepare()), pause=False)\n            else:\n                return plugin_ep\n        else:\n            return None\n\n\nnoninstaller_plugins = [\"webroot\", \"manual\", \"standalone\", \"dns-cloudflare\",\n                        \"dns-digitalocean\", \"dns-dnsimple\", \"dns-dnsmadeeasy\", \"dns-gehirn\",\n                        \"dns-google\", \"dns-linode\", \"dns-luadns\", \"dns-nsone\", \"dns-ovh\",\n                        \"dns-rfc2136\", \"dns-route53\", \"dns-sakuracloud\"]\n\n\ndef record_chosen_plugins(config: configuration.NamespaceConfig, plugins: disco.PluginsRegistry,\n                          auth: Optional[interfaces.Authenticator],\n                          inst: Optional[interfaces.Installer]) -> None:\n    \"\"\"Update the config entries to reflect the plugins we actually selected.\"\"\"\n    config.authenticator = None\n    if auth:\n        auth_ep = plugins.find_init(auth)\n        if auth_ep:\n            config.authenticator = auth_ep.name\n    config.installer = None\n    if inst:\n        inst_ep = plugins.find_init(inst)\n        if inst_ep:\n            config.installer = inst_ep.name\n    logger.info(\"Plugins selected: Authenticator %s, Installer %s\",\n                config.authenticator, config.installer)\n\n\ndef choose_configurator_plugins(config: configuration.NamespaceConfig,\n                                plugins: disco.PluginsRegistry,\n                                verb: str) -> tuple[Optional[interfaces.Installer],\n                                                    Optional[interfaces.Authenticator]]:\n    \"\"\"\n    Figure out which configurator we're going to use, modifies\n    config.authenticator and config.installer strings to reflect that choice if\n    necessary.\n\n    :raises errors.PluginSelectionError if there was a problem\n\n    :returns: tuple of (`Installer` or None, `Authenticator` or None)\n    :rtype: tuple\n    \"\"\"\n\n    req_auth, req_inst = cli_plugin_requests(config)\n    installer_question = \"\"\n\n    if verb == \"enhance\":\n        installer_question = (\"Which installer would you like to use to \"\n                              \"configure the selected enhancements?\")\n\n    # Which plugins do we need?\n    if verb == \"run\":\n        need_inst = need_auth = True\n        from certbot._internal.cli import cli_command\n        if req_auth in noninstaller_plugins and not req_inst:\n            msg = ('With the {0} plugin, you probably want to use the \"certonly\" command, eg:{1}'\n                   '{1}    {2} certonly --{0}{1}{1}'\n                   '(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins'\n                   '{1} and \"--help plugins\" for more information.)'.format(\n                       req_auth, os.linesep, cli_command))\n\n            raise errors.MissingCommandlineFlag(msg)\n    else:\n        need_inst = need_auth = False\n    if verb == \"certonly\":\n        need_auth = True\n    elif verb in (\"install\", \"enhance\"):\n        need_inst = True\n        if config.authenticator:\n            logger.warning(\"Specifying an authenticator doesn't make sense when \"\n                           \"running Certbot with verb \\\"%s\\\"\", verb)\n    # Try to meet the user's request and/or ask them to pick plugins\n    authenticator: Optional[interfaces.Authenticator] = None\n    installer: Optional[interfaces.Installer] = None\n    if verb == \"run\" and req_auth == req_inst:\n        # Unless the user has explicitly asked for different auth/install,\n        # only consider offering a single choice\n        configurator = pick_configurator(config, req_inst, plugins)\n        authenticator = cast(Optional[interfaces.Authenticator], configurator)\n        installer = cast(Optional[interfaces.Installer], configurator)\n    else:\n        if need_inst or req_inst:\n            installer = pick_installer(config, req_inst, plugins, installer_question)\n        if need_auth:\n            authenticator = pick_authenticator(config, req_auth, plugins)\n\n    # Report on any failures\n    if need_inst and not installer:\n        diagnose_configurator_problem(\"installer\", req_inst, plugins)\n    if need_auth and not authenticator:\n        diagnose_configurator_problem(\"authenticator\", req_auth, plugins)\n\n    # As a special case for certonly, if a user selected apache or nginx, set\n    # the relevant installer (unless the user specifically specified no\n    # installer or only specified an authenticator on the command line)\n    if verb == \"certonly\" and authenticator is not None:\n        # user specified --nginx or --apache on CLI\n        selected_configurator = config.nginx or config.apache\n        # user didn't request an authenticator, and so interactively chose nginx\n        # or apache\n        interactively_selected = req_auth is None and authenticator.name in (\"nginx\", \"apache\")\n\n        if selected_configurator or interactively_selected:\n            installer = cast(Optional[interfaces.Installer], authenticator)\n    logger.debug(\"Selected authenticator %s and installer %s\", authenticator, installer)\n\n    record_chosen_plugins(config, plugins, authenticator, installer)\n    return installer, authenticator\n\n\ndef set_configurator(previously: Optional[str], now: Optional[str]) -> Optional[str]:\n    \"\"\"\n    Setting configurators multiple ways is okay, as long as they all agree\n    :param str previously: previously identified request for the installer/authenticator\n    :param str now: the request currently being processed\n    \"\"\"\n    if not now:\n        # we're not actually setting anything\n        return previously\n    if previously:\n        if previously != now:\n            msg = \"Too many flags setting configurators/installers/authenticators {0} -> {1}\"\n            raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))\n    return now\n\n\ndef cli_plugin_requests(config: configuration.NamespaceConfig\n                        ) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"\n    Figure out which plugins the user requested with CLI and config options\n\n    :returns: (requested authenticator string or None, requested installer string or None)\n    :rtype: tuple\n    \"\"\"\n    req_inst = req_auth = config.configurator\n    req_inst = set_configurator(req_inst, config.installer)\n    req_auth = set_configurator(req_auth, config.authenticator)\n\n    if config.nginx:\n        req_inst = set_configurator(req_inst, \"nginx\")\n        req_auth = set_configurator(req_auth, \"nginx\")\n    if config.apache:\n        req_inst = set_configurator(req_inst, \"apache\")\n        req_auth = set_configurator(req_auth, \"apache\")\n    if config.standalone:\n        req_auth = set_configurator(req_auth, \"standalone\")\n    if config.webroot:\n        req_auth = set_configurator(req_auth, \"webroot\")\n    if config.manual:\n        req_auth = set_configurator(req_auth, \"manual\")\n    if config.dns_cloudflare:\n        req_auth = set_configurator(req_auth, \"dns-cloudflare\")\n    if config.dns_digitalocean:\n        req_auth = set_configurator(req_auth, \"dns-digitalocean\")\n    if config.dns_dnsimple:\n        req_auth = set_configurator(req_auth, \"dns-dnsimple\")\n    if config.dns_dnsmadeeasy:\n        req_auth = set_configurator(req_auth, \"dns-dnsmadeeasy\")\n    if config.dns_gehirn:\n        req_auth = set_configurator(req_auth, \"dns-gehirn\")\n    if config.dns_google:\n        req_auth = set_configurator(req_auth, \"dns-google\")\n    if config.dns_linode:\n        req_auth = set_configurator(req_auth, \"dns-linode\")\n    if config.dns_luadns:\n        req_auth = set_configurator(req_auth, \"dns-luadns\")\n    if config.dns_nsone:\n        req_auth = set_configurator(req_auth, \"dns-nsone\")\n    if config.dns_ovh:\n        req_auth = set_configurator(req_auth, \"dns-ovh\")\n    if config.dns_rfc2136:\n        req_auth = set_configurator(req_auth, \"dns-rfc2136\")\n    if config.dns_route53:\n        req_auth = set_configurator(req_auth, \"dns-route53\")\n    if config.dns_sakuracloud:\n        req_auth = set_configurator(req_auth, \"dns-sakuracloud\")\n    logger.debug(\"Requested authenticator %s and installer %s\", req_auth, req_inst)\n    return req_auth, req_inst\n\n\ndef diagnose_configurator_problem(cfg_type: str, requested: Optional[str],\n                                  plugins: disco.PluginsRegistry) -> None:\n    \"\"\"\n    Raise the most helpful error message about a plugin being unavailable\n\n    :param str cfg_type: either \"installer\" or \"authenticator\"\n    :param str requested: the plugin that was requested\n    :param .PluginsRegistry plugins: available plugins\n\n    :raises error.PluginSelectionError: if there was a problem\n    \"\"\"\n\n    if requested:\n        if requested not in plugins:\n            msg = \"The requested {0} plugin does not appear to be installed\".format(requested)\n        else:\n            msg = (\"The {0} plugin is not working; there may be problems with \"\n                   \"your existing configuration.\\nThe error was: {1!r}\"\n                   .format(requested, plugins[requested].problem))\n    elif cfg_type == \"installer\":\n        from certbot._internal.cli import cli_command\n        msg = ('Certbot doesn\\'t know how to automatically configure the web '\n          'server on this system. However, it can still get a certificate for '\n          'you. Please run \"{0} certonly\" to do so. You\\'ll need to '\n          'manually configure your web server to use the resulting '\n          'certificate.').format(cli_command)\n    else:\n        msg = \"{0} could not be determined or is not installed\".format(cfg_type)\n    raise errors.PluginSelectionError(msg)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/standalone.py",
    "content": "\"\"\"Standalone Authenticator.\"\"\"\nimport collections\nimport errno\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import TYPE_CHECKING\n\nfrom acme import challenges\nfrom acme import standalone as acme_standalone\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\n\nlogger = logging.getLogger(__name__)\n\nif TYPE_CHECKING:\n    ServedType = collections.defaultdict[\n        acme_standalone.BaseDualNetworkedServers,\n        set[achallenges.AnnotatedChallenge]\n    ]\n\n\nclass ServerManager:\n    \"\"\"Manager for HTTP-01 standalone server instances.\"\"\"\n\n    def __init__(self,\n                 http_01_resources: set[acme_standalone.HTTP01RequestHandler.HTTP01Resource]\n                 ) -> None:\n        self._instances: dict[int, acme_standalone.HTTP01DualNetworkedServers] = {}\n        self.http_01_resources = http_01_resources\n\n    def run(self, port: int, challenge_type: type[challenges.Challenge],\n            listenaddr: str = \"\") -> acme_standalone.HTTP01DualNetworkedServers:\n        \"\"\"Run ACME server on specified ``port``.\n\n        This method is idempotent, i.e. all calls with the same pair of\n        ``(port, challenge_type)`` will reuse the same server.\n\n        :param int port: Port to run the server on.\n        :param challenge_type: Subclass of `acme.challenges.Challenge`,\n            currently only `acme.challenge.HTTP01`.\n        :param str listenaddr: (optional) The address to listen on. Defaults to all addrs.\n\n        :returns: DualNetworkedServers instance.\n        :rtype: ACMEServerMixin\n\n        \"\"\"\n        assert challenge_type == challenges.HTTP01\n        if port in self._instances:\n            return self._instances[port]\n\n        address = (listenaddr, port)\n        try:\n            servers = acme_standalone.HTTP01DualNetworkedServers(\n                address, self.http_01_resources)\n        except OSError as error:\n            raise errors.StandaloneBindError(error, port)\n\n        servers.serve_forever()\n\n        # if port == 0, then random free port on OS is taken\n        # both servers, if they exist, have the same port\n        real_port = servers.getsocknames()[0][1]\n        self._instances[real_port] = servers\n        return servers\n\n    def stop(self, port: int) -> None:\n        \"\"\"Stop ACME server running on the specified ``port``.\n\n        :param int port:\n\n        \"\"\"\n        instance = self._instances[port]\n        for sockname in instance.getsocknames():\n            logger.debug(\"Stopping server at %s:%d...\",\n                         *sockname[:2])\n        instance.shutdown_and_server_close()\n        del self._instances[port]\n\n    def running(self) -> dict[int, acme_standalone.HTTP01DualNetworkedServers]:\n        \"\"\"Return all running instances.\n\n        Once the server is stopped using `stop`, it will not be\n        returned.\n\n        :returns: Mapping from ``port`` to ``servers``.\n        :rtype: tuple\n\n        \"\"\"\n        return self._instances.copy()\n\n\nclass Authenticator(common.Plugin, interfaces.Authenticator):\n    \"\"\"Standalone Authenticator.\n\n    This authenticator creates its own ephemeral TCP listener on the\n    necessary port in order to respond to incoming http-01\n    challenges from the certificate authority. Therefore, it does not\n    rely on any existing server program.\n    \"\"\"\n\n    description = \"\"\"Runs an HTTP server locally which serves the necessary validation files \\\nunder the /.well-known/acme-challenge/ request path. Suitable if there is no HTTP server already \\\nrunning. HTTP challenge only (wildcards not supported).\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n\n        self.served: ServedType = collections.defaultdict(set)\n\n        # Stuff below is shared across threads (i.e. servers read\n        # values, main thread writes). Due to the nature of CPython's\n        # GIL, the operations are safe, c.f.\n        # https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe\n        self.http_01_resources: set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = set()\n\n        self.servers = ServerManager(self.http_01_resources)\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        pass  # No additional argument for the standalone plugin parser\n\n    def more_info(self) -> str:  # pylint: disable=missing-function-docstring\n        return(\"This authenticator creates its own ephemeral TCP listener \"\n               \"on the necessary port in order to respond to incoming \"\n               \"http-01 challenges from the certificate authority. Therefore, \"\n               \"it does not rely on any existing server program.\")\n\n    def prepare(self) -> None:  # pylint: disable=missing-function-docstring\n        pass\n\n    def get_chall_pref(self, identifier: str) -> Iterable[type[challenges.Challenge]]:\n        # pylint: disable=unused-argument,missing-function-docstring\n        return [challenges.HTTP01]\n\n    def perform(self, achalls: Iterable[achallenges.AnnotatedChallenge]\n                ) -> list[challenges.ChallengeResponse]:  # pylint: disable=missing-function-docstring\n        return [self._try_perform_single(achall) for achall in achalls]\n\n    def _try_perform_single(self,\n                            achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse:\n        while True:\n            try:\n                return self._perform_single(achall)\n            except errors.StandaloneBindError as error:\n                _handle_perform_error(error)\n\n    def _perform_single(self,\n                        achall: achallenges.AnnotatedChallenge) -> challenges.ChallengeResponse:\n        servers, response = self._perform_http_01(achall)\n        self.served[servers].add(achall)\n        return response\n\n    def _perform_http_01(self, achall: achallenges.AnnotatedChallenge\n                         ) -> tuple[acme_standalone.HTTP01DualNetworkedServers,\n                                    challenges.ChallengeResponse]:\n        port = self.config.http01_port\n        addr = self.config.http01_address\n        servers = self.servers.run(port, challenges.HTTP01, listenaddr=addr)\n        response, validation = achall.response_and_validation()\n        resource = acme_standalone.HTTP01RequestHandler.HTTP01Resource(\n            chall=achall.chall, response=response, validation=validation)\n        self.http_01_resources.add(resource)\n        return servers, response\n\n    def cleanup(self, achalls: Iterable[achallenges.AnnotatedChallenge]) -> None:  # pylint: disable=missing-function-docstring\n        # reduce self.served and close servers if no challenges are served\n        for unused_servers, server_achalls in self.served.items():\n            for achall in achalls:\n                if achall in server_achalls:\n                    server_achalls.remove(achall)\n        for port, servers in self.servers.running().items():\n            if not self.served[servers]:\n                self.servers.stop(port)\n\n    def auth_hint(self, failed_achalls: list[achallenges.AnnotatedChallenge]) -> str:\n        port, addr = self.config.http01_port, self.config.http01_address\n        neat_addr = f\"{addr}:{port}\" if addr else f\"port {port}\"\n        return (\"The Certificate Authority failed to download the challenge files from \"\n                f\"the temporary standalone webserver started by Certbot on {neat_addr}. \"\n                \"Ensure that the listed domains point to this machine and that it can \"\n                \"accept inbound connections from the internet.\")\n\n\ndef _handle_perform_error(error: errors.StandaloneBindError) -> None:\n    if error.socket_error.errno == errno.EACCES:\n        raise errors.PluginError(\n            \"Could not bind TCP port {0} because you don't have \"\n            \"the appropriate permissions (for example, you \"\n            \"aren't running this program as \"\n            \"root).\".format(error.port))\n    if error.socket_error.errno == errno.EADDRINUSE:\n        msg = (\n            \"Could not bind TCP port {0} because it is already in \"\n            \"use by another process on this system (such as a web \"\n            \"server). Please stop the program in question and \"\n            \"then try again.\".format(error.port))\n        should_retry = display_util.yesno(msg, \"Retry\", \"Cancel\", default=False)\n        if not should_retry:\n            raise errors.PluginError(msg)\n    else:\n        raise error\n"
  },
  {
    "path": "certbot/src/certbot/_internal/plugins/webroot.py",
    "content": "\"\"\"Webroot plugin.\"\"\"\nimport argparse\nimport collections\nimport json\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\n\nfrom acme import challenges, messages\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal import cli\nfrom certbot._internal import san\nfrom certbot.achallenges import AnnotatedChallenge\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import ops\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\nfrom certbot.plugins import util\nfrom certbot.util import safe_open\n\nlogger = logging.getLogger(__name__)\n\n\n_WEB_CONFIG_CONTENT = \"\"\"\\\n<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!--Generated by Certbot-->\n<configuration>\n  <system.webServer>\n      <staticContent>\n          <remove fileExtension=\".\"/>\n          <mimeMap fileExtension=\".\" mimeType=\"text/plain\" />\n      </staticContent>\n  </system.webServer>\n</configuration>\n\"\"\"\n# This list references the hashes of all versions of the web.config files that Certbot could\n# have generated during an HTTP-01 challenge. If you modify _WEB_CONFIG_CONTENT, you MUST add\n# the new hash in this list.\n_WEB_CONFIG_SHA256SUMS = [\n    \"20c5ca1bd58fa8ad5f07a2f1be8b7cbb707c20fcb607a8fc8db9393952846a97\",\n    \"8d31383d3a079d2098a9d0c0921f4ab87e708b9868dc3f314d54094c2fe70336\"\n]\n\n\nclass Authenticator(common.Plugin, interfaces.Authenticator):\n    \"\"\"Webroot Authenticator.\"\"\"\n\n    description = \"\"\"\\\nSaves the necessary validation files to a .well-known/acme-challenge/ directory within the \\\nnominated webroot path. A separate HTTP server must be running and serving files from the \\\nwebroot path. HTTP challenge only (wildcards not supported).\"\"\"\n\n    MORE_INFO = \"\"\"\\\nAuthenticator plugin that performs http-01 challenge by saving\nnecessary validation resources to appropriate paths on the file\nsystem. It expects that there is some other HTTP server configured\nto serve all files under specified web root ({0}).\"\"\"\n\n    def more_info(self) -> str:  # pylint: disable=missing-function-docstring\n        return self.MORE_INFO.format(self.conf(\"path\"))\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        add(\"path\", \"-w\", default=[], action=_WebrootPathAction,\n            help=\"public_html / webroot path. This can be specified multiple \"\n                 \"times to handle different identifiers; each identifier will have \"\n                 \"the webroot path that preceded it.  For instance: `-w \"\n                 \"/var/www/example -d example.com -d www.example.com -w \"\n                 \"/var/www/thing -d thing.net -d m.thing.net` (default: Ask)\")\n        add(\"map\", default={}, action=_WebrootMapAction,\n            help=\"JSON dictionary mapping identifiers to webroot paths; this \"\n                 \"implies -d or --ip-address for each entry. You may need to \"\n                 \" escape this from your shell. E.g.: --webroot-map \"\n                 '\\'{\"eg1.is,m.eg1.is\":\"/www/eg1/\", \"eg2.is\":\"/www/eg2\"}\\' '\n                 \"This option is merged with, but takes precedence over, -w / \"\n                 \"-d entries. At present, if you put webroot-map in a config \"\n                 \"file, it needs to be on a single line, like: webroot-map = \"\n                 '{\"example.com\":\"/var/www\"}.')\n\n    def auth_hint(self, failed_achalls: list[AnnotatedChallenge]) -> str:  # pragma: no cover\n        return (\"The Certificate Authority failed to download the temporary challenge files \"\n                \"created by Certbot. Ensure that the listed identifiers serve their content from \"\n                \"the provided --webroot-path/-w and that files created there can be downloaded \"\n                \"from the internet.\")\n\n    def get_chall_pref(self, identifier: str) -> Iterable[type[challenges.Challenge]]:\n        # pylint: disable=unused-argument,missing-function-docstring\n        return [challenges.HTTP01]\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.full_roots: dict[str, str] = {}\n        self.performed: collections.defaultdict[str, set[AnnotatedChallenge]] = (\n            collections.defaultdict(set)\n        )\n        # stack of dirs successfully created by this authenticator\n        self._created_dirs: list[str] = []\n\n    def prepare(self) -> None:  # pylint: disable=missing-function-docstring\n        pass\n\n    def perform(self, achalls: list[AnnotatedChallenge]) -> list[challenges.ChallengeResponse]:  # pylint: disable=missing-function-docstring\n        self._set_webroots(achalls)\n\n        self._create_challenge_dirs()\n\n        return [self._perform_single(achall) for achall in achalls]\n\n    def _set_webroots(self, achalls: Iterable[AnnotatedChallenge]) -> None:\n        if self.conf(\"path\"):\n            webroot_path = self.conf(\"path\")[-1]\n            logger.info(\"Using the webroot path %s for all unmatched identifiers.\",\n                        webroot_path)\n            for achall in achalls:\n                self.conf(\"map\").setdefault(achall.identifier.value, webroot_path)\n        else:\n            known_webroots = list(set(self.conf(\"map\").values()))\n            for achall in achalls:\n                if achall.identifier.value not in self.conf(\"map\"):\n                    new_webroot = self._prompt_for_webroot(achall.identifier,\n                                                           known_webroots)\n                    # Put the most recently input\n                    # webroot first for easy selection\n                    try:\n                        known_webroots.remove(new_webroot)\n                    except ValueError:\n                        pass\n                    known_webroots.insert(0, new_webroot)\n                    self.conf(\"map\")[achall.identifier.value] = new_webroot\n\n    def _prompt_for_webroot(self, identifier: messages.Identifier,\n                            known_webroots: list[str]) -> Optional[str]:\n        webroot = None\n\n        while webroot is None:\n            if known_webroots:\n                # Only show the menu if we have options for it\n                webroot = self._prompt_with_webroot_list(identifier, known_webroots)\n                if webroot is None:\n                    webroot = self._prompt_for_new_webroot(identifier)\n            else:\n                # Allow prompt to raise PluginError instead of looping forever\n                webroot = self._prompt_for_new_webroot(identifier, True)\n\n        return webroot\n\n    def _prompt_with_webroot_list(self, identifier: messages.Identifier,\n                                  known_webroots: list[str]) -> Optional[str]:\n        path_flag = \"--\" + self.option_name(\"path\")\n\n        while True:\n            code, index = display_util.menu(\n                \"Select the webroot for {0}:\".format(identifier.value),\n                [\"Enter a new webroot\"] + known_webroots,\n                cli_flag=path_flag, force_interactive=True)\n            if code == display_util.CANCEL:\n                raise errors.PluginError(\n                    \"Every requested identifier must have a \"\n                    \"webroot when using the webroot plugin.\")\n            return None if index == 0 else known_webroots[index - 1]  # code == display_util.OK\n\n    def _prompt_for_new_webroot(self, identifier: messages.Identifier,\n                                allowraise: bool = False) -> Optional[str]:\n        code, webroot = ops.validated_directory(\n            _validate_webroot,\n            \"Input the webroot for {0}:\".format(identifier.value),\n            force_interactive=True)\n        if code == display_util.CANCEL:\n            if not allowraise:\n                return None\n            raise errors.PluginError(\n                \"Every requested identifier must have a \"\n                \"webroot when using the webroot plugin.\")\n        return _validate_webroot(webroot)  # code == display_util.OK\n\n    def _create_challenge_dirs(self) -> None:\n        path_map = self.conf(\"map\")\n        if not path_map:\n            raise errors.PluginError(\n                \"Missing parts of webroot configuration; please set \"\n                \"--webroot-path and --domains or --ip-address. \"\n                \"Alternatively you may set --webroot-map. \"\n                \"Run with --help webroot for examples.\")\n        for name, path in path_map.items():\n            self.full_roots[name] = os.path.join(path, os.path.normcase(\n                challenges.HTTP01.URI_ROOT_PATH))\n            logger.debug(\"Creating root challenges validation dir at %s\",\n                         self.full_roots[name])\n\n            # Change the permissions to be writable (GH #1389)\n            # Umask is used instead of chmod to ensure the client can also\n            # run as non-root (GH #1795)\n            with filesystem.temp_umask(0o022):\n                # We ignore the last prefix in the next iteration,\n                # as it does not correspond to a folder path ('/' or 'C:')\n                for prefix in sorted(util.get_prefixes(self.full_roots[name])[:-1], key=len):\n                    if os.path.isdir(prefix):\n                        # Don't try to create directory if it already exists, as some filesystems\n                        # won't reliably raise EEXIST or EISDIR if directory exists.\n                        continue\n                    try:\n                        # Set owner as parent directory if possible, apply mode for Linux/Windows.\n                        # For Linux, this is coupled with the \"umask\" call above because\n                        # os.mkdir's \"mode\" parameter may not always work:\n                        # https://docs.python.org/3/library/os.html#os.mkdir\n                        filesystem.mkdir(prefix, 0o755)\n                        self._created_dirs.append(prefix)\n                        try:\n                            filesystem.copy_ownership_and_apply_mode(\n                                path, prefix, 0o755, copy_user=True, copy_group=True)\n                        except (OSError, AttributeError) as exception:\n                            logger.warning(\"Unable to change owner and uid of webroot directory\")\n                            logger.debug(\"Error was: %s\", exception)\n                    except OSError as exception:\n                        raise errors.PluginError(\n                            \"Couldn't create root for {0} http-01 \"\n                            \"challenge responses: {1}\".format(name, exception))\n\n            # On Windows, generate a local web.config file that allows IIS to serve expose\n            # challenge files despite the fact they do not have a file extension.\n            if not filesystem.POSIX_MODE:\n                web_config_path = os.path.join(self.full_roots[name], \"web.config\")\n                if os.path.exists(web_config_path):\n                    logger.info(\"A web.config file has not been created in \"\n                                \"%s because another one already exists.\", self.full_roots[name])\n                    continue\n                logger.info(\"Creating a web.config file in %s to allow IIS \"\n                            \"to serve challenge files.\", self.full_roots[name])\n                with safe_open(web_config_path, mode=\"w\", chmod=0o644) as web_config:\n                    web_config.write(_WEB_CONFIG_CONTENT)\n\n    def _get_validation_path(self, root_path: str, achall: AnnotatedChallenge) -> str:\n        return os.path.join(root_path, achall.chall.encode(\"token\"))\n\n    def _perform_single(self, achall: AnnotatedChallenge) -> challenges.ChallengeResponse:\n        response, validation = achall.response_and_validation()\n\n        root_path = self.full_roots[achall.identifier.value]\n        validation_path = self._get_validation_path(root_path, achall)\n        logger.debug(\"Attempting to save validation to %s\", validation_path)\n\n        # Change permissions to be world-readable, owner-writable (GH #1795)\n        with filesystem.temp_umask(0o022):\n            with safe_open(validation_path, mode=\"wb\", chmod=0o644) as validation_file:\n                validation_file.write(validation.encode())\n\n        self.performed[root_path].add(achall)\n        return response\n\n    def cleanup(self, achalls: list[AnnotatedChallenge]) -> None:  # pylint: disable=missing-function-docstring\n        for achall in achalls:\n            root_path = self.full_roots.get(achall.identifier.value, None)\n            if root_path is not None:\n                validation_path = self._get_validation_path(root_path, achall)\n                logger.debug(\"Removing %s\", validation_path)\n                os.remove(validation_path)\n                self.performed[root_path].remove(achall)\n\n                if not filesystem.POSIX_MODE:\n                    web_config_path = os.path.join(root_path, \"web.config\")\n                    if os.path.exists(web_config_path):\n                        sha256sum = crypto_util.sha256sum(web_config_path)\n                        if sha256sum in _WEB_CONFIG_SHA256SUMS:\n                            logger.info(\"Cleaning web.config file generated by Certbot in %s.\",\n                                        root_path)\n                            os.remove(web_config_path)\n                        else:\n                            logger.info(\"Not cleaning up the web.config file in %s \"\n                                        \"because it is not generated by Certbot.\", root_path)\n\n        not_removed: list[str] = []\n        while self._created_dirs:\n            path = self._created_dirs.pop()\n            try:\n                os.rmdir(path)\n            except OSError as exc:\n                not_removed.insert(0, path)\n                logger.info(\"Challenge directory %s was not empty, didn't remove\", path)\n                logger.debug(\"Error was: %s\", exc)\n        self._created_dirs = not_removed\n        logger.debug(\"All challenges cleaned up\")\n\n\nclass _WebrootMapAction(argparse.Action):\n    \"\"\"Action class for parsing webroot_map.\"\"\"\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 webroot_map: Union[str, Sequence[Any], None],\n                 option_string: Optional[str] = None) -> None:\n        if webroot_map is None:\n            return\n        for identlist, webroot_path in json.loads(str(webroot_map)).items():\n            webroot_path = _validate_webroot(webroot_path)\n            for s in san.guess(identlist.split(\",\")):\n                match s:\n                    case san.IPAddress():\n                        cli.add_ip_address(namespace, s)\n                    case san.DNSName():\n                        cli.add_dns_name(namespace, s)\n\n                namespace.webroot_map[str(s)] = webroot_path\n\n\nclass _WebrootPathAction(argparse.Action):\n    \"\"\"Action class for parsing webroot_path.\"\"\"\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._ident_before_webroot = False\n\n    def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace,\n                 webroot_path: Union[str, Sequence[Any], None],\n                 option_string: Optional[str] = None) -> None:\n        if webroot_path is None:\n            return\n        if self._ident_before_webroot:\n            raise errors.PluginError(\n                \"If you specify multiple webroot paths, \"\n                \"one of them must precede all --domain and --ip-address flags\")\n\n        if namespace.webroot_path:\n            # Apply previous webroot to all matched\n            # domains before setting the new webroot path\n            prev_webroot = namespace.webroot_path[-1]\n            for domain in namespace.domains:\n                namespace.webroot_map.setdefault(domain.dns_name, prev_webroot)\n            for ip_address in namespace.ip_addresses:\n                namespace.webroot_map.setdefault(str(ip_address), prev_webroot)\n        elif namespace.domains or namespace.ip_addresses:\n            self._ident_before_webroot = True\n\n        namespace.webroot_path.append(_validate_webroot(str(webroot_path)))\n\n\ndef _validate_webroot(webroot_path: str) -> str:\n    \"\"\"Validates and returns the absolute path of webroot_path.\n\n    :param str webroot_path: path to the webroot directory\n\n    :returns: absolute path of webroot_path\n    :rtype: str\n\n    \"\"\"\n    if not os.path.isdir(webroot_path):\n        raise errors.PluginError(webroot_path + \" does not exist or is not a directory\")\n\n    return os.path.abspath(webroot_path)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/renewal.py",
    "content": "\"\"\"Functionality for autorenewal and associated juggling of configurations\"\"\"\nimport configobj\nimport copy\nimport datetime\nimport itertools\nimport logging\nimport random\nimport sys\nimport time\nimport traceback\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nfrom cryptography import x509\n\nfrom acme import client as acme_client\n\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import cli\nfrom certbot._internal import client\nfrom certbot._internal import constants\nfrom certbot._internal import hooks\nfrom certbot._internal import san\nfrom certbot._internal import storage\nfrom certbot._internal import updater\nfrom certbot._internal.display import obj as display_obj\nfrom certbot._internal.plugins import disco as plugins_disco\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\n\nlogger = logging.getLogger(__name__)\n\nARI_RETRY_AFTER_CONFIG_ITEM = \"ari_retry_after\"\n\n# These are the items which get pulled out of a renewal configuration\n# file's renewalparams and actually used in the client configuration\n# during the renewal process. We have to record their types here because\n# the renewal configuration process loses this information.\nSTR_CONFIG_ITEMS = [\"config_dir\", \"logs_dir\", \"work_dir\", \"user_agent\",\n                    \"server\", \"account\", \"authenticator\", \"installer\",\n                    \"deploy_hook\", \"pre_hook\", \"post_hook\", \"http01_address\",\n                    \"preferred_chain\", \"key_type\", \"elliptic_curve\",\n                    \"preferred_profile\", \"required_profile\"]\nINT_CONFIG_ITEMS = [\"rsa_key_size\", \"http01_port\"]\nBOOL_CONFIG_ITEMS = [\"must_staple\", \"allow_subset_of_names\", \"reuse_key\",\n                     \"autorenew\"]\n\nCONFIG_ITEMS = set(itertools.chain(\n    BOOL_CONFIG_ITEMS, INT_CONFIG_ITEMS, STR_CONFIG_ITEMS, ('pref_challs',)))\n\nclass AriClientPool:\n    \"\"\"A cache of ACME clients for using in performing ACME Renewal Info (ARI) requests.\n\n    During `certbot renew` we need to check ARI for many certificates, and usually these\n    are all issued by the same server. To avoid redundant directory fetches, we create\n    one acme.ClientV2 per server.\n\n    This takes a command line configuration object so it can set the User-Agent header and\n    observe the --no-verify-ssl flag.\n    \"\"\"\n    def __init__(self, cli_config: configuration.NamespaceConfig):\n        self._verify_ssl = not cli_config.no_verify_ssl\n        self._user_agent = client.determine_user_agent(cli_config)\n        self._pool: dict[str, acme_client.ClientV2] = {}\n\n    def get(self, server: str) -> acme_client.ClientV2:\n        \"\"\"\n        Retrieve or create an ACME client for the specified server.\n\n        Returns:\n            acme_client.ClientV2: The ACME client associated with the specified server.\n        \"\"\"\n        ari_client = self._pool.get(server, None)\n        if ari_client:\n            return ari_client\n\n        net = acme_client.ClientNetwork(verify_ssl=self._verify_ssl, user_agent=self._user_agent)\n        directory = acme_client.ClientV2.get_directory(server, net)\n        ari_client = acme_client.ClientV2(directory, net)\n\n        self._pool[server] = ari_client\n        return ari_client\n\n\ndef reconstitute(config: configuration.NamespaceConfig,\n                  full_path: str) -> Optional[storage.RenewableCert]:\n    \"\"\"Try to instantiate a RenewableCert, updating config with relevant items.\n\n    This is specifically for use in renewal and enforces several checks\n    and policies to ensure that we can try to proceed with the renewal\n    request. The config argument is modified by including relevant options\n    read from the renewal configuration file.\n\n    :param configuration.NamespaceConfig config: configuration for the\n        current lineage\n    :param str full_path: Absolute path to the configuration file that\n        defines this lineage\n\n    :returns: the RenewableCert object or None if a fatal error occurred\n    :rtype: `storage.RenewableCert` or NoneType\n\n    \"\"\"\n    try:\n        renewal_candidate = storage.RenewableCert(full_path, config)\n    except (OSError, errors.CertStorageError) as error:\n        logger.error(\"Renewal configuration file %s is broken.\", full_path)\n        logger.error(\"The error was: %s\\nSkipping.\", str(error))\n        logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n        return None\n    if \"renewalparams\" not in renewal_candidate.configuration:\n        logger.error(\"Renewal configuration file %s lacks \"\n                       \"renewalparams. Skipping.\", full_path)\n        return None\n    renewalparams = renewal_candidate.configuration[\"renewalparams\"]\n    if \"authenticator\" not in renewalparams:\n        logger.error(\"Renewal configuration file %s does not specify \"\n                       \"an authenticator. Skipping.\", full_path)\n        return None\n\n    # Prior to Certbot v1.25.0, the default value of key_type (rsa) was not persisted to the\n    # renewal params. If the option is absent, it means the certificate was an RSA key.\n    # Restoring the option here is necessary to preserve the certificate key_type if\n    # the user has upgraded directly from Certbot <v1.25.0 to >=v2.0.0, where the default\n    # key_type was changed to ECDSA. See https://github.com/certbot/certbot/issues/9635.\n    renewalparams[\"key_type\"] = renewalparams.get(\"key_type\", \"rsa\")\n\n    # Now restore specific values along with their data types, if\n    # those elements are present.\n    renewalparams = _remove_deprecated_config_elements(renewalparams)\n    try:\n        restore_required_config_elements(config, renewalparams)\n        _restore_plugin_configs(config, renewalparams)\n    except (ValueError, errors.Error) as error:\n        logger.error(\n            \"An error occurred while parsing %s. The error was %s. \"\n            \"Skipping the file.\", full_path, str(error))\n        logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n        return None\n\n    try:\n        domains, ip_addresses = san.split(renewal_candidate.sans())\n        config.domains = domains\n        config.ip_addresses = ip_addresses\n    except errors.ConfigurationError as error:\n        logger.error(\"Renewal configuration file %s references a certificate \"\n                       \"that contains an invalid domain name. The problem \"\n                       \"was: %s. Skipping.\", full_path, error)\n        return None\n\n    return renewal_candidate\n\n\ndef _restore_webroot_config(config: configuration.NamespaceConfig,\n                            renewalparams: Mapping[str, Any]) -> None:\n    \"\"\"\n    webroot_map is, uniquely, a dict, and the general-purpose configuration\n    restoring logic is not able to correctly parse it from the serialized\n    form.\n    \"\"\"\n    if \"webroot_map\" in renewalparams and not config.set_by_user(\"webroot_map\"):\n        config.webroot_map = renewalparams[\"webroot_map\"]\n    # To understand why webroot_path and webroot_map processing are not mutually exclusive,\n    # see https://github.com/certbot/certbot/pull/7095\n    if \"webroot_path\" in renewalparams and not config.set_by_user(\"webroot_path\"):\n        wp = renewalparams[\"webroot_path\"]\n        if isinstance(wp, str):  # prior to 0.1.0, webroot_path was a string\n            wp = [wp]\n        config.webroot_path = wp\n\n\ndef _restore_plugin_configs(config: configuration.NamespaceConfig,\n                            renewalparams: Mapping[str, Any]) -> None:\n    \"\"\"Sets plugin specific values in config from renewalparams\n\n    :param configuration.NamespaceConfig config: configuration for the\n        current lineage\n    :param configobj.Section renewalparams: Parameters from the renewal\n        configuration file that defines this lineage\n\n    \"\"\"\n    # Now use parser to get plugin-prefixed items with correct types\n    # XXX: the current approach of extracting only prefixed items\n    #      related to the actually-used installer and authenticator\n    #      works as long as plugins don't need to read plugin-specific\n    #      variables set by someone else (e.g., assuming Apache\n    #      configurator doesn't need to read webroot_ variables).\n    # Note: if a parameter that used to be defined in the parser is no\n    #      longer defined, stored copies of that parameter will be\n    #      deserialized as strings by this logic even if they were\n    #      originally meant to be some other type.\n    plugin_prefixes: list[str] = []\n    if renewalparams[\"authenticator\"] == \"webroot\":\n        _restore_webroot_config(config, renewalparams)\n    else:\n        plugin_prefixes.append(renewalparams[\"authenticator\"])\n\n    if renewalparams.get(\"installer\") is not None:\n        plugin_prefixes.append(renewalparams[\"installer\"])\n\n    for plugin_prefix in set(plugin_prefixes):\n        plugin_prefix = plugin_prefix.replace('-', '_')\n        for config_item, config_value in renewalparams.items():\n            if config_item.startswith(plugin_prefix + \"_\") and not config.set_by_user(config_item):\n                # Values None, True, and False need to be treated specially,\n                # As their types aren't handled correctly by configobj\n                if config_value in (\"None\", \"True\", \"False\"):\n                    # bool(\"False\") == True\n                    # pylint: disable=eval-used\n                    setattr(config, config_item, eval(config_value))\n                else:\n                    cast = cli.argparse_type(config_item)\n                    setattr(config, config_item, cast(config_value))\n\n\ndef restore_required_config_elements(config: configuration.NamespaceConfig,\n                                     renewalparams: Mapping[str, Any]) -> None:\n    \"\"\"Sets non-plugin specific values in config from renewalparams\n\n    :param configuration.NamespaceConfig config: configuration for the\n        current lineage\n    :param configobj.Section renewalparams: parameters from the renewal\n        configuration file that defines this lineage\n\n    \"\"\"\n\n    updated_values = {}\n    required_items = itertools.chain(\n        ((\"pref_challs\", _restore_pref_challs),),\n        zip(BOOL_CONFIG_ITEMS, itertools.repeat(_restore_bool)),\n        zip(INT_CONFIG_ITEMS, itertools.repeat(_restore_int)),\n        zip(STR_CONFIG_ITEMS, itertools.repeat(_restore_str)))\n    for item_name, restore_func in required_items:\n        if item_name in renewalparams and not config.set_by_user(item_name):\n            value = restore_func(item_name, renewalparams[item_name])\n            updated_values[item_name] = value\n    for key, value in updated_values.items():\n        setattr(config, key, value)\n\n\ndef _remove_deprecated_config_elements(renewalparams: Mapping[str, Any]) -> dict[str, Any]:\n    \"\"\"Removes deprecated config options from the parsed renewalparams.\n\n    :param dict renewalparams: list of parsed renewalparams\n\n    :returns: list of renewalparams with deprecated config options removed\n    :rtype: dict\n\n    \"\"\"\n    return {option_name: v for (option_name, v) in renewalparams.items()\n        if option_name not in cli.DEPRECATED_OPTIONS}\n\n\ndef _restore_pref_challs(unused_name: str, value: Union[list[str], str]) -> list[str]:\n    \"\"\"Restores preferred challenges from a renewal config file.\n\n    If value is a `str`, it should be a single challenge type.\n\n    :param str unused_name: option name\n    :param value: option value\n    :type value: `list` of `str` or `str`\n\n    :returns: converted option value to be stored in the runtime config\n    :rtype: `list` of `str`\n\n    :raises errors.Error: if value can't be converted to a bool\n\n    \"\"\"\n    # If pref_challs has only one element, configobj saves the value\n    # with a trailing comma so it's parsed as a list. If this comma is\n    # removed by the user, the value is parsed as a str.\n    value = [value] if isinstance(value, str) else value\n    return cli.parse_preferred_challenges(value)\n\n\ndef _restore_bool(name: str, value: str) -> bool:\n    \"\"\"Restores a boolean key-value pair from a renewal config file.\n\n    :param str name: option name\n    :param str value: option value\n\n    :returns: converted option value to be stored in the runtime config\n    :rtype: bool\n\n    :raises errors.Error: if value can't be converted to a bool\n\n    \"\"\"\n    lowercase_value = value.lower()\n    if lowercase_value not in (\"true\", \"false\"):\n        raise errors.Error(f\"Expected True or False for {name} but found {value}\")\n    return lowercase_value == \"true\"\n\n\ndef _restore_int(name: str, value: str) -> int:\n    \"\"\"Restores an integer key-value pair from a renewal config file.\n\n    :param str name: option name\n    :param str value: option value\n\n    :returns: converted option value to be stored in the runtime config\n    :rtype: int\n\n    :raises errors.Error: if value can't be converted to an int\n\n    \"\"\"\n    if name == \"http01_port\" and value == \"None\":\n        logger.info(\"updating legacy http01_port value\")\n        return cli.flag_default(\"http01_port\")\n\n    try:\n        return int(value)\n    except ValueError:\n        raise errors.Error(f\"Expected a numeric value for {name}\")\n\n\ndef _restore_str(name: str, value: str) -> Optional[str]:\n    \"\"\"Restores a string key-value pair from a renewal config file.\n\n    :param str name: option name\n    :param str value: option value\n\n    :returns: converted option value to be stored in the runtime config\n    :rtype: str or None\n\n    \"\"\"\n    # To automatically migrate users from Let's Encrypt's old ACMEv1 URL, we replace the it here\n    # with the default ACME URL. It is still possible to override this choice with the explicit\n    # `--server` CLI flag.\n    if name == \"server\" and value == constants.V1_URI:\n        logger.info(\"Using server %s instead of legacy %s\",\n                    constants.CLI_DEFAULTS[\"server\"], value)\n        return constants.CLI_DEFAULTS[\"server\"]\n\n    return None if value == \"None\" else value\n\n\ndef should_renew(config: configuration.NamespaceConfig,\n                 lineage: storage.RenewableCert,\n                 ari_clients: AriClientPool) -> bool:\n    \"\"\"Return true if any of the circumstances for automatic renewal apply.\"\"\"\n    if config.renew_by_default:\n        logger.debug(\"Auto-renewal forced with --force-renewal...\")\n        return True\n    if config.dry_run:\n        logger.info(\"Certificate not due for renewal, but simulating renewal for dry run\")\n        return True\n    if should_autorenew(lineage, ari_clients):\n        logger.info(\"Certificate is due for renewal, auto-renewing...\")\n        return True\n    display_util.notify(\"Certificate not yet due for renewal\")\n    return False\n\n\ndef _ari_renewal_time(lineage: storage.RenewableCert,\n                     cert_pem: bytes,\n                     ari_clients: AriClientPool) -> Optional[datetime.datetime]:\n    \"\"\"Return the ARI suggested renewal time if it's available.\"\"\"\n    # For ARI requests, we want to use the ACME directory URL from which the\n    # cert was originally requested. Since `NamespaceConfig.server` can be overridden on\n    # the command line, we're using the server stored in the cert's renewal\n    # conf, i.e. `lineage.server`\n    #\n    # Fixes https://github.com/certbot/certbot/issues/10339\n    if not lineage.server:\n        logger.warning(\"Skipping ARI check because %s has no 'server' field. This issue will not \"\n                       \"prevent certificate renewal\", lineage.configfile.filename)\n        return None\n\n    ari_config_section = lineage.configfile.get(\"acme_renewal_info\", {})\n    retry_after = ari_config_section.get(ARI_RETRY_AFTER_CONFIG_ITEM, None)\n    if retry_after:\n        retry_after_datetime = datetime.datetime.fromisoformat(retry_after)\n        now = datetime.datetime.now()\n        if now < retry_after_datetime:\n            logger.debug(\"Skipped ACME Renewal Info check because ari_retry_after %s is in \"\n                         \"the future\",\n                         retry_after)\n            return None\n\n    renewal_time = None\n    try:\n        ari_client = ari_clients.get(lineage.server)\n        renewal_time, retry_after = ari_client.renewal_time(cert_pem)\n    except Exception:  # pylint: disable=broad-except\n        # We want to stop errors around ARI preventing renewal so we catch all exceptions here\n        # with a warning asking users to tell us about any problems they are experiencing\n        logger.warning(\"An error occurred requesting ACME Renewal Information (ARI). If this \"\n                       \"problem persists and you think it's a bug in Certbot, please open an \"\n                       \"issue at https://github.com/certbot/certbot/issues/new/choose.\")\n        logger.debug(\"Error while requesting ARI was:\", exc_info=True)\n        retry_after = datetime.datetime.now() + datetime.timedelta(seconds=60 * 60 * 6)\n\n\n    config_update = configobj.ConfigObj()\n    config_update[\"acme_renewal_info\"] = {\n        # Note: the ACME client returns naive (no timezone) datetimes for retry_after, and that\n        # is what we serialize here.\n        ARI_RETRY_AFTER_CONFIG_ITEM: retry_after.isoformat(timespec=\"seconds\"),\n    }\n    storage.atomic_rewrite(lineage.configfile.filename, config_update)\n    return renewal_time\n\n\ndef _default_renewal_time(cert_pem: bytes) -> datetime.datetime:\n    \"\"\"Return an reasonable default time to attempt renewal of the certificate\n    based on the certificate lifetime.\n\n    :param bytes cert_pem: cert as pem file\n\n    :returns: Time to attempt renewal\n    :rtype: `datetime.datetime`\n    \"\"\"\n    cert = x509.load_pem_x509_certificate(cert_pem)\n\n    not_before = cert.not_valid_before_utc\n    lifetime = cert.not_valid_after_utc - not_before\n    if lifetime.total_seconds() < 10 * 86400:\n        default_rt = not_before + lifetime / 2\n    else:\n        default_rt = not_before + lifetime * 2 / 3\n\n    return default_rt\n\ndef should_autorenew(lineage: storage.RenewableCert,\n                     ari_clients: AriClientPool) -> bool:\n    \"\"\"Should we now try to autorenew the most recent cert version?\n\n    If automatic renewal is disabled for the lineage, this function\n    immediately returns False.\n\n    Otherwise, if any of ACME Renewal Info (ARI), OCSP, or the\n    renew_before_expiry config option indicates we should renew, we\n    return True. If none of those values say we should renew and at\n    least one of ARI or renew_before_expiry has an available value, we\n    return False. We otherwise decide whether to renew based on the\n    certificate's remaining lifetime.\n\n    Note that this examines the numerically most recent cert version,\n    not the currently deployed version.\n\n    :returns: whether an attempt should now be made to autorenew the\n        most current cert version in this lineage\n    :rtype: bool\n\n    \"\"\"\n    if not lineage.autorenewal_is_enabled():\n        return False\n\n    cert = lineage.version(\"cert\", lineage.latest_common_version())\n    with open(cert, 'rb') as f:\n        cert_pem = f.read()\n\n    renewal_time = _ari_renewal_time(lineage, cert_pem, ari_clients)\n\n    now = datetime.datetime.now(datetime.timezone.utc)\n\n    if renewal_time and now > renewal_time:\n        return True\n\n    # Renewals on the basis of revocation\n    if lineage.ocsp_revoked(lineage.latest_common_version()):\n        logger.debug(\"Should renew, certificate is revoked.\")\n        return True\n\n    # If the renew_before_expiry config field is set, check if it says we should renew\n    config_interval = lineage.configuration.get(\"renew_before_expiry\")\n    if config_interval is not None:\n        notAfter = crypto_util.notAfter(cert)\n        if notAfter < storage.add_time_interval(now, config_interval):\n            logger.debug(\"Should renew, less than %s before certificate \"\n                            \"expiry %s.\", config_interval,\n                            notAfter.strftime(\"%Y-%m-%d %H:%M:%S %Z\"))\n            return True\n    # Only use the default if we don't have an ARI or renew_before_expiry value\n    elif renewal_time is None:\n        default_renewal_time = _default_renewal_time(cert_pem)\n        if now > default_renewal_time:\n            return True\n\n    return False\n\n\ndef _avoid_invalidating_lineage(config: configuration.NamespaceConfig,\n                                lineage: storage.RenewableCert, original_server: str) -> None:\n    \"\"\"Do not renew a valid cert with one from a staging server!\"\"\"\n    if util.is_staging(config.server):\n        if not util.is_staging(original_server):\n            if not config.break_my_certs:\n                names = san.display(lineage.sans())\n                raise errors.Error(\n                    \"You've asked to renew/replace a seemingly valid certificate with \"\n                    f\"a test certificate (domains: {names}). We will not do that \"\n                    \"unless you use the --break-my-certs flag!\")\n\n\ndef _avoid_reuse_key_conflicts(config: configuration.NamespaceConfig,\n                               lineage: storage.RenewableCert) -> None:\n    \"\"\"Don't allow combining --reuse-key with any flags that would conflict\n    with key reuse (--key-type, --rsa-key-size, --elliptic-curve), unless\n    --new-key is also set.\n    \"\"\"\n    # If --no-reuse-key is set, no conflict\n    if config.set_by_user(\"reuse_key\") and not config.reuse_key:\n        return\n\n    # If reuse_key is not set on the lineage and --reuse-key is not\n    # set on the CLI, no conflict.\n    if not lineage.reuse_key and not config.reuse_key:\n        return\n\n    # If --new-key is set, no conflict\n    if config.new_key:\n        return\n\n    kt = config.key_type.lower()\n\n    # The remaining cases where conflicts are present:\n    # - --key-type is set on the CLI and doesn't match the stored private key\n    # - It's an RSA key and --rsa-key-size is set and doesn't match\n    # - It's an ECDSA key and --eliptic-curve is set and doesn't match\n    potential_conflicts = [\n        (\"--key-type\",\n         lambda: kt != lineage.private_key_type.lower()),\n        (\"--rsa-key-size\",\n         lambda: kt == \"rsa\" and config.rsa_key_size != lineage.rsa_key_size),\n        (\"--elliptic-curve\",\n         lambda: kt == \"ecdsa\" and lineage.elliptic_curve and \\\n                 config.elliptic_curve.lower() != lineage.elliptic_curve.lower())\n    ]\n\n    for conflict in potential_conflicts:\n        if conflict[1]():\n            raise errors.Error(\n                f\"Unable to change the {conflict[0]} of this certificate because --reuse-key \"\n                \"is set. To stop reusing the private key, specify --no-reuse-key. \"\n                \"To change the private key this one time and then reuse it in future, \"\n                \"add --new-key.\")\n\n\ndef renew_cert(config: configuration.NamespaceConfig, sans: Optional[list[san.SAN]],\n               le_client: client.Client, lineage: storage.RenewableCert) -> None:\n    \"\"\"Renew a certificate lineage.\"\"\"\n    renewal_params = lineage.configuration[\"renewalparams\"]\n    original_server = renewal_params.get(\"server\", cli.flag_default(\"server\"))\n    _avoid_invalidating_lineage(config, lineage, original_server)\n    _avoid_reuse_key_conflicts(config, lineage)\n    if not sans:\n        sans = lineage.sans()\n    # The private key is the existing lineage private key if reuse_key is set.\n    # Otherwise, generate a fresh private key by passing None.\n    if config.reuse_key and not config.new_key:\n        new_key = os.path.normpath(lineage.privkey)\n        _update_renewal_params_from_key(new_key, config)\n    else:\n        new_key = None\n    new_cert, new_chain, new_key, _ = le_client.obtain_certificate(sans, new_key)\n    if config.dry_run:\n        logger.debug(\"Dry run: skipping updating lineage at %s\", os.path.dirname(lineage.cert))\n    else:\n        prior_version = lineage.latest_common_version()\n        # TODO: Check return value of save_successor\n        lineage.save_successor(prior_version, new_cert, new_key.pem, new_chain, config)\n        lineage.update_all_links_to(lineage.latest_common_version())\n        lineage.truncate()\n\n    hooks.deploy_hook(config, sans, lineage.live_dir)\n\n\ndef report(msgs: Iterable[str], category: str) -> str:\n    \"\"\"Format a results report for a category of renewal outcomes\"\"\"\n    lines = (\"%s (%s)\" % (m, category) for m in msgs)\n    return \"  \" + \"\\n  \".join(lines)\n\n\ndef _renew_describe_results(config: configuration.NamespaceConfig, renew_successes: list[str],\n                            renew_failures: list[str], renew_skipped: list[str],\n                            parse_failures: list[str]) -> None:\n    \"\"\"\n    Print a report to the terminal about the results of the renewal process.\n\n    :param configuration.NamespaceConfiguration config: Configuration\n    :param list renew_successes: list of fullchain paths which were renewed\n    :param list renew_failures: list of fullchain paths which failed to be renewed\n    :param list renew_skipped: list of messages to print about skipped certificates\n    :param list parse_failures: list of renewal parameter paths which had errors\n    \"\"\"\n    notify = display_util.notify\n    notify_error = logger.error\n\n    notify(f'\\n{display_obj.SIDE_FRAME}')\n\n    renewal_noun = \"simulated renewal\" if config.dry_run else \"renewal\"\n\n    if renew_skipped:\n        notify(\"The following certificates are not due for renewal yet:\")\n        notify(report(renew_skipped, \"skipped\"))\n    if not renew_successes and not renew_failures:\n        notify(f\"No {renewal_noun}s were attempted.\")\n        if (config.pre_hook is not None or\n                config.deploy_hook is not None or config.post_hook is not None):\n            notify(\"No hooks were run.\")\n    elif renew_successes and not renew_failures:\n        notify(f\"Congratulations, all {renewal_noun}s succeeded: \")\n        notify(report(renew_successes, \"success\"))\n    elif renew_failures and not renew_successes:\n        notify_error(\"All %ss failed. The following certificates could \"\n               \"not be renewed:\", renewal_noun)\n        notify_error(report(renew_failures, \"failure\"))\n    elif renew_failures and renew_successes:\n        notify(f\"The following {renewal_noun}s succeeded:\")\n        notify(report(renew_successes, \"success\") + \"\\n\")\n        notify_error(\"The following %ss failed:\", renewal_noun)\n        notify_error(report(renew_failures, \"failure\"))\n\n    if parse_failures:\n        notify(\"\\nAdditionally, the following renewal configurations \"\n               \"were invalid: \")\n        notify(report(parse_failures, \"parsefail\"))\n\n    notify(display_obj.SIDE_FRAME)\n\n\ndef handle_renewal_request(config: configuration.NamespaceConfig) -> None:\n    \"\"\"Examine each lineage; renew if due and report results\"\"\"\n\n    sans: list[san.SAN] = config.domains + config.ip_addresses\n\n    # This is trivially False if sans is empty\n    if any(str(san) not in config.webroot_map for san in sans):\n        # If more plugins start using cli.add_domain / cli.add_ip_address,\n        # we may want to only log a warning here\n        raise errors.Error(\"Currently, the renew verb is capable of either \"\n                           \"renewing all installed certificates that are due \"\n                           \"to be renewed or renewing a single certificate specified \"\n                           \"by its name using the --cert-name option (-d, --domain, and \"\n                           \"--ip-address are not valid options for the renew subcommand). If you \"\n                           \"would like to renew specific certificates by their identifiers, use \"\n                           \"the certonly command instead. The renew verb may provide other options \"\n                           \"for selecting certificates to renew in the future.\")\n\n    if config.certname:\n        conf_files = [storage.renewal_file_for_certname(config, config.certname)]\n    else:\n        conf_files = storage.renewal_conf_files(config)\n\n    renew_successes = []\n    renew_failures = []\n    renew_skipped = []\n    parse_failures = []\n\n    renewed_domains = []\n    failed_domains = []\n\n    # Noninteractive renewals include a random delay in order to spread\n    # out the load on the certificate authority servers, even if many\n    # users all pick the same time for renewals.  This delay precedes\n    # running any hooks, so that side effects of the hooks (such as\n    # shutting down a web service) aren't prolonged unnecessarily.\n    apply_random_sleep = not sys.stdin.isatty() and config.random_sleep_on_renew\n\n    # We initialize acme clients on a per-server basis, but most\n    # lineages use the same server. Memoize clients here so we can\n    # share the connection pool and reuse a single fetched directory.\n    ari_clients = AriClientPool(config)\n\n    for renewal_file in conf_files:\n        display_util.notification(\"Processing \" + renewal_file, pause=False)\n        lineage_config = copy.deepcopy(config)\n        assert renewal_file.endswith(\".conf\") # make sure lineagename_for_filename will not error\n        lineagename = storage.lineagename_for_filename(renewal_file)\n\n        # Note that this modifies config (to add back the configuration\n        # elements from within the renewal configuration file).\n        try:\n            renewal_candidate = reconstitute(lineage_config, renewal_file)\n        except Exception as e:  # pylint: disable=broad-except\n            logger.error(\"Renewal configuration file %s (cert: %s) \"\n                           \"produced an unexpected error: %s. Skipping.\",\n                           renewal_file, lineagename, e)\n            logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n            parse_failures.append(renewal_file)\n            continue\n\n        try:\n            if not renewal_candidate:\n                parse_failures.append(renewal_file)\n            else:\n                renewal_candidate.ensure_deployed()\n                from certbot._internal import main\n                plugins = plugins_disco.PluginsRegistry.find_all()\n                if should_renew(lineage_config, renewal_candidate, ari_clients):\n                    # Apply random sleep upon first renewal if needed\n                    if apply_random_sleep:\n                        sleep_time = random.uniform(1, 60 * 8)\n                        logger.info(\"Non-interactive renewal: random delay of %s seconds\",\n                                    sleep_time)\n                        time.sleep(sleep_time)\n                        # We will sleep only once this day, folks.\n                        apply_random_sleep = False\n\n                    # domains have been restored into lineage_config by reconstitute\n                    # but they're unnecessary anyway because renew_cert here\n                    # will just grab them from the certificate\n                    # we already know it's time to renew based on should_renew\n                    # and we have a lineage in renewal_candidate\n                    main.renew_cert(lineage_config, plugins, renewal_candidate)\n                    renew_successes.append(renewal_candidate.fullchain)\n                    renewed_domains.extend(renewal_candidate.sans())\n                else:\n                    expiry = crypto_util.notAfter(renewal_candidate.version(\n                        \"cert\", renewal_candidate.latest_common_version()))\n                    renew_skipped.append(\"%s expires on %s\" % (renewal_candidate.fullchain,\n                                         expiry.strftime(\"%Y-%m-%d\")))\n                # Run updater interface methods\n                updater.run_generic_updaters(lineage_config, renewal_candidate,\n                                             plugins)\n\n        except Exception as e:  # pylint: disable=broad-except\n            # obtain_certificate (presumably) encountered an unanticipated problem.\n            logger.error(\n                \"Failed to renew certificate %s with error: %s\",\n                lineagename, e\n            )\n            logger.debug(\"Traceback was:\\n%s\", traceback.format_exc())\n            if renewal_candidate:\n                renew_failures.append(renewal_candidate.fullchain)\n                failed_domains.extend(renewal_candidate.sans())\n\n    # Describe all the results\n    _renew_describe_results(config, renew_successes, renew_failures,\n                            renew_skipped, parse_failures)\n\n    hooks.run_saved_post_hooks(renewed_domains, failed_domains)\n\n    if renew_failures or parse_failures:\n        raise errors.Error(\n            f\"{len(renew_failures)} renew failure(s), {len(parse_failures)} parse failure(s)\")\n\n    logger.debug(\"no renewal failures\")\n\n\ndef _update_renewal_params_from_key(key_path: str, config: configuration.NamespaceConfig) -> None:\n    with open(key_path, 'rb') as file_h:\n        key = load_pem_private_key(file_h.read(), password=None, backend=default_backend())\n    if isinstance(key, rsa.RSAPrivateKey):\n        config.key_type = 'rsa'\n        config.rsa_key_size = key.key_size\n    elif isinstance(key, ec.EllipticCurvePrivateKey):\n        config.key_type = 'ecdsa'\n        config.elliptic_curve = key.curve.name\n    else:\n        raise errors.Error(f'Key at {key_path} is of an unsupported type: {type(key)}.')\n"
  },
  {
    "path": "certbot/src/certbot/_internal/san.py",
    "content": "\"\"\"Types for representing IP addresses and DNS names internal to Certbot.\"\"\"\nimport ipaddress\nfrom abc import abstractmethod\nfrom typing import Any, Iterable\n\nfrom acme import crypto_util as acme_crypto_util\nfrom cryptography import x509\n\nfrom certbot.util import enforce_domain_sanity\n\nclass SAN:\n    \"\"\"A domain or IP address.\n\n    These are Certbot-internal types, independent of the acme module's messages.Identifier.\n    \"\"\"\n    @abstractmethod\n    def is_wildcard(self) -> bool:\n        \"\"\"Return True if this is a wildcard DNS name.\"\"\"\n\nclass DNSName(SAN):\n    \"\"\"An FQDN or wildcard domain name.\n\n    Raises ConfigurationError if the domain name is syntactically invalid.\n\n    Normalizes inputs by converting to lowercase and removing a trailing dot, if present.\n    \"\"\"\n    def __init__(self, dns_name: str) -> None:\n        if not isinstance(dns_name, str):\n            raise TypeError(\"tried to initialize DNSName with non-str\")\n        self.dns_name = enforce_domain_sanity(dns_name)\n\n    def __str__(self) -> str:\n        return self.dns_name\n\n    def __hash__(self) -> int:\n        return hash(self.dns_name)\n\n    def __repr__(self) -> str:\n        return 'DNS(%s)' % self.dns_name\n\n    def __eq__(self, other: Any) -> bool:\n        match other:\n            case DNSName():\n                return self.dns_name == other.dns_name\n            case IPAddress():\n                return False\n            case _:\n                raise TypeError(f\"DNSName SAN compared to non-SAN: {type(other)}\")\n\n    def is_wildcard(self) -> bool:\n        \"\"\"Return True if this DNS name is a wildcard.\"\"\"\n        return self.dns_name.startswith('*.')\n\nclass IPAddress(SAN):\n    \"\"\"An IP address (IPv4 or IPv6).\n\n    Validated upon construction.\n    \"\"\"\n    def __init__(self, ip_address: str) -> None:\n        self.ip_address = ipaddress.ip_address(ip_address)\n\n    def __str__(self) -> str:\n        return str(self.ip_address)\n\n    def __hash__(self) -> int:\n        return hash(self.ip_address)\n\n    def __repr__(self) -> str:\n        return 'IP(%s)' % self.ip_address\n\n    def __eq__(self, other: Any) -> bool:\n        match other:\n            case IPAddress():\n                return self.ip_address == other.ip_address\n            case DNSName():\n                return False\n            case _:\n                raise TypeError(f\"IPAddress SAN compared to non-SAN: {type(other)}\")\n\n    def is_wildcard(self) -> bool:\n        \"\"\"Always False.\"\"\"\n        return False\n\ndef guess(names: Iterable[str]) -> list[SAN]:\n    \"\"\"Turn a list of strings in to a list of SANs based on how they parse.\"\"\"\n    sans: list[SAN] = []\n    for name in names:\n        try:\n            sans.append(IPAddress(name))\n        except ValueError:\n            sans.append(DNSName(name))\n    return sans\n\ndef split(sans: Iterable[SAN]) -> tuple[list[DNSName], list[IPAddress]]:\n    \"\"\"Split a list of SANs into a list of DNSNames and one of IPAddress, in that order.\"\"\"\n    domains = []\n    ip_addresses = []\n    for s in sans:\n        match s:\n            case IPAddress():\n                ip_addresses.append(s)\n            case DNSName():\n                domains.append(s)\n            case _:\n                raise TypeError(f\"SAN of type {type(s)}\")\n    return domains, ip_addresses\n\ndef join(dns_names: Iterable[DNSName], ip_addresses: Iterable[IPAddress]) -> list[SAN]:\n    \"\"\"Combine a list of DNS names and a list of IP addresses.\"\"\"\n    return list(dns_names) + list(ip_addresses)\n\ndef display(sans: Iterable[SAN]) -> str:\n    \"\"\"Return the list of SANs in string form, separated by comma and space.\"\"\"\n    return \", \".join(map(str, sans))\n\ndef from_x509(subject: x509.Name, exts: x509.Extensions) -> tuple[list[DNSName], list[IPAddress]]:\n    \"\"\"Get all DNS names and IP addresses, plus the first Common Name from subject.\n\n    The CN will be first in the list, if present. It will always be interpreted\n    as a DNS name.\n\n    :param subject: Name of the x509 object, which may include Common Name\n    :type subject: `cryptography.x509.Name`\n    :param exts: Extensions of the x509 object, which may include SANs\n    :type exts: `cryptography.x509.Extensions`\n\n    :returns: Tuple containing a list of DNSNames and a list of IPAddresses\n    \"\"\"\n    dns_names, ip_addresses = acme_crypto_util.get_identifiers_from_x509(subject, exts)\n    return [DNSName(d) for d in dns_names], [IPAddress(i) for i in ip_addresses]\n"
  },
  {
    "path": "certbot/src/certbot/_internal/snap_config.py",
    "content": "\"\"\"Module configuring Certbot in a snap environment\"\"\"\nfrom __future__ import annotations\nimport logging\nimport socket\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nfrom requests import PreparedRequest, Session\nfrom requests.adapters import HTTPAdapter\nfrom requests.exceptions import HTTPError\nfrom requests.exceptions import RequestException\n\nfrom certbot.compat import os\nfrom certbot.errors import Error\n\ntry:\n    from urllib3.connection import HTTPConnection\n    from urllib3.connectionpool import HTTPConnectionPool\nexcept ImportError:\n    # Stub imports for oldest requirements, that will never be used in snaps.\n    HTTPConnection = object  # type: ignore[misc,assignment]\n    HTTPConnectionPool = object  # type: ignore[misc,assignment]\n\n\n_ARCH_TRIPLET_MAP = {\n    'arm64': 'aarch64-linux-gnu',\n    'armhf': 'arm-linux-gnueabihf',\n    'i386': 'i386-linux-gnu',\n    'ppc64el': 'powerpc64le-linux-gnu',\n    'powerpc': 'powerpc-linux-gnu',\n    'amd64': 'x86_64-linux-gnu',\n    's390x': 's390x-linux-gnu',\n}\nCURRENT_PYTHON_VERSION_STRING = 'python3.12'\n\nLOGGER = logging.getLogger(__name__)\n\n\ndef prepare_env(cli_args: list[str]) -> list[str]:\n    \"\"\"\n    Prepare runtime environment for a certbot execution in snap.\n    :param list cli_args: List of command line arguments\n    :return: Update list of command line arguments\n    :rtype: list\n    \"\"\"\n    snap_arch = os.environ.get('SNAP_ARCH')\n\n    if snap_arch not in _ARCH_TRIPLET_MAP:\n        raise Error('Unrecognized value of SNAP_ARCH: {0}'.format(snap_arch))\n\n    os.environ['CERTBOT_AUGEAS_PATH'] = '{0}/usr/lib/{1}/libaugeas.so.0'.format(\n        os.environ.get('SNAP'), _ARCH_TRIPLET_MAP[snap_arch])\n\n    with Session() as session:\n        session.mount('http://snapd/', _SnapdAdapter())\n\n        try:\n            response = session.get('http://snapd/v2/connections?snap=certbot&interface=content',\n                                   timeout=30.0)\n            response.raise_for_status()\n        except RequestException as e:\n            if isinstance(e, HTTPError) and e.response.status_code == 404:\n                LOGGER.error('An error occurred while fetching Certbot snap plugins: '\n                             'your version of snapd is outdated.')\n                LOGGER.error('Please run \"sudo snap install core; sudo snap refresh core\" '\n                             'in your terminal and try again.')\n            else:\n                LOGGER.error('An error occurred while fetching Certbot snap plugins: '\n                             'make sure the snapd service is running.')\n            raise e\n\n    data = response.json()\n    connections = []\n    outdated_plugins = []\n    for plugin in data.get('result', {}).get('established', []):\n        plug: str = plugin.get('plug', {}).get('plug')\n        plug_content: str = plugin.get('plug-attrs', {}).get('content')\n        if plug == 'plugin' and plug_content == 'certbot-1':\n            plugin_name: str = plugin['slot']['snap']\n            # First, check that the plugin is using our expected python version,\n            # i.e. its \"read\" slot is something like\n            # \"$SNAP/lib/python3.12/site-packages\". If not, skip it and print an\n            # error.\n            slot_read: str = plugin.get('slot-attrs', {}).get('read', [])\n            if len(slot_read) != 0 and CURRENT_PYTHON_VERSION_STRING not in slot_read[0]:\n                outdated_plugins.append(plugin_name)\n                continue\n\n            connections.append('/snap/{0}/current/lib/{1}/site-packages/'.format(\n                plugin_name,\n                CURRENT_PYTHON_VERSION_STRING\n            ))\n\n    if outdated_plugins:\n        LOGGER.warning('The following plugins are using an outdated python version and must be '\n                    'updated to be compatible with Certbot 3.0. Please see '\n                    'https://community.letsencrypt.org/t/'\n                    'certbot-3-0-could-have-potential-third-party-snap-breakages/226940 '\n                    'for more information:')\n        plugin_list = '\\n'.join('  * {}'.format(plugin) for plugin in outdated_plugins)\n        LOGGER.warning(plugin_list)\n\n    os.environ['CERTBOT_PLUGIN_PATH'] = ':'.join(connections)\n\n    cli_args.append('--preconfigured-renewal')\n\n    return cli_args\n\n\nclass _SnapdConnection(HTTPConnection):\n    def __init__(self) -> None:\n        super().__init__(\"localhost\")\n        self.sock: Optional[socket.socket] = None\n\n    def connect(self) -> None:\n        self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)\n        self.sock.connect(\"/run/snapd.socket\")\n\n\nclass _SnapdConnectionPool(HTTPConnectionPool):\n    def __init__(self) -> None:\n        super().__init__(\"localhost\")\n\n    def _new_conn(self) -> _SnapdConnection:\n        return _SnapdConnection()\n\n\nclass _SnapdAdapter(HTTPAdapter):\n    # get_connection is used with versions of requests before 2.32.2 and\n    # get_connection_with_tls_context is used instead in versions after that. as of\n    # writing this, Certbot in EPEL 9 is still seeing updates and they have requests 2.25.1 so to\n    # help out those packagers while ensuring this code works reliably, we offer custom versions of\n    # both functions for now. when certbot does declare a dependency on requests>=2.32.2 in its\n    # setup.py files, get_connection can be deleted\n    def get_connection(self, url: str | bytes,\n                       proxies: Optional[Iterable[str]] = None) -> _SnapdConnectionPool:\n        return _SnapdConnectionPool()\n\n    def get_connection_with_tls_context(self, request: PreparedRequest,\n                                        verify: bool | str | None,\n                                        proxies: Optional[Iterable[str]] = None,\n                                        cert: Optional[Union[str, tuple[str,str]]] = None\n                                        ) -> _SnapdConnectionPool:\n        \"\"\"Required method for creating a new connection pool. Simply return our\n        shim that forces a UNIX socket connection to snapd.\"\"\"\n        return _SnapdConnectionPool()\n"
  },
  {
    "path": "certbot/src/certbot/_internal/storage.py",
    "content": "\"\"\"Renewable certificates storage.\"\"\"\n# pylint: disable=too-many-lines\nimport datetime\nimport glob\nimport logging\nimport re\nimport shutil\nimport stat\nfrom typing import Any\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\n\nimport configobj\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\nimport parsedatetime\n\nimport certbot\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import error_handler\nfrom certbot._internal import ocsp\nfrom certbot._internal import san\nfrom certbot._internal.plugins import disco as plugins_disco\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.plugins import common as plugins_common\nfrom certbot.util import parse_loose_version\n\nlogger = logging.getLogger(__name__)\n\nALL_FOUR = (\"cert\", \"privkey\", \"chain\", \"fullchain\")\nREADME = \"README\"\nCURRENT_VERSION = parse_loose_version(certbot.__version__)\nBASE_PRIVKEY_MODE = 0o600\n\n# pylint: disable=too-many-lines\n\n\ndef renewal_conf_files(config: configuration.NamespaceConfig) -> list[str]:\n    \"\"\"Build a list of all renewal configuration files.\n\n    :param configuration.NamespaceConfig config: Configuration object\n\n    :returns: list of renewal configuration files\n    :rtype: `list` of `str`\n\n    \"\"\"\n    result = glob.glob(os.path.join(config.renewal_configs_dir, \"*.conf\"))\n    result.sort()\n    return result\n\n\ndef renewal_file_for_certname(config: configuration.NamespaceConfig, certname: str) -> str:\n    \"\"\"Return /path/to/certname.conf in the renewal conf directory\"\"\"\n    path = os.path.join(config.renewal_configs_dir, f\"{certname}.conf\")\n    if not os.path.exists(path):\n        raise errors.CertStorageError(\n            f\"No certificate found with name {certname} (expected {path}).\")\n    return path\n\n\ndef cert_path_for_cert_name(config: configuration.NamespaceConfig, cert_name: str) -> str:\n    \"\"\" If `--cert-name` was specified, but you need a value for `--cert-path`.\n\n    :param configuration.NamespaceConfig config: parsed command line arguments\n    :param str cert_name: cert name.\n\n    \"\"\"\n    cert_name_implied_conf = renewal_file_for_certname(config, cert_name)\n    return configobj.ConfigObj(\n        cert_name_implied_conf, encoding='utf-8', default_encoding='utf-8')[\"fullchain\"]\n\n\ndef add_time_interval(base_time: datetime.datetime, interval: str,\n                      textparser: parsedatetime.Calendar = parsedatetime.Calendar()\n                      ) -> datetime.datetime:\n    \"\"\"Parse the time specified time interval, and add it to the base_time\n\n    The interval can be in the English-language format understood by\n    parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or\n    a sequence of such intervals like '6 months 1 week' or '3 days 12\n    hours'. If an integer is found with no associated unit, it is\n    interpreted by default as a number of days.\n\n    :param datetime.datetime base_time: The time to be added with the interval.\n    :param str interval: The time interval to parse.\n\n    :returns: The base_time plus the interpretation of the time interval.\n    :rtype: :class:`datetime.datetime`\"\"\"\n\n    if interval.strip().isdigit():\n        interval += \" days\"\n\n    # try to use the same timezone, but fallback to UTC\n    tzinfo = base_time.tzinfo or datetime.timezone.utc\n\n    return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]\n\n\ndef create_renewal_config_file(filename: str, archive_dir: str,\n                               target: Mapping[str, str],\n                               cli_config: configuration.NamespaceConfig) -> None:\n    \"\"\"Creates a renewal config file with the specified name and values.\n\n    :param str filename: Absolute path to the new destination of config file\n    :param str archive_dir: Absolute path to the archive directory\n    :param dict target: Maps ALL_FOUR to their symlink paths\n    :param .NamespaceConfig cli_config: parsed command line\n        arguments\n    \"\"\"\n    config = make_renewal_configobj(archive_dir, target, cli_config)\n    with open(filename, \"wb\") as f:\n        config.write(outfile=f)\n\n\ndef atomic_rewrite(config_filename: str, new_config: configobj.ConfigObj) -> None:\n    \"\"\"Update a config file on disk with the provided values.\n\n    The update will be atomic: if writing fails, the original file will not be modified. The\n    updated file will preserve comments from the original and will have the same permissions.\n\n    In general, fields that exist on disk but not in new_config will be preserved. As a special\n    case, fields in the 'renewalparams' section will be deleted unless they exist in new_config.\n    This deletion clears out old fields from when we used to dump _all_ flags (as opposed to\n    relevant ones).\n\n    :param str config_filename: Absolute path to the configuration file\n    :param configobj.ConfigObj new_config: New configuration object\n    \"\"\"\n    # Read the existing values from `config_filename`, or error (file_error=True)\n    merged_config = configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8',\n                                        file_error=True)\n    merged_config.merge(new_config)\n\n    # When updating the \"renewalparams\" section, only carry through fields that actually exist\n    # in the new config's \"renewalparams\" section. We could achieve this straightforwardly by\n    # assigning the new config's \"renewalparams\" section into the merged config. But then we would\n    # lose comments. Merging and then deleting allows us to preserve comments.\n    #\n    # As a concrete example, if a renewal params config has elliptic_curve=secp384r1, and the\n    # user executes a command to issue with `--key-type=rsa` instead, the old elliptic_curve value\n    # should disappear because it's not specified in the current command line.\n    #\n    # This only applies when \"renewalparams\" is actually being updated, because sometimes we\n    # update other sections independently (like \"acme_renewal_info\").\n    if \"renewalparams\" in new_config:\n        for k in merged_config[\"renewalparams\"]:\n            if k not in new_config[\"renewalparams\"]:\n                del merged_config[\"renewalparams\"][k]\n\n    current_permissions = stat.S_IMODE(os.lstat(config_filename).st_mode)\n\n    temp_filename = config_filename + \".new\"\n\n    # If an existing tempfile exists, delete it\n    if os.path.exists(temp_filename):\n        os.unlink(temp_filename)\n\n    with util.safe_open(temp_filename, \"wb\", current_permissions) as f:\n        merged_config.write(outfile=f)\n\n    filesystem.replace(temp_filename, config_filename)\n\n\ndef update_configuration(lineagename: str, archive_dir: str, target: Mapping[str, str],\n                         cli_config: configuration.NamespaceConfig) -> configobj.ConfigObj:\n    \"\"\"Modifies lineagename's config to contain the specified values.\n\n    :param str lineagename: Name of the lineage being modified\n    :param str archive_dir: Absolute path to the archive directory\n    :param dict target: Maps ALL_FOUR to their symlink paths\n    :param .NamespaceConfig cli_config: parsed command line arguments\n\n    :returns: Configuration object for the updated config file\n    :rtype: configobj.ConfigObj\n\n    \"\"\"\n    config = make_renewal_configobj(archive_dir, target, cli_config)\n\n    config_filename = renewal_filename_for_lineagename(cli_config, lineagename)\n\n    atomic_rewrite(config_filename, config)\n\n    return configobj.ConfigObj(config_filename, encoding='utf-8', default_encoding='utf-8')\n\n\ndef make_renewal_configobj(archive_dir: str, target: Mapping[str, str],\n                           cli_config: configuration.NamespaceConfig) -> configobj.ConfigObj:\n    \"\"\"Create a configobj.ConfigObj representing the provided values.\n\n    :param str archive_dir: Absolute path to the archive directory\n    :param dict target: Maps ALL_FOUR to their symlink paths\n    :param .NamespaceConfig cli_config: parsed command line arguments\n\n    :returns: Configuration object representing the inputs\n    :rtype: configobj.ConfigObj\n    \"\"\"\n    config = configobj.ConfigObj(encoding='utf-8', default_encoding='utf-8')\n    config[\"version\"] = certbot.__version__\n    config[\"archive_dir\"] = archive_dir\n    for kind in ALL_FOUR:\n        config[kind] = target[kind]\n    config['renewalparams'] = {}\n    config['renewalparams'].update(relevant_values(cli_config))\n\n    # MAGIC CODE ALERT\n    # In keeping with the code in RenewableCert.__init__, we use deploy_hook internally,\n    # but write out the value as renew_hook to allow downgrade compatibility.\n    # So, if there's a deploy_hook (the internal name), change it to renew_hook (the renewal\n    # config file name).\n    if \"deploy_hook\" in config[\"renewalparams\"]:\n        config[\"renewalparams\"][\"renew_hook\"] = config[\"renewalparams\"][\"deploy_hook\"]\n        del config[\"renewalparams\"][\"deploy_hook\"]\n\n    return config\n\n\ndef get_link_target(link: str) -> str:\n    \"\"\"Get an absolute path to the target of link.\n\n    :param str link: Path to a symbolic link\n\n    :returns: Absolute path to the target of link\n    :rtype: str\n\n    :raises .CertStorageError: If link does not exists.\n\n    \"\"\"\n    try:\n        target = filesystem.readlink(link)\n    except OSError:\n        raise errors.CertStorageError(\n            \"Expected {0} to be a symlink\".format(link))\n\n    if not os.path.isabs(target):\n        target = os.path.join(os.path.dirname(link), target)\n    return os.path.abspath(target)\n\n\ndef _write_live_readme_to(readme_path: str, is_base_dir: bool = False) -> None:\n    prefix = \"\"\n    if is_base_dir:\n        prefix = \"[cert name]/\"\n    with open(readme_path, \"w\") as f:\n        logger.debug(\"Writing README to %s.\", readme_path)\n        f.write(\"This directory contains your keys and certificates.\\n\\n\"\n                \"`{prefix}privkey.pem`  : the private key for your certificate.\\n\"\n                \"`{prefix}fullchain.pem`: the certificate file used in most server software.\\n\"\n                \"`{prefix}chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.\\n\"\n                \"`{prefix}cert.pem`     : will break many server configurations, and \"\n                                    \"should not be used\\n\"\n                \"                 without reading further documentation (see link below).\\n\\n\"\n                \"WARNING: DO NOT MOVE OR RENAME THESE FILES!\\n\"\n                \"         Certbot expects these files to remain in this location in order\\n\"\n                \"         to function properly!\\n\\n\"\n                \"We recommend not moving these files. For more information, see the Certbot\\n\"\n                \"User Guide at https://certbot.eff.org/docs/using.html#where-are-my-\"\n                                    \"certificates.\\n\".format(prefix=prefix))\n\n\ndef _relevant(namespaces: Iterable[str], option: str) -> bool:\n    \"\"\"\n    Is this option one that could be restored for future renewal purposes?\n\n    :param namespaces: plugin namespaces for configuration options\n    :type namespaces: `list` of `str`\n    :param str option: the name of the option\n\n    :rtype: bool\n    \"\"\"\n    from certbot._internal import renewal\n\n    return (option in renewal.CONFIG_ITEMS or\n            any(option.startswith(namespace) for namespace in namespaces))\n\n\ndef relevant_values(config: configuration.NamespaceConfig) -> dict[str, Any]:\n    \"\"\"Return a new dict containing only items relevant for renewal.\n\n    :param .NamespaceConfig config: parsed command line\n\n    :returns: A new dictionary containing items that can be used in renewal.\n    :rtype dict:\n\n    \"\"\"\n    all_values = config.to_dict()\n    plugins = plugins_disco.PluginsRegistry.find_all()\n    namespaces = [plugins_common.dest_namespace(plugin) for plugin in plugins]\n\n    rv = {\n        option: value\n        for option, value in all_values.items()\n        if _relevant(namespaces, option) and config.set_by_user(option)\n    }\n    # We always save the server value to help with forward compatibility\n    # and behavioral consistency when versions of Certbot with different\n    # server defaults are used.\n    rv[\"server\"] = all_values[\"server\"]\n\n    # Save key type to help with forward compatibility on Certbot's transition\n    # from RSA to ECDSA certificates by default.\n    rv[\"key_type\"] = all_values[\"key_type\"]\n\n    return rv\n\n\ndef lineagename_for_filename(config_filename: str) -> str:\n    \"\"\"Returns the lineagename for a configuration filename.\n    \"\"\"\n    if not config_filename.endswith(\".conf\"):\n        raise errors.CertStorageError(\n            \"renewal config file name must end in .conf\")\n    return os.path.basename(config_filename[:-len(\".conf\")])\n\n\ndef renewal_filename_for_lineagename(config: configuration.NamespaceConfig,\n                                     lineagename: str) -> str:\n    \"\"\"Returns the lineagename for a configuration filename.\n    \"\"\"\n    return os.path.join(config.renewal_configs_dir, lineagename) + \".conf\"\n\n\ndef _relpath_from_file(archive_dir: str, from_file: str) -> str:\n    \"\"\"Path to a directory from a file\"\"\"\n    return os.path.relpath(archive_dir, os.path.dirname(from_file))\n\n\ndef full_archive_path(config_obj: configobj.ConfigObj, cli_config: configuration.NamespaceConfig,\n                      lineagename: str) -> str:\n    \"\"\"Returns the full archive path for a lineagename\n\n    Uses cli_config to determine archive path if not available from config_obj.\n\n    :param configobj.ConfigObj config_obj: Renewal conf file contents (can be None)\n    :param configuration.NamespaceConfig cli_config: Main config file\n    :param str lineagename: Certificate name\n    \"\"\"\n    if config_obj and \"archive_dir\" in config_obj:\n        return config_obj[\"archive_dir\"]\n    return os.path.join(cli_config.default_archive_dir, lineagename)\n\n\ndef _full_live_path(cli_config: configuration.NamespaceConfig, lineagename: str) -> str:\n    \"\"\"Returns the full default live path for a lineagename\"\"\"\n    return os.path.join(cli_config.live_dir, lineagename)\n\n\ndef delete_files(config: configuration.NamespaceConfig, certname: str) -> None:\n    \"\"\"Delete all files related to the certificate.\n\n    If some files are not found, ignore them and continue.\n    \"\"\"\n    renewal_filename = renewal_file_for_certname(config, certname)\n    # file exists\n    full_default_archive_dir = full_archive_path(None, config, certname)\n    full_default_live_dir = _full_live_path(config, certname)\n    try:\n        renewal_config = configobj.ConfigObj(\n            renewal_filename, encoding='utf-8', default_encoding='utf-8')\n    except configobj.ConfigObjError:\n        # config is corrupted\n        logger.error(\"Could not parse %s. You may wish to manually \"\n            \"delete the contents of %s and %s.\", renewal_filename,\n            full_default_live_dir, full_default_archive_dir)\n        raise errors.CertStorageError(\n            \"error parsing {0}\".format(renewal_filename))\n    finally:\n        # we couldn't read it, but let's at least delete it\n        # if this was going to fail, it already would have.\n        os.remove(renewal_filename)\n        logger.info(\"Removed %s\", renewal_filename)\n\n    # cert files and (hopefully) live directory\n    # it's not guaranteed that the files are in our default storage\n    # structure. so, first delete the cert files.\n    directory_names = set()\n    for kind in ALL_FOUR:\n        link = renewal_config.get(kind)\n        try:\n            os.remove(link)\n            logger.debug(\"Removed %s\", link)\n        except OSError:\n            logger.debug(\"Unable to delete %s\", link)\n        directory = os.path.dirname(link)\n        directory_names.add(directory)\n\n    # if all four were in the same directory, and the only thing left\n    # is the README file (or nothing), delete that directory.\n    # this will be wrong in very few but some cases.\n    if len(directory_names) == 1:\n        # delete the README file\n        directory = directory_names.pop()\n        readme_path = os.path.join(directory, README)\n        try:\n            os.remove(readme_path)\n            logger.debug(\"Removed %s\", readme_path)\n        except OSError:\n            logger.debug(\"Unable to delete %s\", readme_path)\n        # if it's now empty, delete the directory\n        try:\n            os.rmdir(directory) # only removes empty directories\n            logger.debug(\"Removed %s\", directory)\n        except OSError:\n            logger.debug(\"Unable to remove %s; may not be empty.\", directory)\n\n    # archive directory\n    try:\n        archive_path = full_archive_path(renewal_config, config, certname)\n        shutil.rmtree(archive_path)\n        logger.debug(\"Removed %s\", archive_path)\n    except OSError:\n        logger.debug(\"Unable to remove %s\", archive_path)\n\n\nclass RenewableCert(interfaces.RenewableCert):\n    \"\"\"Renewable certificate.\n\n    Represents a lineage of certificates that is under the management of\n    Certbot, indicated by the existence of an associated renewal\n    configuration file.\n\n    Note that the notion of \"current version\" for a lineage is\n    maintained on disk in the structure of symbolic links, and is not\n    explicitly stored in any instance variable in this object. The\n    RenewableCert object is able to determine information about the\n    current (or other) version by accessing data on disk, but does not\n    inherently know any of this information except by examining the\n    symbolic links as needed. The instance variables mentioned below\n    point to symlinks that reflect the notion of \"current version\" of\n    each managed object, and it is these paths that should be used when\n    configuring servers to use the certificate managed in a lineage.\n    These paths are normally within the \"live\" directory, and their\n    symlink targets -- the actual cert files -- are normally found\n    within the \"archive\" directory.\n\n    :ivar str cert: The path to the symlink representing the current\n        version of the certificate managed by this lineage.\n    :ivar str privkey: The path to the symlink representing the current\n        version of the private key managed by this lineage.\n    :ivar str chain: The path to the symlink representing the current version\n        of the chain managed by this lineage.\n    :ivar str fullchain: The path to the symlink representing the\n        current version of the fullchain (combined chain and cert)\n        managed by this lineage.\n    :ivar configobj.ConfigObj configuration: The renewal configuration\n        options associated with this lineage, obtained from parsing the\n        renewal configuration file and/or systemwide defaults.\n\n    \"\"\"\n    def __init__(self, config_filename: str, cli_config: configuration.NamespaceConfig) -> None:\n        \"\"\"Instantiate a RenewableCert object from an existing lineage.\n\n        :param str config_filename: the path to the renewal config file\n            that defines this lineage.\n        :param .NamespaceConfig: parsed command line arguments\n\n        :raises .CertStorageError: if the configuration file's name didn't end\n            in \".conf\", or the file is missing or broken.\n\n        \"\"\"\n        self.cli_config = cli_config\n        self._lineagename = lineagename_for_filename(config_filename)\n\n        try:\n            self.configfile = configobj.ConfigObj(\n                config_filename, encoding='utf-8', default_encoding='utf-8')\n        except configobj.ConfigObjError:\n            raise errors.CertStorageError(\n                \"error parsing {0}\".format(config_filename))\n\n        # These are equivalent. Previously we were adding the unused default\n        # value of renew_before_expiry. Keeping both names because cleaning\n        # out the variables from callers is annoying. Ideally new code should\n        # use self.configfile so we can remove self.configuration at some point,\n        # but either should work currently.\n        self.configuration = self.configfile\n\n        if not all(x in self.configuration for x in ALL_FOUR):\n            raise errors.CertStorageError(\n                \"renewal config file {0} is missing a required \"\n                \"file reference\".format(self.configfile))\n\n        conf_version = self.configuration.get(\"version\")\n        if (conf_version is not None and\n                parse_loose_version(conf_version) > CURRENT_VERSION):\n            logger.info(\n                \"Attempting to parse the version %s renewal configuration \"\n                \"file found at %s with version %s of Certbot. This might not \"\n                \"work.\", conf_version, config_filename, certbot.__version__)\n\n        self.cert = self.configuration[\"cert\"]\n        self.privkey = self.configuration[\"privkey\"]\n        self.chain = self.configuration[\"chain\"]\n        self.fullchain = self.configuration[\"fullchain\"]\n        self.live_dir = os.path.dirname(self.cert)\n\n        # MAGIC CODE ALERT\n        # We changed the name of the internal property from deploy hook to renew hook\n        # There are already configs out there with renew_hook saved, so we're going to keep saving\n        # it out as renew_hook to allow downgrade compatibility. Load it in as deploy\n        # hook. Then, renewal.py's STR_CONFIG_ITEMS will check against the new internal name.\n        if \"renewalparams\" in self.configuration:\n            if \"renew_hook\" in self.configuration[\"renewalparams\"]:\n                self.configuration[\"renewalparams\"][\"deploy_hook\"] = \\\n                    self.configuration[\"renewalparams\"][\"renew_hook\"]\n                del self.configuration[\"renewalparams\"][\"renew_hook\"]\n\n        self._fix_symlinks()\n        self._check_symlinks()\n\n    @property\n    def key_path(self) -> str:\n        \"\"\"Duck type for self.privkey\"\"\"\n        return self.privkey\n\n    @property\n    def cert_path(self) -> str:\n        \"\"\"Duck type for self.cert\"\"\"\n        return self.cert\n\n    @property\n    def chain_path(self) -> str:\n        \"\"\"Duck type for self.chain\"\"\"\n        return self.chain\n\n    @property\n    def fullchain_path(self) -> str:\n        \"\"\"Duck type for self.fullchain\"\"\"\n        return self.fullchain\n\n    @property\n    def lineagename(self) -> str:\n        \"\"\"Name given to the certificate lineage.\n\n        :rtype: str\n\n        \"\"\"\n        return self._lineagename\n\n    @property\n    def target_expiry(self) -> datetime.datetime:\n        \"\"\"The current target certificate's expiration datetime\n\n        :returns: Expiration datetime of the current target certificate\n        :rtype: :class:`datetime.datetime`\n        \"\"\"\n        cert_path = self.current_target(\"cert\")\n        if not cert_path:\n            raise errors.Error(\"Target certificate does not exist.\")\n        return crypto_util.notAfter(cert_path)\n\n    @property\n    def archive_dir(self) -> str:\n        \"\"\"Returns the default or specified archive directory\"\"\"\n        return full_archive_path(self.configuration,\n            self.cli_config, self.lineagename)\n\n    def relative_archive_dir(self, from_file: str) -> str:\n        \"\"\"Returns the default or specified archive directory as a relative path\n\n        Used for creating symbolic links.\n        \"\"\"\n        return _relpath_from_file(self.archive_dir, from_file)\n\n    @property\n    def server(self) -> Optional[str]:\n        \"\"\"Returns the ACME server associated with this certificate\"\"\"\n        return self.configuration[\"renewalparams\"].get(\"server\", None)\n\n    @property\n    def is_test_cert(self) -> bool:\n        \"\"\"Returns true if this is a test cert from a staging server.\"\"\"\n        if self.server:\n            return util.is_staging(self.server)\n        return False\n\n    @property\n    def reuse_key(self) -> bool:\n        \"\"\"Returns whether this certificate is configured to reuse its private key\"\"\"\n        return \"reuse_key\" in self.configuration[\"renewalparams\"] and \\\n               self.configuration[\"renewalparams\"].as_bool(\"reuse_key\")\n\n    def _check_symlinks(self) -> None:\n        \"\"\"Raises an exception if a symlink doesn't exist\"\"\"\n        for kind in ALL_FOUR:\n            link = getattr(self, kind)\n            if not os.path.islink(link):\n                raise errors.CertStorageError(\n                    \"expected {0} to be a symlink\".format(link))\n            target = get_link_target(link)\n            if not os.path.exists(target):\n                raise errors.CertStorageError(\"target {0} of symlink {1} does \"\n                                              \"not exist\".format(target, link))\n\n    def _consistent(self) -> bool:\n        \"\"\"Are the files associated with this lineage self-consistent?\n\n        :returns: Whether the files stored in connection with this\n            lineage appear to be correct and consistent with one\n            another.\n        :rtype: bool\n\n        \"\"\"\n        # Each element must be referenced with an absolute path\n        for x in (self.cert, self.privkey, self.chain, self.fullchain):\n            if not os.path.isabs(x):\n                logger.debug(\"Element %s is not referenced with an \"\n                             \"absolute path.\", x)\n                return False\n\n        # Each element must exist and be a symbolic link\n        for x in (self.cert, self.privkey, self.chain, self.fullchain):\n            if not os.path.islink(x):\n                logger.debug(\"Element %s is not a symbolic link.\", x)\n                return False\n        for kind in ALL_FOUR:\n            link = getattr(self, kind)\n            target = get_link_target(link)\n\n            # Each element's link must point within the cert lineage's\n            # directory within the official archive directory\n            if not os.path.samefile(os.path.dirname(target), self.archive_dir):\n                logger.debug(\"Element's link does not point within the \"\n                             \"cert lineage's directory within the \"\n                             \"official archive directory. Link: %s, \"\n                             \"target directory: %s, \"\n                             \"archive directory: %s.\",\n                             link, os.path.dirname(target), self.archive_dir)\n                return False\n\n            # The link must point to a file that exists\n            if not os.path.exists(target):\n                logger.debug(\"Link %s points to file %s that does not exist.\",\n                             link, target)\n                return False\n\n            # The link must point to a file that follows the archive\n            # naming convention\n            pattern = re.compile(r\"^{0}([0-9]+)\\.pem$\".format(kind))\n            if not pattern.match(os.path.basename(target)):\n                logger.debug(\"%s does not follow the archive naming \"\n                             \"convention.\", target)\n                return False\n\n            # It is NOT required that the link's target be a regular\n            # file (it may itself be a symlink). But we should probably\n            # do a recursive check that ultimately the target does\n            # exist?\n        # XXX: Additional possible consistency checks (e.g.\n        #      cryptographic validation of the chain being a chain,\n        #      the chain matching the cert, and the cert matching\n        #      the subject key)\n        # XXX: All four of the targets are in the same directory\n        #      (This check is redundant with the check that they\n        #      are all in the desired directory!)\n        #      len(set(os.path.basename(self.current_target(x)\n        #      for x in ALL_FOUR))) == 1\n        return True\n\n    def _fix(self) -> None:\n        \"\"\"Attempt to fix defects or inconsistencies in this lineage.\n\n        .. todo:: Currently unimplemented.\n\n        \"\"\"\n        # TODO: Figure out what kinds of fixes are possible.  For\n        #       example, checking if there is a valid version that\n        #       we can update the symlinks to.  (Maybe involve\n        #       parsing keys and certs to see if they exist and\n        #       if a key corresponds to the subject key of a cert?)\n\n    # TODO: In general, the symlink-reading functions below are not\n    #       cautious enough about the possibility that links or their\n    #       targets may not exist.  (This shouldn't happen, but might\n    #       happen as a result of random tampering by a sysadmin, or\n    #       filesystem errors, or crashes.)\n\n    def _previous_symlinks(self) -> list[tuple[str, str]]:\n        \"\"\"Returns the kind and path of all symlinks used in recovery.\n\n        :returns: list of (kind, symlink) tuples\n        :rtype: list\n\n        \"\"\"\n        previous_symlinks = []\n        for kind in ALL_FOUR:\n            link_dir = os.path.dirname(getattr(self, kind))\n            link_base = \"previous_{0}.pem\".format(kind)\n            previous_symlinks.append((kind, os.path.join(link_dir, link_base)))\n\n        return previous_symlinks\n\n    def _fix_symlinks(self) -> None:\n        \"\"\"Fixes symlinks in the event of an incomplete version update.\n\n        If there is no problem with the current symlinks, this function\n        has no effect.\n\n        \"\"\"\n        previous_symlinks = self._previous_symlinks()\n        if all(os.path.exists(link[1]) for link in previous_symlinks):\n            for kind, previous_link in previous_symlinks:\n                current_link = getattr(self, kind)\n                if os.path.lexists(current_link):\n                    os.unlink(current_link)\n                os.symlink(filesystem.readlink(previous_link), current_link)\n\n        for _, link in previous_symlinks:\n            if os.path.exists(link):\n                os.unlink(link)\n\n    def current_target(self, kind: str) -> Optional[str]:\n        \"\"\"Returns full path to which the specified item currently points.\n\n        :param str kind: the lineage member item (\"cert\", \"privkey\",\n            \"chain\", or \"fullchain\")\n\n        :returns: The path to the current version of the specified\n            member.\n        :rtype: str or None\n\n        \"\"\"\n        if kind not in ALL_FOUR:\n            raise errors.CertStorageError(\"unknown kind of item\")\n        link = getattr(self, kind)\n        if not os.path.exists(link):\n            logger.debug(\"Expected symlink %s for %s does not exist.\",\n                         link, kind)\n            return None\n        return get_link_target(link)\n\n    def current_version(self, kind: str) -> Optional[int]:\n        \"\"\"Returns numerical version of the specified item.\n\n        For example, if kind is \"chain\" and the current chain link\n        points to a file named \"chain7.pem\", returns the integer 7.\n\n        :param str kind: the lineage member item (\"cert\", \"privkey\",\n            \"chain\", or \"fullchain\")\n\n        :returns: the current version of the specified member.\n        :rtype: int\n\n        \"\"\"\n        if kind not in ALL_FOUR:\n            raise errors.CertStorageError(\"unknown kind of item\")\n        pattern = re.compile(r\"^{0}([0-9]+)\\.pem$\".format(kind))\n        target = self.current_target(kind)\n        if target is None or not os.path.exists(target):\n            logger.debug(\"Current-version target for %s \"\n                         \"does not exist at %s.\", kind, target)\n            target = \"\"\n        matches = pattern.match(os.path.basename(target))\n        if matches:\n            return int(matches.groups()[0])\n        logger.debug(\"No matches for target %s.\", kind)\n        return None\n\n    def version(self, kind: str, version: int) -> str:\n        \"\"\"The filename that corresponds to the specified version and kind.\n\n        .. warning:: The specified version may not exist in this\n           lineage. There is no guarantee that the file path returned\n           by this method actually exists.\n\n        :param str kind: the lineage member item (\"cert\", \"privkey\",\n            \"chain\", or \"fullchain\")\n        :param int version: the desired version\n\n        :returns: The path to the specified version of the specified member.\n        :rtype: str\n\n        \"\"\"\n        if kind not in ALL_FOUR:\n            raise errors.CertStorageError(\"unknown kind of item\")\n        link = self.current_target(kind)\n        if not link:\n            raise errors.Error(f\"Target {kind} does not exist!\")\n        where = os.path.dirname(link)\n        return os.path.join(where, \"{0}{1}.pem\".format(kind, version))\n\n    def available_versions(self, kind: str) -> list[int]:\n        \"\"\"Which alternative versions of the specified kind of item exist?\n\n        The archive directory where the current version is stored is\n        consulted to obtain the list of alternatives.\n\n        :param str kind: the lineage member item (\n            ``cert``, ``privkey``, ``chain``, or ``fullchain``)\n\n        :returns: all of the version numbers that currently exist\n        :rtype: `list` of `int`\n\n        \"\"\"\n        if kind not in ALL_FOUR:\n            raise errors.CertStorageError(\"unknown kind of item\")\n        link = self.current_target(kind)\n        if not link:\n            raise errors.Error(f\"Target {kind} does not exist!\")\n        where = os.path.dirname(link)\n        files = os.listdir(where)\n        pattern = re.compile(r\"^{0}([0-9]+)\\.pem$\".format(kind))\n        matches = [pattern.match(f) for f in files]\n        return sorted([int(m.groups()[0]) for m in matches if m])\n\n    def newest_available_version(self, kind: str) -> int:\n        \"\"\"Newest available version of the specified kind of item?\n\n        :param str kind: the lineage member item (``cert``,\n            ``privkey``, ``chain``, or ``fullchain``)\n\n        :returns: the newest available version of this member\n        :rtype: int\n\n        \"\"\"\n        return max(self.available_versions(kind))\n\n    def latest_common_version(self) -> int:\n        \"\"\"Newest version for which all items are available?\n\n        :returns: the newest available version for which all members\n            (``cert, ``privkey``, ``chain``, and ``fullchain``) exist\n        :rtype: int\n\n        \"\"\"\n        # TODO: this can raise CertStorageError if there is no version overlap\n        #       (it should probably return None instead)\n        # TODO: this can raise a spurious AttributeError if the current\n        #       link for any kind is missing (it should probably return None)\n        versions = [self.available_versions(x) for x in ALL_FOUR]\n        return max(n for n in versions[0] if all(n in v for v in versions[1:]))\n\n    def next_free_version(self) -> int:\n        \"\"\"Smallest version newer than all full or partial versions?\n\n        :returns: the smallest version number that is larger than any\n            version of any item currently stored in this lineage\n        :rtype: int\n\n        \"\"\"\n        # TODO: consider locking/mutual exclusion between updating processes\n        # This isn't self.latest_common_version() + 1 because we don't want\n        # collide with a version that might exist for one file type but not\n        # for the others.\n        return max(self.newest_available_version(x) for x in ALL_FOUR) + 1\n\n    def ensure_deployed(self) -> bool:\n        \"\"\"Make sure we've deployed the latest version.\n\n        :returns: False if a change was needed, True otherwise\n        :rtype: bool\n\n        May need to recover from rare interrupted / crashed states.\"\"\"\n\n        if self.has_pending_deployment():\n            logger.warning(\"Found a new certificate /archive/ that was not \"\n                           \"linked to in /live/; fixing...\")\n            self.update_all_links_to(self.latest_common_version())\n            return False\n        return True\n\n    def has_pending_deployment(self) -> bool:\n        \"\"\"Is there a later version of all of the managed items?\n\n        :returns: ``True`` if there is a complete version of this\n            lineage with a larger version number than the current\n            version, and ``False`` otherwise\n        :rtype: bool\n\n        \"\"\"\n        all_versions: list[int] = []\n        for item in ALL_FOUR:\n            version = self.current_version(item)\n            if version is None:\n                raise errors.Error(f\"{item} is required but missing for this certificate.\")\n            all_versions.append(version)\n        # TODO: consider whether to assume consistency or treat\n        #       inconsistent/consistent versions differently\n        smallest_current = min(all_versions)\n        return smallest_current < self.latest_common_version()\n\n    def _update_link_to(self, kind: str, version: int) -> None:\n        \"\"\"Make the specified item point at the specified version.\n\n        (Note that this method doesn't verify that the specified version\n        exists.)\n\n        :param str kind: the lineage member item (\"cert\", \"privkey\",\n            \"chain\", or \"fullchain\")\n        :param int version: the desired version\n\n        \"\"\"\n        if kind not in ALL_FOUR:\n            raise errors.CertStorageError(\"unknown kind of item\")\n        link = getattr(self, kind)\n        filename = \"{0}{1}.pem\".format(kind, version)\n        # Relative rather than absolute target directory\n        target_directory = os.path.dirname(filesystem.readlink(link))\n        # TODO: it could be safer to make the link first under a temporary\n        #       filename, then unlink the old link, then rename the new link\n        #       to the old link; this ensures that this process is able to\n        #       create symlinks.\n        # TODO: we might also want to check consistency of related links\n        #       for the other corresponding items\n        os.unlink(link)\n        os.symlink(os.path.join(target_directory, filename), link)\n\n    def update_all_links_to(self, version: int) -> None:\n        \"\"\"Change all member objects to point to the specified version.\n\n        :param int version: the desired version\n\n        \"\"\"\n        with error_handler.ErrorHandler(self._fix_symlinks):\n            previous_links = self._previous_symlinks()\n            for kind, link in previous_links:\n                target = self.current_target(kind)\n                if not target:\n                    raise errors.Error(f\"Target {kind} does not exist!\")\n                os.symlink(target, link)\n\n            for kind in ALL_FOUR:\n                self._update_link_to(kind, version)\n\n            for _, link in previous_links:\n                os.unlink(link)\n\n    def names(self) -> list[str]:\n        \"\"\"Return the DNS names and IP addresses from this certificate as strings.\n\n        :returns: the subject names\n        :rtype: `list` of `str`\n        :raises .CertStorageError: if could not find cert file.\n        \"\"\"\n        return list(map(str, self.sans()))\n\n    def sans(self) -> list[san.SAN]:\n        \"\"\"Return the DNS names and IP addresses from this certificate as SAN objects.\n\n        :returns: the subject names\n        :rtype: `list` of `san.SAN`\n        :raises .CertStorageError: if could not find cert file.\n\n        \"\"\"\n        target = self.current_target(\"cert\")\n        if target is None:\n            raise errors.CertStorageError(\"could not find the certificate file\")\n        with open(target, \"rb\") as f:\n            cert_bytes = f.read()\n        x509_cert = x509.load_pem_x509_certificate(cert_bytes)\n        dns_names, ip_addrs = san.from_x509(x509_cert.subject, x509_cert.extensions)\n        return cast(list[san.SAN], dns_names + ip_addrs)\n\n    def ocsp_revoked(self, version: int) -> bool:\n        \"\"\"Is the specified cert version revoked according to OCSP?\n\n        Also returns True if the cert version is declared as revoked\n        according to OCSP. If OCSP status could not be determined, False\n        is returned.\n\n        :param int version: the desired version number\n\n        :returns: True if the certificate is revoked, otherwise, False\n        :rtype: bool\n\n        \"\"\"\n        cert_path = self.version(\"cert\", version)\n        chain_path = self.version(\"chain\", version)\n        # While the RevocationChecker should return False if it failed to\n        # determine the OCSP status, let's ensure we don't crash Certbot by\n        # catching all exceptions here.\n        try:\n            return ocsp.RevocationChecker().ocsp_revoked_by_paths(cert_path,\n                                                                  chain_path)\n        except Exception as e:  # pylint: disable=broad-except\n            logger.warning(\n                \"An error occurred determining the OCSP status of %s.\",\n                cert_path)\n            logger.debug(str(e))\n            return False\n\n    def autorenewal_is_enabled(self) -> bool:\n        \"\"\"Is automatic renewal enabled for this cert?\n\n        If autorenew is not specified, defaults to True.\n\n        :returns: True if automatic renewal is enabled\n        :rtype: bool\n\n        \"\"\"\n        return (\"autorenew\" not in self.configuration[\"renewalparams\"] or\n                self.configuration[\"renewalparams\"].as_bool(\"autorenew\"))\n\n    @classmethod\n    def new_lineage(cls, lineagename: str, cert: bytes, privkey: bytes, chain: bytes,\n                    cli_config: configuration.NamespaceConfig) -> \"RenewableCert\":\n        \"\"\"Create a new certificate lineage.\n\n        Attempts to create a certificate lineage -- enrolled for\n        potential future renewal -- with the (suggested) lineage name\n        lineagename, and the associated cert, privkey, and chain (the\n        associated fullchain will be created automatically). Optional\n        configurator and renewalparams record the configuration that was\n        originally used to obtain this cert, so that it can be reused\n        later during automated renewal.\n\n        Returns a new RenewableCert object referring to the created\n        lineage. (The actual lineage name, as well as all the relevant\n        file paths, will be available within this object.)\n\n        :param str lineagename: the suggested name for this lineage\n            (normally the current cert's first subject DNS name)\n        :param str cert: the initial certificate version in PEM format\n        :param str privkey: the private key in PEM format\n        :param str chain: the certificate chain in PEM format\n        :param .NamespaceConfig cli_config: parsed command line\n            arguments\n\n        :returns: the newly-created RenewalCert object\n        :rtype: :class:`storage.renewableCert`\n\n        \"\"\"\n\n        # Examine the configuration and find the new lineage's name\n        for i in (cli_config.renewal_configs_dir, cli_config.default_archive_dir,\n                  cli_config.live_dir):\n            if not os.path.exists(i):\n                filesystem.makedirs(i, 0o700)\n                logger.debug(\"Creating directory %s.\", i)\n        config_file, config_filename = util.unique_lineage_name(\n            cli_config.renewal_configs_dir, lineagename)\n        base_readme_path = os.path.join(cli_config.live_dir, README)\n        if not os.path.exists(base_readme_path):\n            _write_live_readme_to(base_readme_path, is_base_dir=True)\n\n        # Determine where on disk everything will go\n        # lineagename will now potentially be modified based on which\n        # renewal configuration file could actually be created\n        lineagename = lineagename_for_filename(config_filename)\n        archive = full_archive_path(None, cli_config, lineagename)\n        live_dir = _full_live_path(cli_config, lineagename)\n        if os.path.exists(archive) and (not os.path.isdir(archive) or os.listdir(archive)):\n            config_file.close()\n            raise errors.CertStorageError(\n                \"archive directory exists for \" + lineagename)\n        if os.path.exists(live_dir) and (not os.path.isdir(live_dir) or os.listdir(live_dir)):\n            config_file.close()\n            raise errors.CertStorageError(\n                \"live directory exists for \" + lineagename)\n        for i in (archive, live_dir):\n            if not os.path.exists(i):\n                filesystem.makedirs(i)\n                logger.debug(\"Creating directory %s.\", i)\n\n        # Put the data into the appropriate files on disk\n        target = {kind: os.path.join(live_dir, kind + \".pem\") for kind in ALL_FOUR}\n        archive_target = {kind: os.path.join(archive, kind + \"1.pem\") for kind in ALL_FOUR}\n        for kind in ALL_FOUR:\n            os.symlink(_relpath_from_file(archive_target[kind], target[kind]), target[kind])\n        with open(target[\"cert\"], \"wb\") as f_b:\n            logger.debug(\"Writing certificate to %s.\", target[\"cert\"])\n            f_b.write(cert)\n        with util.safe_open(archive_target[\"privkey\"], \"wb\", chmod=BASE_PRIVKEY_MODE) as f_a:\n            logger.debug(\"Writing private key to %s.\", target[\"privkey\"])\n            f_a.write(privkey)\n            # XXX: Let's make sure to get the file permissions right here\n        with open(target[\"chain\"], \"wb\") as f_b:\n            logger.debug(\"Writing chain to %s.\", target[\"chain\"])\n            f_b.write(chain)\n        with open(target[\"fullchain\"], \"wb\") as f_b:\n            # assumes the cert includes ending newline character\n            logger.debug(\"Writing full chain to %s.\", target[\"fullchain\"])\n            f_b.write(cert + chain)\n\n        # Write a README file to the live directory\n        readme_path = os.path.join(live_dir, README)\n        _write_live_readme_to(readme_path)\n\n        # Document what we've done in a new renewal config file\n        config_file.close()\n\n        create_renewal_config_file(config_filename, archive, target, cli_config)\n        return cls(config_filename, cli_config)\n\n    def _private_key(self) -> Union[RSAPrivateKey, EllipticCurvePrivateKey]:\n        with open(self.configuration[\"privkey\"], \"rb\") as priv_key_file:\n            key = load_pem_private_key(\n                data=priv_key_file.read(),\n                password=None,\n                backend=default_backend()\n            )\n            return cast(Union[RSAPrivateKey, EllipticCurvePrivateKey], key)\n\n    @property\n    def private_key_type(self) -> str:\n        \"\"\"\n        :returns: The type of algorithm for the private, RSA or ECDSA\n        :rtype: str\n        \"\"\"\n        key = self._private_key()\n        if isinstance(key, RSAPrivateKey):\n            return \"RSA\"\n        return \"ECDSA\"\n\n    @property\n    def rsa_key_size(self) -> Optional[int]:\n        \"\"\"\n        :returns: If the private key is an RSA key, its size.\n        :rtype: int\n        \"\"\"\n        key = self._private_key()\n        if isinstance(key, RSAPrivateKey):\n            return key.key_size\n        return None\n\n    @property\n    def elliptic_curve(self) -> Optional[str]:\n        \"\"\"\n        :returns: If the private key is an elliptic key, the name of its curve.\n        :rtype: str\n        \"\"\"\n        key = self._private_key()\n        if isinstance(key, EllipticCurvePrivateKey):\n            return key.curve.name\n        return None\n\n    def save_successor(self, prior_version: int, new_cert: bytes, new_privkey: bytes,\n                       new_chain: bytes, cli_config: configuration.NamespaceConfig) -> int:\n        \"\"\"Save new cert and chain as a successor of a prior version.\n\n        Returns the new version number that was created.\n\n        .. note:: this function does NOT update links to deploy this\n                  version\n\n        :param int prior_version: the old version to which this version\n            is regarded as a successor (used to choose a privkey, if the\n            key has not changed, but otherwise this information is not\n            permanently recorded anywhere)\n        :param bytes new_cert: the new certificate, in PEM format\n        :param bytes new_privkey: the new private key, in PEM format,\n            or ``None``, if the private key has not changed\n        :param bytes new_chain: the new chain, in PEM format\n        :param .NamespaceConfig cli_config: parsed command line\n            arguments\n\n        :returns: the new version number that was created\n        :rtype: int\n\n        \"\"\"\n        # XXX: assumes official archive location rather than examining links\n        # XXX: consider using os.open for availability of os.O_EXCL\n        # XXX: ensure file permissions are correct; also create directories\n        #      if needed (ensuring their permissions are correct)\n        # Figure out what the new version is and hence where to save things\n\n        self.cli_config = cli_config\n        target_version = self.next_free_version()\n        target = {kind: os.path.join(self.archive_dir, \"{0}{1}.pem\".format(kind, target_version))\n                  for kind in ALL_FOUR}\n\n        old_privkey = os.path.join(\n            self.archive_dir, \"privkey{0}.pem\".format(prior_version))\n\n        # Distinguish the cases where the privkey has changed and where it\n        # has not changed (in the latter case, making an appropriate symlink\n        # to an earlier privkey version)\n        if new_privkey is None:\n            # The behavior below keeps the prior key by creating a new\n            # symlink to the old key or the target of the old key symlink.\n            if os.path.islink(old_privkey):\n                old_privkey = filesystem.readlink(old_privkey)\n            else:\n                old_privkey = f\"privkey{prior_version}.pem\"\n            logger.debug(\"Writing symlink to old private key, %s.\", old_privkey)\n            os.symlink(old_privkey, target[\"privkey\"])\n        else:\n            with util.safe_open(target[\"privkey\"], \"wb\", chmod=BASE_PRIVKEY_MODE) as f:\n                logger.debug(\"Writing new private key to %s.\", target[\"privkey\"])\n                f.write(new_privkey)\n            # Preserve gid and (mode & MASK_FOR_PRIVATE_KEY_PERMISSIONS)\n            # from previous privkey in this lineage.\n            mode = filesystem.compute_private_key_mode(old_privkey, BASE_PRIVKEY_MODE)\n            filesystem.copy_ownership_and_apply_mode(\n                old_privkey, target[\"privkey\"], mode, copy_user=False, copy_group=True)\n\n        # Save everything else\n        with open(target[\"cert\"], \"wb\") as f:\n            logger.debug(\"Writing certificate to %s.\", target[\"cert\"])\n            f.write(new_cert)\n        with open(target[\"chain\"], \"wb\") as f:\n            logger.debug(\"Writing chain to %s.\", target[\"chain\"])\n            f.write(new_chain)\n        with open(target[\"fullchain\"], \"wb\") as f:\n            logger.debug(\"Writing full chain to %s.\", target[\"fullchain\"])\n            f.write(new_cert + new_chain)\n\n        symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR}\n        # Update renewal config file\n        self.configfile = update_configuration(\n            self.lineagename, self.archive_dir, symlinks, cli_config)\n        self.configuration = self.configfile\n\n        return target_version\n\n    def save_new_config_values(self, cli_config: configuration.NamespaceConfig) -> None:\n        \"\"\"Save only the config information without writing the new cert.\n\n        :param .NamespaceConfig cli_config: parsed command line\n            arguments\n        \"\"\"\n        self.cli_config = cli_config\n        symlinks = {kind: self.configuration[kind] for kind in ALL_FOUR}\n        # Update renewal config file\n        self.configfile = update_configuration(\n            self.lineagename, self.archive_dir, symlinks, cli_config)\n        self.configuration = self.configfile\n\n    def truncate(self, num_prior_certs_to_keep: int = 5) -> None:\n        \"\"\"Delete unused historical certificate, chain and key items from the lineage.\n\n        A certificate version will be deleted if it is:\n          1. not the current target, and\n          2. not a previous version within num_prior_certs_to_keep.\n\n        :param num_prior_certs_to_keep: How many prior certificate versions to keep.\n\n        \"\"\"\n        # Do not want to delete the current or the previous num_prior_certs_to_keep certs\n        current_version = self.latest_common_version()\n        versions_to_delete = set(self.available_versions(\"cert\"))\n        versions_to_delete -= set(range(current_version,\n                                        current_version - 1 - num_prior_certs_to_keep, -1))\n        archive = self.archive_dir\n\n        # Delete the remaining lineage items kinds for those certificate versions.\n        for ver in versions_to_delete:\n            logger.debug(\"Deleting %s/cert%d.pem and related items during clean up\",\n                         archive, ver)\n            for kind in ALL_FOUR:\n                item_path = os.path.join(archive, f\"{kind}{ver}.pem\")\n                try:\n                    if os.path.exists(item_path):\n                        os.unlink(item_path)\n                except OSError:\n                    logger.debug(\"Failed to clean up %s\", item_path, exc_info=True)\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/__init__.py",
    "content": "\"\"\"certbot tests\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/account_test.py",
    "content": "\"\"\"Tests for certbot._internal.account.\"\"\"\nimport datetime\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import messages\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import misc\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\nKEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass AccountTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.account.Account.\"\"\"\n\n    def setUp(self):\n        from certbot._internal.account import Account\n        self.regr = mock.MagicMock()\n        self.meta = Account.Meta(\n            creation_host=\"test.certbot.org\",\n            creation_dt=datetime.datetime(\n                2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc))\n        self.acc = Account(self.regr, KEY, self.meta)\n        self.regr.__repr__ = mock.MagicMock(return_value=\"i_am_a_regr\")\n\n        with mock.patch(\"certbot._internal.account.socket\") as mock_socket:\n            mock_socket.gethostname.return_value = \"test.certbot.org\"\n            with mock.patch(\"certbot._internal.account.datetime\") as mock_dt:\n                mock_dt.datetime.now.return_value = self.meta.creation_dt\n                self.acc_no_meta = Account(self.regr, KEY)\n\n    def test_init(self):\n        assert self.regr == self.acc.regr\n        assert KEY == self.acc.key\n        assert self.meta == self.acc_no_meta.meta\n\n    def test_id(self):\n        assert self.acc.id == \"7adac10320f585ddf118429c0c4af2cd\"\n\n    def test_slug(self):\n        assert self.acc.slug == \"test.certbot.org@2015-07-04T14:04:10Z (7ada)\"\n\n    def test_repr(self):\n        assert repr(self.acc).startswith(\n          \"<Account(i_am_a_regr, 7adac10320f585ddf118429c0c4af2cd, Meta(\")\n\n\nclass MetaTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.account.Meta.\"\"\"\n    def test_deserialize_partial(self):\n        from certbot._internal.account import Account\n        meta = Account.Meta.json_loads(\n            '{'\n            '   \"creation_dt\": \"2020-06-13T07:46:45Z\",'\n            '   \"creation_host\": \"hyperion.localdomain\"'\n            '}')\n        assert meta.creation_dt is not None\n        assert meta.creation_host is not None\n        assert meta.register_to_eff is None\n\n    def test_deserialize_full(self):\n        from certbot._internal.account import Account\n        meta = Account.Meta.json_loads(\n            '{'\n            '   \"creation_dt\": \"2020-06-13T07:46:45Z\",'\n            '   \"creation_host\": \"hyperion.localdomain\",'\n            '   \"register_to_eff\": \"bar\"'\n            '}')\n        assert meta.creation_dt is not None\n        assert meta.creation_host is not None\n        assert meta.register_to_eff is not None\n\n\nclass AccountMemoryStorageTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.account.AccountMemoryStorage.\"\"\"\n\n    def setUp(self):\n        from certbot._internal.account import AccountMemoryStorage\n        self.storage = AccountMemoryStorage()\n\n    def test_it(self):\n        account = mock.Mock(id=\"x\")\n        assert [] == self.storage.find_all()\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.load(\"x\")\n        self.storage.save(account, None)\n        assert [account] == self.storage.find_all()\n        assert account == self.storage.load(\"x\")\n        self.storage.save(account, None)\n        assert [account] == self.storage.find_all()\n\n\nclass AccountFileStorageTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.account.AccountFileStorage.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot._internal.account import AccountFileStorage\n        self.storage = AccountFileStorage(self.config)\n\n        from certbot._internal.account import Account\n        meta = Account.Meta(\n            creation_host=\"test.example.org\",\n            creation_dt=datetime.datetime(\n                2021, 1, 5, 14, 4, 10, tzinfo=datetime.timezone.utc))\n        self.acc = Account(\n            regr=messages.RegistrationResource(\n                uri=None, body=messages.Registration()),\n            key=KEY,\n            meta=meta)\n        self.mock_client = mock.MagicMock()\n\n    def test_init_creates_dir(self):\n        assert os.path.isdir(\n            misc.underscores_for_unsupported_characters_in_path(self.config.accounts_dir))\n\n    def test_save_and_restore(self):\n        self.storage.save(self.acc, self.mock_client)\n        account_path = os.path.join(self.config.accounts_dir, self.acc.id)\n        assert os.path.exists(account_path)\n        for file_name in \"regr.json\", \"meta.json\", \"private_key.json\":\n            assert os.path.exists(\n                os.path.join(account_path, file_name))\n        assert filesystem.check_mode(os.path.join(account_path, \"private_key.json\"), 0o400)\n\n        # restore\n        loaded = self.storage.load(self.acc.id)\n        assert self.acc == loaded\n\n    def test_update_regr(self):\n        self.storage.update_regr(self.acc)\n        account_path = os.path.join(self.config.accounts_dir, self.acc.id)\n        assert os.path.exists(account_path)\n        assert os.path.exists(os.path.join(account_path, \"regr.json\"))\n\n        assert not os.path.exists(os.path.join(account_path, \"meta.json\"))\n        assert not os.path.exists(os.path.join(account_path, \"private_key.json\"))\n\n    def test_update_meta(self):\n        self.storage.update_meta(self.acc)\n        account_path = os.path.join(self.config.accounts_dir, self.acc.id)\n        assert os.path.exists(account_path)\n        assert os.path.exists(os.path.join(account_path, \"meta.json\"))\n\n        assert not os.path.exists(os.path.join(account_path, \"regr.json\"))\n        assert not os.path.exists(os.path.join(account_path, \"private_key.json\"))\n\n    def test_find_all(self):\n        self.storage.save(self.acc, self.mock_client)\n        assert [self.acc] == self.storage.find_all()\n\n    def test_find_all_none_empty_list(self):\n        assert [] == self.storage.find_all()\n\n    def test_find_all_accounts_dir_absent(self):\n        os.rmdir(self.config.accounts_dir)\n        assert [] == self.storage.find_all()\n\n    def test_find_all_load_skips(self):\n        # pylint: disable=protected-access\n        self.storage._load_for_server_path = mock.MagicMock(\n            side_effect=[\"x\", errors.AccountStorageError, \"z\"])\n        with mock.patch(\"certbot._internal.account.os.listdir\") as mock_listdir:\n            mock_listdir.return_value = [\"x\", \"y\", \"z\"]\n            assert [\"x\", \"z\"] == self.storage.find_all()\n\n    def test_load_non_existent_raises_error(self):\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.load(\"missing\")\n\n    def _set_server(self, server):\n        self.config.server = server\n        from certbot._internal.account import AccountFileStorage\n        self.storage = AccountFileStorage(self.config)\n\n    def test_find_all_neither_exists(self):\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        assert [] == self.storage.find_all()\n        assert [] == self.storage.find_all()\n        assert not os.path.islink(self.config.accounts_dir)\n\n    def test_find_all_find_before_save(self):\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        assert [] == self.storage.find_all()\n        self.storage.save(self.acc, self.mock_client)\n        assert [self.acc] == self.storage.find_all()\n        assert [self.acc] == self.storage.find_all()\n        assert not os.path.islink(self.config.accounts_dir)\n        # we shouldn't have created a v1 account\n        prev_server_path = 'https://acme-staging.api.letsencrypt.org/directory'\n        assert not os.path.isdir(self.config.accounts_dir_for_server_path(prev_server_path))\n\n    def test_find_all_save_before_find(self):\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        assert [self.acc] == self.storage.find_all()\n        assert [self.acc] == self.storage.find_all()\n        assert not os.path.islink(self.config.accounts_dir)\n        assert os.path.isdir(self.config.accounts_dir)\n        prev_server_path = 'https://acme-staging.api.letsencrypt.org/directory'\n        assert not os.path.isdir(self.config.accounts_dir_for_server_path(prev_server_path))\n\n    def test_find_all_server_downgrade(self):\n        # don't use v2 accounts with a v1 url\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        assert [] == self.storage.find_all()\n        self.storage.save(self.acc, self.mock_client)\n        assert [self.acc] == self.storage.find_all()\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        assert [] == self.storage.find_all()\n\n    def test_upgrade_version_staging(self):\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        assert [self.acc] == self.storage.find_all()\n\n    def test_upgrade_version_production(self):\n        self._set_server('https://acme-v01.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        self._set_server('https://acme-v02.api.letsencrypt.org/directory')\n        assert [self.acc] == self.storage.find_all()\n\n    @mock.patch('certbot.compat.os.rmdir')\n    def test_corrupted_account(self, mock_rmdir):\n        # pylint: disable=protected-access\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        mock_rmdir.side_effect = OSError\n        self.storage._load_for_server_path = mock.MagicMock(\n            side_effect=errors.AccountStorageError)\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        assert [] == self.storage.find_all()\n\n    def test_upgrade_load(self):\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        prev_account = self.storage.load(self.acc.id)\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        account = self.storage.load(self.acc.id)\n        assert prev_account == account\n\n    def test_upgrade_load_single_account(self):\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        prev_account = self.storage.load(self.acc.id)\n        self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')\n        account = self.storage.load(self.acc.id)\n        assert prev_account == account\n\n    def test_load_ioerror(self):\n        self.storage.save(self.acc, self.mock_client)\n        mock_open = mock.mock_open()\n        mock_open.side_effect = IOError\n        with mock.patch(\"builtins.open\", mock_open):\n            with pytest.raises(errors.AccountStorageError):\n                self.storage.load(self.acc.id)\n\n    def test_save_ioerrors(self):\n        mock_open = mock.mock_open()\n        mock_open.side_effect = IOError  # TODO: [None, None, IOError]\n        with mock.patch(\"builtins.open\", mock_open):\n            with pytest.raises(errors.AccountStorageError):\n                self.storage.save(self.acc, self.mock_client)\n\n    def test_delete(self):\n        self.storage.save(self.acc, self.mock_client)\n        self.storage.delete(self.acc.id)\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.load(self.acc.id)\n\n    def test_delete_no_account(self):\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.delete(self.acc.id)\n\n    def _assert_symlinked_account_removed(self):\n        # create v1 account\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        # ensure v2 isn't already linked to it\n        with mock.patch('certbot._internal.constants.LE_REUSE_SERVERS', {}):\n            self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n            with pytest.raises(errors.AccountNotFound):\n                self.storage.load(self.acc.id)\n\n    def _test_delete_folders(self, server_url):\n        # create symlinked servers\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        self.storage.save(self.acc, self.mock_client)\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        self.storage.load(self.acc.id)\n\n        # delete starting at given server_url\n        self._set_server(server_url)\n        self.storage.delete(self.acc.id)\n\n        # make sure we're gone from both urls\n        self._set_server('https://acme-staging.api.letsencrypt.org/directory')\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.load(self.acc.id)\n        self._set_server('https://acme-staging-v02.api.letsencrypt.org/directory')\n        with pytest.raises(errors.AccountNotFound):\n            self.storage.load(self.acc.id)\n\n    def test_delete_folders_up(self):\n        self._test_delete_folders('https://acme-staging.api.letsencrypt.org/directory')\n        self._assert_symlinked_account_removed()\n\n    def test_delete_folders_down(self):\n        self._test_delete_folders('https://acme-staging-v02.api.letsencrypt.org/directory')\n        self._assert_symlinked_account_removed()\n\n    def _set_server_and_stop_symlink(self, server_path):\n        self._set_server(server_path)\n        with open(os.path.join(self.config.accounts_dir, 'foo'), 'w') as f:\n            f.write('bar')\n\n    def test_delete_shared_account_up(self):\n        self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')\n        self._test_delete_folders('https://acme-staging.api.letsencrypt.org/directory')\n\n    def test_delete_shared_account_down(self):\n        self._set_server_and_stop_symlink('https://acme-staging-v02.api.letsencrypt.org/directory')\n        self._test_delete_folders('https://acme-staging-v02.api.letsencrypt.org/directory')\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/auth_handler_test.py",
    "content": "\"\"\"Tests for certbot._internal.auth_handler.\"\"\"\nimport datetime\nimport logging\nimport sys\nimport unittest\nfrom unittest import mock\n\nfrom josepy import b64encode\nimport pytest\n\nfrom acme import challenges\nfrom acme import client as acme_client\nfrom acme import errors as acme_errors\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.plugins import common as plugin_common\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\n\nclass ChallengeFactoryTest(unittest.TestCase):\n    # pylint: disable=protected-access\n\n    def setUp(self):\n        from certbot._internal.auth_handler import AuthHandler\n\n        # Account is mocked...\n        self.handler = AuthHandler(None, None, mock.Mock(key=\"mock_key\"), [])\n\n        self.authzr = acme_util.gen_authzr(\n            messages.STATUS_PENDING, \"test\", acme_util.CHALLENGES,\n            [messages.STATUS_PENDING] * 6)\n\n    def test_all(self):\n        achalls = self.handler._challenge_factory(\n            self.authzr, range(0, len(acme_util.CHALLENGES)))\n\n        assert [achall.chall for achall in achalls] == acme_util.CHALLENGES\n\n    def test_one_http(self):\n        achalls = self.handler._challenge_factory(self.authzr, [0])\n\n        assert [achall.chall for achall in achalls] == [acme_util.HTTP01]\n\n    def test_unrecognized(self):\n        authzr = acme_util.gen_authzr(\n            messages.STATUS_PENDING, \"test\",\n            [mock.Mock(chall=\"chall\", typ=\"unrecognized\")],\n            [messages.STATUS_PENDING])\n\n        achalls = self.handler._challenge_factory(authzr, [0])\n        assert type(achalls[0]) is achallenges.Other\n\n\nclass HandleAuthorizationsTest(unittest.TestCase):\n    \"\"\"handle_authorizations test.\n\n    This tests everything except for all functions under _poll_challenges.\n\n    \"\"\"\n\n    def setUp(self):\n        from certbot._internal.auth_handler import AuthHandler\n\n        self.mock_display = mock.Mock()\n        self.mock_config = mock.Mock(debug_challenges=False)\n        display_obj.set_display(self.mock_display)\n\n        self.mock_auth = mock.MagicMock(name=\"Authenticator\")\n\n        self.mock_auth.get_chall_pref.return_value = [challenges.HTTP01]\n\n        self.mock_auth.perform.side_effect = gen_auth_resp\n\n        self.mock_account = mock.MagicMock()\n        self.mock_net = mock.MagicMock(spec=acme_client.ClientV2)\n        self.mock_net.retry_after.side_effect = acme_client.ClientV2.retry_after\n\n        self.handler = AuthHandler(\n            self.mock_auth, self.mock_net, self.mock_account, [])\n\n        logging.disable(logging.CRITICAL)\n\n    def tearDown(self):\n        logging.disable(logging.NOTSET)\n\n    def _test_name1_http_01_1_common(self):\n        authzr = gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)\n        mock_order = mock.MagicMock(authorizations=[authzr])\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=1, wait_value=30)\n        with mock.patch('certbot._internal.auth_handler.time') as mock_time:\n            authzr = self.handler.handle_authorizations(mock_order, self.mock_config)\n\n            assert self.mock_net.answer_challenge.call_count == 1\n\n            assert self.mock_net.poll.call_count == 2  # Because there is one retry\n            assert mock_time.sleep.call_count == 2\n            # Retry-After header is 30 seconds, but at the time sleep is invoked, several\n            # instructions are executed, and next pool is in less than 30 seconds.\n            assert mock_time.sleep.call_args_list[1][0][0] <= 30\n            # However, assert that we did not took the default value of 3 seconds.\n            assert mock_time.sleep.call_args_list[1][0][0] > 3\n\n            assert self.mock_auth.cleanup.call_count == 1\n            # Test if list first element is http-01, use typ because it is an achall\n            assert self.mock_auth.cleanup.call_args[0][0][0].typ == \"http-01\"\n\n            assert len(authzr) == 1\n\n    def test_name1_http_01_1_acme_2(self):\n        self._test_name1_http_01_1_common()\n\n    def test_name1_http_01_1_dns_1_acme_2(self):\n        self.mock_net.poll.side_effect = _gen_mock_on_poll()\n        self.mock_auth.get_chall_pref.return_value.append(challenges.DNS01)\n\n        authzr = gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)\n        mock_order = mock.MagicMock(authorizations=[authzr])\n        authzr = self.handler.handle_authorizations(mock_order, self.mock_config)\n\n        assert self.mock_net.answer_challenge.call_count == 1\n\n        assert self.mock_net.poll.call_count == 1\n\n        assert self.mock_auth.cleanup.call_count == 1\n        cleaned_up_achalls = self.mock_auth.cleanup.call_args[0][0]\n        assert len(cleaned_up_achalls) == 1\n        assert cleaned_up_achalls[0].typ == \"http-01\"\n\n        # Length of authorizations list\n        assert len(authzr) == 1\n\n    def test_name3_http_01_3_common_acme_2(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES),\n                   gen_dom_authzr(domain=\"1\", challs=acme_util.CHALLENGES),\n                   gen_dom_authzr(domain=\"2\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll()\n        authzr = self.handler.handle_authorizations(mock_order, self.mock_config)\n\n        assert self.mock_net.answer_challenge.call_count == 3\n\n        # Check poll call\n        assert self.mock_net.poll.call_count == 3\n\n        assert self.mock_auth.cleanup.call_count == 1\n\n        assert len(authzr) == 3\n\n    def test_debug_challenges(self):\n        config = mock.Mock(debug_challenges=True, verbose_count=0)\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        account_key_thumbprint = b\"foobarbaz\"\n        self.mock_account.key.thumbprint.return_value = account_key_thumbprint\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll()\n\n        self.handler.handle_authorizations(mock_order, config)\n\n        assert self.mock_net.answer_challenge.call_count == 1\n        assert self.mock_display.notification.call_count == 1\n        assert 'Pass \"-v\" for more info' in \\\n                      self.mock_display.notification.call_args[0][0]\n        assert f\"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/\" + \\\n                         b64encode(authzrs[0].body.challenges[0].chall.token).decode() not in \\\n                         self.mock_display.notification.call_args[0][0]\n        assert b64encode(account_key_thumbprint).decode() not in \\\n                      self.mock_display.notification.call_args[0][0]\n\n    def test_debug_challenges_verbose(self):\n        config = mock.Mock(debug_challenges=True, verbose_count=1)\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=[acme_util.HTTP01]),\n                   gen_dom_authzr(domain=\"1\", challs=[acme_util.DNS01])]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        account_key_thumbprint = b\"foobarbaz\"\n        self.mock_account.key.thumbprint.return_value = account_key_thumbprint\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll()\n\n        self.mock_auth.get_chall_pref.return_value = [challenges.HTTP01,\n                                                      challenges.DNS01]\n\n        self.handler.handle_authorizations(mock_order, config)\n\n        assert self.mock_net.answer_challenge.call_count == 2\n        assert self.mock_display.notification.call_count == 1\n        assert 'Pass \"-v\" for more info' not in \\\n                         self.mock_display.notification.call_args[0][0]\n        assert f\"http://{authzrs[0].body.identifier.value}/.well-known/acme-challenge/\" + \\\n                      b64encode(authzrs[0].body.challenges[0].chall.token).decode() in \\\n                      self.mock_display.notification.call_args[0][0]\n        assert b64encode(account_key_thumbprint).decode() in \\\n                      self.mock_display.notification.call_args[0][0]\n        assert f\"_acme-challenge.{authzrs[1].body.identifier.value}\" in \\\n                      self.mock_display.notification.call_args[0][0]\n        assert authzrs[1].body.challenges[0].validation(self.mock_account.key) in \\\n                      self.mock_display.notification.call_args[0][0]\n\n    def test_perform_failure(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        self.mock_auth.perform.side_effect = errors.AuthorizationError\n\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n    def test_max_retries_exceeded(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        # We will return STATUS_PENDING twice before returning STATUS_VALID.\n        self.mock_net.poll.side_effect = _gen_mock_on_poll(retry=2)\n\n        with pytest.raises(errors.AuthorizationError,\n                           match='All authorizations were not finalized by the CA.'):\n            # We retry only once, so retries will be exhausted before STATUS_VALID is returned.\n            self.handler.handle_authorizations(mock_order, self.mock_config, False, 1)\n\n    @mock.patch('certbot._internal.auth_handler.time.sleep')\n    def test_deadline_exceeded(self, mock_sleep):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        orig_now = datetime.datetime.now\n        state = {'time_slept': 0}\n\n        def mock_sleep_effect(secs):\n            state['time_slept'] += secs\n        mock_sleep.side_effect = mock_sleep_effect\n\n        def mock_now_effect():\n            return orig_now() + datetime.timedelta(seconds=state[\"time_slept\"])\n\n        # We will return STATUS_PENDING and ask Certbot to sleep for 20 minutes at a time.\n        interval = datetime.timedelta(minutes=20).seconds\n        self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_PENDING,\n                                                           wait_value=interval)\n\n        with pytest.raises(errors.AuthorizationError,\n                           match='All authorizations were not finalized by the CA.'):\n            with mock.patch('certbot._internal.auth_handler.datetime.datetime') as mock_dt:\n                mock_dt.now.side_effect = mock_now_effect\n                # Polling will only proceed for 30 minutes at most, so the second 20 minute sleep\n                # should be truncated and the polling should be aborted.\n                self.handler.handle_authorizations(mock_order, self.mock_config, False)\n\n        assert mock_sleep.call_count == 3 # 1s, 20m and 10m sleep\n        assert mock_sleep.call_args_list[0][0][0] == 1\n        assert abs(mock_sleep.call_args_list[1][0][0] - (interval - 1)) <= 1\n        assert abs(mock_sleep.call_args_list[2][0][0] - (interval/2 - 1)) <= 1\n\n    def test_no_domains(self):\n        mock_order = mock.MagicMock(authorizations=[])\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n    def test_preferred_challenge_choice_common_acme_2(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        self.mock_auth.get_chall_pref.return_value.append(challenges.HTTP01)\n\n        self.handler.pref_challs.extend((challenges.HTTP01.typ,\n                                         challenges.DNS01.typ,))\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll()\n        self.handler.handle_authorizations(mock_order, self.mock_config)\n\n        assert self.mock_auth.cleanup.call_count == 1\n        assert self.mock_auth.cleanup.call_args[0][0][0].typ == \"http-01\"\n\n    def test_preferred_challenges_not_supported_acme_2(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n        self.handler.pref_challs.append(challenges.DNS01.typ)\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n    def test_dns_only_challenge_not_supported(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=[acme_util.DNS01])]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n    def test_perform_error(self):\n        self.mock_auth.perform.side_effect = errors.AuthorizationError\n\n        authzr = gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)\n        mock_order = mock.MagicMock(authorizations=[authzr])\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n        assert self.mock_auth.cleanup.call_count == 1\n        assert self.mock_auth.cleanup.call_args[0][0][0].typ == \"http-01\"\n\n    def test_answer_error(self):\n        self.mock_net.answer_challenge.side_effect = errors.AuthorizationError\n\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n        assert self.mock_auth.cleanup.call_count == 1\n        assert self.mock_auth.cleanup.call_args[0][0][0].typ == \"http-01\"\n\n    def test_incomplete_authzr_error(self):\n        authzrs = [gen_dom_authzr(domain=\"0\", challs=acme_util.CHALLENGES)]\n        mock_order = mock.MagicMock(authorizations=authzrs)\n        self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID)\n\n        with test_util.patch_display_util():\n            with pytest.raises(errors.AuthorizationError, match='Some challenges have failed.'):\n                self.handler.handle_authorizations(mock_order, self.mock_config, False)\n        assert self.mock_auth.cleanup.call_count == 1\n        assert self.mock_auth.cleanup.call_args[0][0][0].typ == \"http-01\"\n\n    def test_best_effort(self):\n        def _conditional_mock_on_poll(authzr):\n            \"\"\"This mock will invalidate one authzr, and invalidate the other one\"\"\"\n            valid_mock = _gen_mock_on_poll(messages.STATUS_VALID)\n            invalid_mock = _gen_mock_on_poll(messages.STATUS_INVALID)\n\n            if authzr.body.identifier.value == 'will-be-invalid':\n                return invalid_mock(authzr)\n            return valid_mock(authzr)\n\n        # Two authzrs. Only one will be valid.\n        authzrs = [gen_dom_authzr(domain=\"will-be-valid\", challs=acme_util.CHALLENGES),\n                   gen_dom_authzr(domain=\"will-be-invalid\", challs=acme_util.CHALLENGES)]\n        self.mock_net.poll.side_effect = _conditional_mock_on_poll\n\n        mock_order = mock.MagicMock(authorizations=authzrs)\n\n        with mock.patch('certbot._internal.auth_handler.AuthHandler._report_failed_authzrs') \\\n            as mock_report:\n            valid_authzr = self.handler.handle_authorizations(mock_order, self.mock_config, True)\n\n        # Because best_effort=True, we did not blow up. Instead ...\n        assert len(valid_authzr) == 1  # ... the valid authzr has been processed\n        assert mock_report.call_count == 1  # ... the invalid authzr has been reported\n\n        self.mock_net.poll.side_effect = _gen_mock_on_poll(status=messages.STATUS_INVALID)\n\n        with test_util.patch_display_util():\n            with pytest.raises(errors.AuthorizationError, match='All challenges have failed.'):\n                # Despite best_effort=True, process will fail because no authzr is valid.\n                self.handler.handle_authorizations(mock_order, self.mock_config, True)\n\n    def test_validated_challenge_not_rerun(self):\n        # With a pending challenge that is not supported by the plugin, we\n        # expect an exception to be raised.\n        authzr = acme_util.gen_authzr(\n                messages.STATUS_PENDING, \"0\",\n                [acme_util.DNS01],\n                [messages.STATUS_PENDING])\n        mock_order = mock.MagicMock(authorizations=[authzr])\n        with pytest.raises(errors.AuthorizationError):\n            self.handler.handle_authorizations(mock_order, self.mock_config)\n\n        # With a validated challenge that is not supported by the plugin, we\n        # expect the challenge to not be solved again and\n        # handle_authorizations() to succeed.\n        authzr = acme_util.gen_authzr(\n                messages.STATUS_VALID, \"0\",\n                [acme_util.DNS01],\n                [messages.STATUS_VALID])\n        mock_order = mock.MagicMock(authorizations=[authzr])\n        self.handler.handle_authorizations(mock_order, self.mock_config)\n\n    def test_valid_authzrs_deactivated(self):\n        \"\"\"When we deactivate valid authzrs in an orderr, we expect them to become deactivated\n        and to receive a list of deactivated authzrs in return.\"\"\"\n        def _mock_deactivate(authzr):\n            if authzr.body.status == messages.STATUS_VALID:\n                if authzr.body.identifier.value == \"is_valid_but_will_fail\":\n                    raise acme_errors.Error(\"Mock deactivation ACME error\")\n                authzb = authzr.body.update(status=messages.STATUS_DEACTIVATED)\n                authzr = messages.AuthorizationResource(body=authzb)\n            else: # pragma: no cover\n                raise errors.Error(\"Can't deactivate non-valid authz\")\n            return authzr\n\n        to_deactivate = [(\"is_valid\", messages.STATUS_VALID),\n                         (\"is_pending\", messages.STATUS_PENDING),\n                         (\"is_valid_but_will_fail\", messages.STATUS_VALID)]\n\n        to_deactivate = [acme_util.gen_authzr(a[1], a[0], [acme_util.HTTP01],\n                         [a[1]]) for a in to_deactivate]\n        orderr = mock.MagicMock(authorizations=to_deactivate)\n\n        self.mock_net.deactivate_authorization.side_effect = _mock_deactivate\n\n        authzrs, failed = self.handler.deactivate_valid_authorizations(orderr)\n\n        assert self.mock_net.deactivate_authorization.call_count == 2\n        assert len(authzrs) == 1\n        assert len(failed) == 1\n        assert authzrs[0].body.identifier.value == \"is_valid\"\n        assert authzrs[0].body.status == messages.STATUS_DEACTIVATED\n        assert failed[0].body.identifier.value == \"is_valid_but_will_fail\"\n        assert failed[0].body.status == messages.STATUS_VALID\n\n\ndef _gen_mock_on_poll(status=messages.STATUS_VALID, retry=0, wait_value=1):\n    state = {'count': retry}\n\n    def _mock(authzr):\n        state['count'] = state['count'] - 1\n        effective_status = status if state['count'] < 0 else messages.STATUS_PENDING\n        updated_azr = acme_util.gen_authzr(\n            effective_status,\n            authzr.body.identifier.value,\n            [challb.chall for challb in authzr.body.challenges],\n            [effective_status] * len(authzr.body.challenges))\n        return updated_azr, mock.MagicMock(headers={'Retry-After': str(wait_value)})\n    return _mock\n\n\nclass ChallbToAchallTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.auth_handler.challb_to_achall.\"\"\"\n\n    def _call(self, challb):\n        from certbot._internal.auth_handler import challb_to_achall\n        ident = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"domain\")\n        return challb_to_achall(challb, \"account_key\", ident)\n\n    def test_it(self):\n        assert self._call(acme_util.HTTP01_P) == \\\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.HTTP01_P, account_key=\"account_key\",\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"domain\"))\n\n\nclass GenChallengePathTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.auth_handler.gen_challenge_path.\n\n    \"\"\"\n    def setUp(self):\n        logging.disable(logging.FATAL)\n\n    def tearDown(self):\n        logging.disable(logging.NOTSET)\n\n    @classmethod\n    def _call(cls, challbs, preferences):\n        from certbot._internal.auth_handler import gen_challenge_path\n        return gen_challenge_path(challbs, preferences)\n\n    def test_common_case(self):\n        \"\"\"Given DNS01 and HTTP01 with appropriate combos.\"\"\"\n        challbs = (acme_util.DNS01_P, acme_util.HTTP01_P)\n        prefs = [challenges.DNS01, challenges.HTTP01]\n\n        assert self._call(challbs, prefs) == (0,)\n        assert self._call(challbs[::-1], prefs) == (1,)\n\n    def test_not_supported(self):\n        challbs = (acme_util.DNS01_P,)\n        prefs = [challenges.HTTP01]\n\n        # smart path fails because no challs in prefs satisfies combos\n        with pytest.raises(errors.AuthorizationError):\n            self._call(challbs, prefs)\n\n\nclass ReportFailedAuthzrsTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.auth_handler.AuthHandler._report_failed_authzrs.\"\"\"\n    # pylint: disable=protected-access\n\n\n    def setUp(self):\n        from certbot._internal.auth_handler import AuthHandler\n\n        self.mock_auth = mock.MagicMock(spec=plugin_common.Plugin, name=\"buzz\")\n        self.mock_auth.name = \"buzz\"\n        self.mock_auth.auth_hint.return_value = \"the buzz hint\"\n        self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), [])\n\n        kwargs = {\n            \"chall\": acme_util.HTTP01,\n            \"uri\": \"uri\",\n            \"status\": messages.STATUS_INVALID,\n            \"error\": messages.Error.with_code(\"tls\", detail=\"detail\"),\n        }\n\n        # Prevent future regressions if the error type changes\n        assert kwargs[\"error\"].description is not None\n\n        http_01 = messages.ChallengeBody(**kwargs)\n\n        kwargs[\"chall\"] = acme_util.HTTP01\n        http_01 = messages.ChallengeBody(**kwargs)\n\n        self.authzr1 = mock.MagicMock()\n        self.authzr1.body.identifier.value = 'example.com'\n        self.authzr1.body.challenges = [http_01, http_01]\n\n        kwargs[\"error\"] = messages.Error.with_code(\"dnssec\", detail=\"detail\")\n        http_01_diff = messages.ChallengeBody(**kwargs)\n\n        self.authzr2 = mock.MagicMock()\n        self.authzr2.body.identifier.value = 'foo.bar'\n        self.authzr2.body.challenges = [http_01_diff]\n\n    @mock.patch('certbot._internal.auth_handler.display_util.notify')\n    def test_same_error_and_domain(self, mock_notify):\n        self.handler._report_failed_authzrs([self.authzr1])\n        mock_notify.assert_called_with(\n            '\\n'\n            'Certbot failed to authenticate some domains (authenticator: buzz). '\n            'The Certificate Authority reported these problems:\\n'\n            '  Identifier: example.com\\n'\n            '  Type:   tls\\n'\n            '  Detail: detail\\n'\n            '\\n'\n            '  Identifier: example.com\\n'\n            '  Type:   tls\\n'\n            '  Detail: detail\\n'\n            '\\nHint: the buzz hint\\n'\n        )\n\n    @mock.patch('certbot._internal.auth_handler.display_util.notify')\n    def test_different_errors_and_domains(self, mock_notify):\n        self.mock_auth.name = \"quux\"\n        self.mock_auth.auth_hint.return_value = \"quuuuuux\"\n        self.handler._report_failed_authzrs([self.authzr1, self.authzr2])\n        mock_notify.assert_called_with(\n            '\\n'\n            'Certbot failed to authenticate some domains (authenticator: quux). '\n            'The Certificate Authority reported these problems:\\n'\n            '  Identifier: foo.bar\\n'\n            '  Type:   dnssec\\n'\n            '  Detail: detail\\n'\n            '\\n'\n            '  Identifier: example.com\\n'\n            '  Type:   tls\\n'\n            '  Detail: detail\\n'\n            '\\n'\n            '  Identifier: example.com\\n'\n            '  Type:   tls\\n'\n            '  Detail: detail\\n'\n            '\\nHint: quuuuuux\\n'\n        )\n\n    @mock.patch('certbot._internal.auth_handler.display_util.notify')\n    def test_non_subclassed_authenticator(self, mock_notify):\n        \"\"\"If authenticator not derived from common.Plugin, we shouldn't call .auth_hint\"\"\"\n        from certbot._internal.auth_handler import AuthHandler\n\n        self.mock_auth = mock.MagicMock(name=\"quuz\")\n        self.mock_auth.name = \"quuz\"\n        self.mock_auth.auth_hint.side_effect = Exception\n        self.handler = AuthHandler(self.mock_auth, mock.MagicMock(), mock.MagicMock(), [])\n        self.handler._report_failed_authzrs([self.authzr1])\n        assert mock_notify.call_count == 1\n\n\ndef gen_auth_resp(chall_list):\n    \"\"\"Generate a dummy authorization response.\"\"\"\n    return [\"%s%s\" % (chall.__class__.__name__, chall.identifier.value)\n            for chall in chall_list]\n\n\ndef gen_dom_authzr(domain, challs):\n    \"\"\"Generates new authzr for domains.\"\"\"\n    return acme_util.gen_authzr(\n        messages.STATUS_PENDING, domain, challs,\n        [messages.STATUS_PENDING] * len(challs))\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/cert_manager_test.py",
    "content": "\n\"\"\"Tests for certbot._internal.cert_manager.\"\"\"\n# pylint: disable=protected-access\nimport re\nimport shutil\nimport sys\nimport tempfile\nimport unittest\nfrom unittest import mock\n\nimport configobj\nimport pytest\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot._internal import san\nfrom certbot._internal.storage import ALL_FOUR\nfrom certbot._internal.tests import storage_test\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot.tests import util as test_util\n\n\nclass BaseCertManagerTest(test_util.ConfigTestCase):\n    \"\"\"Base class for setting up Cert Manager tests.\n    \"\"\"\n    def setUp(self):\n        super().setUp()\n\n        self.config.quiet = False\n        filesystem.makedirs(self.config.renewal_configs_dir)\n\n        self.domains = {\n            \"example.org\": None,\n            \"other.com\": os.path.join(self.config.config_dir, \"specialarchive\")\n        }\n        self.config_files = {domain: self._set_up_config(domain, self.domains[domain])\n            for domain in self.domains}\n\n        # We also create a file that isn't a renewal config in the same\n        # location to test that logic that reads in all-and-only renewal\n        # configs will ignore it and NOT attempt to parse it.\n        with open(os.path.join(self.config.renewal_configs_dir, \"IGNORE.THIS\"), \"w\") as junk:\n            junk.write(\"This file should be ignored!\")\n\n    def _set_up_config(self, domain, custom_archive):\n        # TODO: maybe provide NamespaceConfig.make_dirs?\n        # TODO: main() should create those dirs, c.f. #902\n        filesystem.makedirs(os.path.join(self.config.live_dir, domain))\n        config_file = configobj.ConfigObj()\n\n        if custom_archive is not None:\n            filesystem.makedirs(custom_archive)\n            config_file[\"archive_dir\"] = custom_archive\n        else:\n            filesystem.makedirs(os.path.join(self.config.default_archive_dir, domain))\n\n        for kind in ALL_FOUR:\n            config_file[kind] = os.path.join(self.config.live_dir, domain,\n                                        kind + \".pem\")\n\n        config_file.filename = os.path.join(self.config.renewal_configs_dir,\n                                       domain + \".conf\")\n        config_file.write()\n        return config_file\n\n\nclass DeleteTest(storage_test.BaseRenewableCertTest):\n    \"\"\"Tests for certbot._internal.cert_manager.delete\n    \"\"\"\n\n    def _call(self):\n        from certbot._internal import cert_manager\n        cert_manager.delete(self.config)\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot.display.util.notify')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_from_config_yes(self, mock_delete_files, mock_lineage_for_certname,\n        mock_notify, mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().yesno.return_value = True\n        self.config.certname = \"example.org\"\n        self._call()\n        mock_delete_files.assert_called_once_with(self.config, \"example.org\")\n        mock_notify.assert_called_once_with(\n            \"Deleted all files relating to certificate example.org.\"\n        )\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_from_config_no(self, mock_delete_files, mock_lineage_for_certname,\n        mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().yesno.return_value = False\n        self.config.certname = \"example.org\"\n        self._call()\n        assert mock_delete_files.call_count == 0\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_interactive_single_yes(self, mock_delete_files, mock_lineage_for_certname,\n        mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().checklist.return_value = (display_util.OK, [\"example.org\"])\n        mock_util().yesno.return_value = True\n        self._call()\n        mock_delete_files.assert_called_once_with(self.config, \"example.org\")\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_interactive_single_no(self, mock_delete_files, mock_lineage_for_certname,\n        mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().checklist.return_value = (display_util.OK, [\"example.org\"])\n        mock_util().yesno.return_value = False\n        self._call()\n        assert mock_delete_files.call_count == 0\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_interactive_multiple_yes(self, mock_delete_files, mock_lineage_for_certname,\n        mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().checklist.return_value = (display_util.OK, [\"example.org\", \"other.org\"])\n        mock_util().yesno.return_value = True\n        self._call()\n        mock_delete_files.assert_any_call(self.config, \"example.org\")\n        mock_delete_files.assert_any_call(self.config, \"other.org\")\n        assert mock_delete_files.call_count == 2\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.storage.delete_files')\n    def test_delete_interactive_multiple_no(self, mock_delete_files, mock_lineage_for_certname,\n        mock_util):\n        \"\"\"Test delete\"\"\"\n        mock_lineage_for_certname.return_value = self.test_rc\n        mock_util().checklist.return_value = (display_util.OK, [\"example.org\", \"other.org\"])\n        mock_util().yesno.return_value = False\n        self._call()\n        assert mock_delete_files.call_count == 0\n\n\nclass CertificatesTest(BaseCertManagerTest):\n    \"\"\"Tests for certbot._internal.cert_manager.certificates\n    \"\"\"\n    def _certificates(self, *args, **kwargs):\n        from certbot._internal.cert_manager import certificates\n        return certificates(*args, **kwargs)\n\n    @mock.patch('certbot._internal.cert_manager.logger')\n    @test_util.patch_display_util()\n    def test_certificates_parse_fail(self, mock_utility, mock_logger):\n        self._certificates(self.config)\n        assert mock_logger.warning.called #pylint: disable=no-member\n        assert mock_utility.called\n\n    @mock.patch('certbot._internal.cert_manager.logger')\n    @test_util.patch_display_util()\n    def test_certificates_quiet(self, mock_utility, mock_logger):\n        self.config.quiet = True\n        self._certificates(self.config)\n        assert mock_utility.notification.called is False\n        assert mock_logger.warning.called #pylint: disable=no-member\n\n    @mock.patch('certbot.crypto_util.verify_renewable_cert')\n    @mock.patch('certbot._internal.cert_manager.logger')\n    @test_util.patch_display_util()\n    @mock.patch(\"certbot._internal.storage.RenewableCert\")\n    @mock.patch('certbot._internal.cert_manager._report_human_readable')\n    def test_certificates_parse_success(self, mock_report, mock_renewable_cert,\n        mock_utility, mock_logger, mock_verifier):\n        mock_verifier.return_value = None\n        mock_report.return_value = \"\"\n        self._certificates(self.config)\n        assert mock_logger.warning.called is False\n        assert mock_report.called\n        assert mock_utility.called\n        assert mock_renewable_cert.called\n\n    @mock.patch('certbot._internal.cert_manager.logger')\n    @test_util.patch_display_util()\n    def test_certificates_no_files(self, mock_utility, mock_logger):\n        empty_tempdir = tempfile.mkdtemp()\n        empty_config = configuration.NamespaceConfig(mock.MagicMock(\n            config_dir=os.path.join(empty_tempdir, \"config\"),\n            work_dir=os.path.join(empty_tempdir, \"work\"),\n            logs_dir=os.path.join(empty_tempdir, \"logs\"),\n            quiet=False\n        ))\n\n        filesystem.makedirs(empty_config.renewal_configs_dir)\n        self._certificates(empty_config)\n        assert mock_logger.warning.called is False\n        assert mock_utility.called\n        shutil.rmtree(empty_tempdir)\n\n    @mock.patch('certbot.crypto_util.get_serial_from_cert')\n    @mock.patch('certbot._internal.cert_manager.ocsp.RevocationChecker.ocsp_revoked')\n    def test_report_human_readable(self, mock_revoked, mock_serial):\n        mock_revoked.return_value = None\n        mock_serial.return_value = 1234567890\n        import datetime\n\n        from certbot._internal import cert_manager\n        expiry = datetime.datetime.now(datetime.timezone.utc)\n\n        cert = mock.MagicMock(lineagename=\"nameone\")\n        cert.target_expiry = expiry\n        cert.sans.return_value = [san.DNSName(\"nameone\"), san.DNSName(\"nametwo\")]\n        cert.is_test_cert = False\n        parsed_certs = [cert]\n\n        mock_config = mock.MagicMock(certname=None, lineagename=None)\n        mock_config.domains = []\n        mock_config.ip_addresses = []\n        # pylint: disable=protected-access\n\n        # pylint: disable=protected-access\n        def get_report():\n            return cert_manager._report_human_readable(mock_config, parsed_certs)\n\n        out = get_report()\n        assert \"INVALID: EXPIRED\" in out\n\n        cert.target_expiry += datetime.timedelta(hours=2)\n        # pylint: disable=protected-access\n        out = get_report()\n        assert ('1 hour' in out or '2 hour(s)' in out) is True\n        assert 'VALID' in out\n        assert 'INVALID' not in out\n\n        cert.target_expiry += datetime.timedelta(days=1)\n        # pylint: disable=protected-access\n        out = get_report()\n        assert '1 day' in out\n        assert 'under' not in out\n        assert 'VALID' in out\n        assert 'INVALID' not in out\n\n        cert.target_expiry += datetime.timedelta(days=2)\n        # pylint: disable=protected-access\n        out = get_report()\n        assert '3 days' in out\n        assert 'VALID' in out\n        assert 'INVALID' not in out\n\n        cert.is_test_cert = True\n        mock_revoked.return_value = True\n        out = get_report()\n        assert 'INVALID: TEST_CERT, REVOKED' in out\n\n        cert = mock.MagicMock(lineagename=\"indescribable\")\n        cert.target_expiry = expiry\n        cert.sans.return_value = [san.DNSName(\"nameone\"), san.DNSName(\"thrice.named\")]\n        cert.is_test_cert = True\n        parsed_certs.append(cert)\n\n        out = get_report()\n        assert len(re.findall(\"INVALID:\", out)) == 2\n        mock_config.domains = [san.DNSName(\"thrice.named\")]\n        out = get_report()\n        assert len(re.findall(\"INVALID:\", out)) == 1\n        mock_config.domains = [san.DNSName(\"nameone\")]\n        out = get_report()\n        assert len(re.findall(\"INVALID:\", out)) == 2\n        mock_config.certname = \"indescribable\"\n        out = get_report()\n        assert len(re.findall(\"INVALID:\", out)) == 1\n        mock_config.certname = \"horror\"\n        out = get_report()\n        assert len(re.findall(\"INVALID:\", out)) == 0\n\n\nclass SearchLineagesTest(BaseCertManagerTest):\n    \"\"\"Tests for certbot._internal.cert_manager._search_lineages.\"\"\"\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    def test_cert_storage_error(self, mock_renewable_cert, mock_renewal_conf_files,\n                                mock_make_or_verify_dir):\n        mock_renewal_conf_files.return_value = [\"badfile\"]\n        mock_renewable_cert.side_effect = errors.CertStorageError\n        from certbot._internal import cert_manager\n\n        # pylint: disable=protected-access\n        assert cert_manager._search_lineages(self.config, lambda x: x, \"check\") == \"check\"\n        assert mock_make_or_verify_dir.called\n\n\nclass LineageForCertnameTest(BaseCertManagerTest):\n    \"\"\"Tests for certbot._internal.cert_manager.lineage_for_certname\"\"\"\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,\n                         mock_make_or_verify_dir):\n        mock_renewal_conf_file.return_value = \"somefile.conf\"\n        mock_match = mock.Mock(lineagename=\"example.com\")\n        mock_renewable_cert.return_value = mock_match\n        from certbot._internal import cert_manager\n        assert cert_manager.lineage_for_certname(self.config, \"example.com\") == mock_match\n        assert mock_make_or_verify_dir.called\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir):\n        mock_renewal_conf_file.return_value = \"other.com.conf\"\n        from certbot._internal import cert_manager\n        assert cert_manager.lineage_for_certname(self.config, \"example.com\") is None\n        assert mock_make_or_verify_dir.called\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    def test_no_renewal_file(self, mock_renewal_conf_file, mock_make_or_verify_dir):\n        mock_renewal_conf_file.side_effect = errors.CertStorageError()\n        from certbot._internal import cert_manager\n        assert cert_manager.lineage_for_certname(self.config, \"example.com\") is None\n        assert mock_make_or_verify_dir.called\n\n\nclass DomainsForCertnameTest(BaseCertManagerTest):\n    \"\"\"Tests for certbot._internal.cert_manager.sans_for_certname\"\"\"\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    def test_found_match(self, mock_renewable_cert, mock_renewal_conf_file,\n                         mock_make_or_verify_dir):\n        mock_renewal_conf_file.return_value = \"somefile.conf\"\n        mock_match = mock.Mock(lineagename=\"example.com\")\n        domains = [san.DNSName(\"example.com\"), san.DNSName(\"example.org\")]\n        mock_match.sans.return_value = domains\n        mock_renewable_cert.return_value = mock_match\n        from certbot._internal import cert_manager\n        assert cert_manager.sans_for_certname(self.config, \"example.com\") == \\\n               domains\n        assert mock_make_or_verify_dir.called\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    def test_no_match(self, mock_renewal_conf_file, mock_make_or_verify_dir):\n        mock_renewal_conf_file.return_value = \"somefile.conf\"\n        from certbot._internal import cert_manager\n        assert cert_manager.sans_for_certname(self.config, \"other.com\") is None\n        assert mock_make_or_verify_dir.called\n\n\nclass DuplicativeCertsTest(storage_test.BaseRenewableCertTest):\n    \"\"\"Test to avoid duplicate lineages.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config_file.write()\n        self._write_out_ex_kinds()\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    def test_find_duplicative_names(self, unused_makedir):\n        from certbot._internal.cert_manager import find_duplicative_certs\n        test_cert = test_util.load_vector('cert-san_512.pem')\n        with open(self.test_rc.cert, 'wb') as f:\n            f.write(test_cert)\n\n        # No overlap at all\n        result = find_duplicative_certs(\n            self.config, [san.DNSName('wow.net'), san.DNSName('hooray.org')])\n        assert result == (None, None)\n\n        # Totally identical\n        result = find_duplicative_certs(\n            self.config, [san.DNSName('example.com'), san.DNSName('www.example.com')])\n        assert result[0].configfile.filename.endswith('example.org.conf')\n        assert result[1] is None\n\n        # Superset\n        result = find_duplicative_certs(\n            self.config, [san.DNSName('example.com'), san.DNSName('www.example.com'),\n                          san.DNSName('something.new')])\n        assert result[0] is None\n        assert result[1].configfile.filename.endswith('example.org.conf')\n\n        # Partial overlap doesn't count\n        result = find_duplicative_certs(\n            self.config, [san.DNSName('example.com'), san.DNSName('something.new')])\n        assert result == (None, None)\n\n\nclass CertPathToLineageTest(storage_test.BaseRenewableCertTest):\n    \"\"\"Tests for certbot._internal.cert_manager.cert_path_to_lineage\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config_file.write()\n        self._write_out_ex_kinds()\n        self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org',\n                'fullchain.pem')\n        self.config.cert_path = self.fullchain\n\n    def _call(self, cli_config):\n        from certbot._internal.cert_manager import cert_path_to_lineage\n        return cert_path_to_lineage(cli_config)\n\n    def _archive_files(self, cli_config, filetype):\n        from certbot._internal.cert_manager import _archive_files\n        return _archive_files(cli_config, filetype)\n\n    def test_basic_match(self):\n        assert 'example.org' == self._call(self.config)\n\n    def test_no_match_exists(self):\n        bad_test_config = self.config\n        bad_test_config.cert_path = os.path.join(self.config.config_dir, 'live',\n                'SailorMoon', 'fullchain.pem')\n        with pytest.raises(errors.Error):\n            self._call(bad_test_config)\n\n    @mock.patch('certbot._internal.cert_manager._acceptable_matches')\n    def test_options_fullchain(self, mock_acceptable_matches):\n        mock_acceptable_matches.return_value = [lambda x: x.fullchain_path]\n        self.config.fullchain_path = self.fullchain\n        assert 'example.org' == self._call(self.config)\n\n    @mock.patch('certbot._internal.cert_manager._acceptable_matches')\n    def test_options_cert_path(self, mock_acceptable_matches):\n        mock_acceptable_matches.return_value = [lambda x: x.cert_path]\n        test_cert_path = os.path.join(self.config.config_dir, 'live', 'example.org',\n                'cert.pem')\n        self.config.cert_path = test_cert_path\n        assert 'example.org' == self._call(self.config)\n\n    @mock.patch('certbot._internal.cert_manager._acceptable_matches')\n    def test_options_archive_cert(self, mock_acceptable_matches):\n        # Also this and the next test check that the regex of _archive_files is working.\n        self.config.cert_path = os.path.join(self.config.config_dir, 'archive', 'example.org',\n            'cert11.pem')\n        mock_acceptable_matches.return_value = [lambda x: self._archive_files(x, 'cert')]\n        assert 'example.org' == self._call(self.config)\n\n    @mock.patch('certbot._internal.cert_manager._acceptable_matches')\n    def test_options_archive_fullchain(self, mock_acceptable_matches):\n        self.config.cert_path = os.path.join(self.config.config_dir, 'archive',\n            'example.org', 'fullchain11.pem')\n        mock_acceptable_matches.return_value = [lambda x:\n                self._archive_files(x, 'fullchain')]\n        assert 'example.org' == self._call(self.config)\n\n    def test_only_path(self):\n        self.config.cert_path = self.fullchain\n        assert 'example.org' == self._call(self.config)\n\n\nclass MatchAndCheckOverlaps(storage_test.BaseRenewableCertTest):\n    \"\"\"Tests for certbot._internal.cert_manager.match_and_check_overlaps w/o overlapping\n       archive dirs.\"\"\"\n    # A test with real overlapping archive dirs can be found in tests/boulder_integration.sh\n    def setUp(self):\n        super().setUp()\n        self.config_file.write()\n        self._write_out_ex_kinds()\n        self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org',\n                'fullchain.pem')\n        self.config.cert_path = self.fullchain\n\n    def _call(self, cli_config, acceptable_matches, match_func, rv_func):\n        from certbot._internal.cert_manager import match_and_check_overlaps\n        return match_and_check_overlaps(cli_config, acceptable_matches, match_func, rv_func)\n\n    def test_basic_match(self):\n        from certbot._internal.cert_manager import _acceptable_matches\n        assert ['example.org'] == self._call(self.config, _acceptable_matches(),\n            lambda x: self.config.cert_path, lambda x: x.lineagename)\n\n    @mock.patch('certbot._internal.cert_manager._search_lineages')\n    def test_no_matches(self, mock_search_lineages):\n        mock_search_lineages.return_value = []\n        with pytest.raises(errors.Error):\n            self._call(self.config, None, None, None)\n\n    @mock.patch('certbot._internal.cert_manager._search_lineages')\n    def test_too_many_matches(self, mock_search_lineages):\n        mock_search_lineages.return_value = ['spider', 'dance']\n        with pytest.raises(errors.OverlappingMatchFound):\n            self._call(self.config, None, None, None)\n\n\nclass GetCertnameTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.cert_manager.\"\"\"\n\n    def setUp(self):\n        get_utility_patch = test_util.patch_display_util()\n        self.mock_get_utility = get_utility_patch.start()\n        self.addCleanup(get_utility_patch.stop)\n        self.config = mock.MagicMock()\n        self.config.certname = None\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        prompt = \"Which certificate would you\"\n        self.mock_get_utility().menu.return_value = (display_util.OK, 0)\n        assert cert_manager.get_certnames(\n                self.config, \"verb\", allow_multiple=False) == ['example.com']\n        assert prompt in self.mock_get_utility().menu.call_args[0][0]\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames_custom_prompt(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        prompt = \"custom prompt\"\n        self.mock_get_utility().menu.return_value = (display_util.OK, 0)\n        assert cert_manager.get_certnames(\n                self.config, \"verb\", allow_multiple=False, custom_prompt=prompt) == \\\n            ['example.com']\n        assert self.mock_get_utility().menu.call_args[0][0] == \\\n                          prompt\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames_user_abort(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        self.mock_get_utility().menu.return_value = (display_util.CANCEL, 0)\n        with pytest.raises(errors.Error):\n            cert_manager.get_certnames(self.config, \"erroring_anyway\", allow_multiple=False)\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames_allow_multiple(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        prompt = \"Which certificate(s) would you\"\n        self.mock_get_utility().checklist.return_value = (display_util.OK,\n                                                          ['example.com'])\n        assert cert_manager.get_certnames(\n                self.config, \"verb\", allow_multiple=True) == ['example.com']\n        assert prompt in self.mock_get_utility().checklist.call_args[0][0]\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames_allow_multiple_custom_prompt(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        prompt = \"custom prompt\"\n        self.mock_get_utility().checklist.return_value = (display_util.OK,\n                                                          ['example.com'])\n        assert cert_manager.get_certnames(\n                self.config, \"verb\", allow_multiple=True, custom_prompt=prompt) == \\\n            ['example.com']\n        assert self.mock_get_utility().checklist.call_args[0][0] == \\\n            prompt\n\n    @mock.patch('certbot._internal.storage.renewal_conf_files')\n    @mock.patch('certbot._internal.storage.lineagename_for_filename')\n    def test_get_certnames_allow_multiple_user_abort(self, mock_name, mock_files):\n        mock_files.return_value = ['example.com.conf']\n        mock_name.return_value = 'example.com'\n        from certbot._internal import cert_manager\n        self.mock_get_utility().checklist.return_value = (display_util.CANCEL, [])\n        with pytest.raises(errors.Error):\n            cert_manager.get_certnames(self.config, \"erroring_anyway\", allow_multiple=True)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/cli_test.py",
    "content": "\"\"\"Tests for certbot._internal.cli.\"\"\"\nimport argparse\nimport copy\nimport io\nimport sys\nimport tempfile\nfrom typing import Any\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import challenges\nfrom certbot import errors\nfrom certbot.configuration import ArgumentSource, NamespaceConfig\nfrom certbot._internal import cli\nfrom certbot._internal import constants\nfrom certbot._internal import san\nfrom certbot._internal.cli.cli_utils import flag_default\nfrom certbot._internal.plugins import disco\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\nfrom certbot.tests.util import TempDirTestCase\n\nPLUGINS = disco.PluginsRegistry.find_all()\n\n\nclass TestReadFile(TempDirTestCase):\n    \"\"\"Test cli.read_file\"\"\"\n    def test_read_file(self):\n        curr_dir = os.getcwd()\n        try:\n            # On Windows current directory may be on a different drive than self.tempdir.\n            # However a relative path between two different drives is invalid. So we move to\n            # self.tempdir to ensure that we stay on the same drive.\n            os.chdir(self.tempdir)\n            # The read-only filesystem introduced with macOS Catalina can break\n            # code using relative paths below. See\n            # https://bugs.python.org/issue38295 for another example of this.\n            # Eliminating any possible symlinks in self.tempdir before passing\n            # it to os.path.relpath solves the problem. This is done by calling\n            # filesystem.realpath which removes any symlinks in the path on\n            # POSIX systems.\n            real_path = filesystem.realpath(os.path.join(self.tempdir, 'foo'))\n            relative_path = os.path.relpath(real_path)\n            with pytest.raises(argparse.ArgumentTypeError):\n                cli.read_file(relative_path)\n\n            test_contents = b'bar\\n'\n            with open(relative_path, 'wb') as f:\n                f.write(test_contents)\n\n            path, contents = cli.read_file(relative_path)\n            assert path == os.path.abspath(path)\n            assert contents == test_contents\n        finally:\n            os.chdir(curr_dir)\n\n\nclass FlagDefaultTest(unittest.TestCase):\n    \"\"\"Tests cli.flag_default\"\"\"\n\n    def test_default_directories(self):\n        if os.name != 'nt':\n            assert cli.flag_default('config_dir') == '/etc/letsencrypt'\n            assert cli.flag_default('work_dir') == '/var/lib/letsencrypt'\n            assert cli.flag_default('logs_dir') == '/var/log/letsencrypt'\n        else:\n            assert cli.flag_default('config_dir') == 'C:\\\\Certbot'\n            assert cli.flag_default('work_dir') == 'C:\\\\Certbot\\\\lib'\n            assert cli.flag_default('logs_dir') == 'C:\\\\Certbot\\\\log'\n\n\ndef assert_set_by_user_with_value(namespace, attr: str, value: Any):\n    assert getattr(namespace, attr) == value\n    assert namespace.set_by_user(attr)\n\n\ndef assert_value_and_source(namespace, attr: str, value: Any, source: ArgumentSource):\n    assert getattr(namespace, attr) == value\n    assert namespace.argument_sources[attr] == source\n\n\nclass ParseTest(unittest.TestCase):\n    '''Test the cli args entrypoint'''\n\n    @staticmethod\n    def _unmocked_parse(args: list[str]) -> NamespaceConfig:\n        \"\"\"Get result of cli.prepare_and_parse_args.\"\"\"\n        return cli.prepare_and_parse_args(PLUGINS, args)\n\n    @staticmethod\n    def parse(args: list[str]) -> NamespaceConfig:\n        \"\"\"Mocks certbot._internal.display.obj.get_display and calls _unmocked_parse.\"\"\"\n        with test_util.patch_display_util():\n            return ParseTest._unmocked_parse(args)\n\n    def _help_output(self, args: list[str]):\n        \"Run a command, and return the output string for scrutiny\"\n\n        output = io.StringIO()\n\n        def write_msg(message, *args, **kwargs): # pylint: disable=missing-docstring,unused-argument\n            output.write(message)\n\n        with mock.patch('certbot._internal.main.sys.stdout', new=output):\n            with test_util.patch_display_util() as mock_get_utility:\n                mock_get_utility().notification.side_effect = write_msg\n                with mock.patch('certbot._internal.main.sys.stderr'):\n                    with pytest.raises(SystemExit):\n                        self._unmocked_parse(args)\n\n        return output.getvalue()\n\n    @mock.patch(\"certbot._internal.cli.helpful.flag_default\")\n    def test_cli_ini_domains(self, mock_flag_default):\n        with tempfile.NamedTemporaryFile() as tmp_config:\n            tmp_config.close()  # close now because of compatibility issues on Windows\n            # use a shim to get ConfigArgParse to pick up tmp_config\n            def shim(v):\n                return (copy.deepcopy(constants.CLI_DEFAULTS[v])\n                                if v != \"config_files\"\n                                else [tmp_config.name])\n            mock_flag_default.side_effect = shim\n\n            namespace = self.parse([\"certonly\"])\n            assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT)\n            with open(tmp_config.name, 'w') as file_h:\n                file_h.write(\"domains = example.com\")\n            namespace = self.parse([\"certonly\"])\n            assert_value_and_source(namespace, 'domains', [san.DNSName(\"example.com\")], ArgumentSource.CONFIG_FILE)\n            namespace = self.parse([\"renew\"])\n            assert_value_and_source(namespace, 'domains', [], ArgumentSource.RUNTIME)\n\n    def test_no_args(self):\n        namespace = self.parse([])\n        for d in ('config_dir', 'logs_dir', 'work_dir'):\n            assert getattr(namespace, d) == cli.flag_default(d)\n            assert not namespace.set_by_user(d)\n\n    def test_install_abspath(self):\n        cert = 'cert'\n        key = 'key'\n        chain = 'chain'\n        fullchain = 'fullchain'\n\n        with mock.patch('certbot._internal.main.install'):\n            namespace = self.parse(['install', '--cert-path', cert,\n                                    '--key-path', 'key', '--chain-path',\n                                    'chain', '--fullchain-path', 'fullchain'])\n\n        assert namespace.cert_path == os.path.abspath(cert)\n        assert namespace.key_path == os.path.abspath(key)\n        assert namespace.chain_path == os.path.abspath(chain)\n        assert namespace.fullchain_path == os.path.abspath(fullchain)\n\n    def test_help(self):\n        self._help_output(['--help'])  # assert SystemExit is raised here\n        out = self._help_output(['--help', 'all'])\n        assert \"--configurator\" in out\n        assert \"how a certificate is deployed\" in out\n        assert \"--webroot-path\" in out\n        assert \"--text\" not in out\n        assert \"%s\" not in out\n        assert \"{0}\" not in out\n        assert \"--renew-hook\" not in out\n\n        out = self._help_output(['-h', 'nginx'])\n        if \"nginx\" in PLUGINS:\n            # may be false while building distributions without plugins\n            assert \"--nginx-ctl\" in out\n        assert \"--webroot-path\" not in out\n        assert \"--checkpoints\" not in out\n\n        out = self._help_output(['-h'])\n        if \"nginx\" in PLUGINS:\n            assert \"Use the Nginx plugin\" in out\n        else:\n            assert \"(the certbot nginx plugin is not\" in out\n\n        out = self._help_output(['--help', 'plugins'])\n        assert \"--webroot-path\" not in out\n        assert \"--prepare\" in out\n        assert '\"plugins\" subcommand' in out\n\n        # test multiple topics\n        out = self._help_output(['-h', 'renew'])\n        assert \"--keep\" in out\n        out = self._help_output(['-h', 'automation'])\n        assert \"--keep\" in out\n        out = self._help_output(['-h', 'revoke'])\n        assert \"--keep\" not in out\n\n        out = self._help_output(['--help', 'install'])\n        assert \"--cert-path\" in out\n        assert \"--key-path\" in out\n\n        out = self._help_output(['--help', 'revoke'])\n        assert \"--cert-path\" in out\n        assert \"--key-path\" in out\n        assert \"--reason\" in out\n        assert \"--delete-after-revoke\" in out\n        assert \"--no-delete-after-revoke\" in out\n\n        out = self._help_output(['-h', 'register'])\n        assert \"--cert-path\" not in out\n        assert \"--key-path\" not in out\n\n        out = self._help_output(['-h'])\n        assert cli.SHORT_USAGE in out\n        assert cli.COMMAND_OVERVIEW[:100] in out\n        assert \"%s\" not in out\n        assert \"{0}\" not in out\n\n    def test_help_no_dashes(self):\n        self._help_output(['help'])  # assert SystemExit is raised here\n\n        out = self._help_output(['help', 'all'])\n        assert \"--configurator\" in out\n        assert \"how a certificate is deployed\" in out\n        assert \"--webroot-path\" in out\n        assert \"--text\" not in out\n        assert \"%s\" not in out\n        assert \"{0}\" not in out\n\n        out = self._help_output(['help', 'install'])\n        assert \"--cert-path\" in out\n        assert \"--key-path\" in out\n\n        out = self._help_output(['help', 'revoke'])\n        assert \"--cert-path\" in out\n        assert \"--key-path\" in out\n\n    def test_parse_domains(self):\n        short_args = ['-d', 'example.com']\n        namespace = self.parse(short_args)\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('example.com')])\n\n        short_args = ['-d', 'trailing.period.com.']\n        namespace = self.parse(short_args)\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('trailing.period.com')])\n\n        short_args = ['-d', 'example.com,another.net,third.org,example.com']\n        namespace = self.parse(short_args)\n        assert_set_by_user_with_value(namespace, 'domains',\n            [san.DNSName('example.com'), san.DNSName('another.net'), san.DNSName('third.org')])\n\n        long_args = ['--domains', 'example.com']\n        namespace = self.parse(long_args)\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('example.com')])\n\n        long_args = ['--domains', 'trailing.period.com.']\n        namespace = self.parse(long_args)\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('trailing.period.com')])\n\n        long_args = ['--domains', 'example.com,another.net,example.com']\n        namespace = self.parse(long_args)\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('example.com'), san.DNSName('another.net')])\n\n    def test_preferred_challenges(self):\n        short_args = ['--preferred-challenges', 'http, dns']\n        namespace = self.parse(short_args)\n\n        expected = [challenges.HTTP01.typ, challenges.DNS01.typ]\n        assert_set_by_user_with_value(namespace, 'pref_challs', expected)\n\n        short_args = ['--preferred-challenges', 'jumping-over-the-moon']\n        # argparse.ArgumentError makes argparse print more information\n        # to stderr and call sys.exit()\n        with mock.patch('sys.stderr'):\n            with pytest.raises(SystemExit):\n                self.parse(short_args)\n\n    def test_server_flag(self):\n        namespace = self.parse('--server example.com'.split())\n        assert_set_by_user_with_value(namespace, 'server', 'example.com')\n\n    def test_must_staple_flag(self):\n        namespace = self.parse(['--must-staple'])\n        assert_set_by_user_with_value(namespace, 'must_staple', True)\n        assert_value_and_source(namespace, 'staple', True, ArgumentSource.RUNTIME)\n\n    def test_must_staple_and_staple_ocsp_flags(self):\n        namespace = self.parse(['--must-staple', '--staple-ocsp'])\n        assert_set_by_user_with_value(namespace, 'must_staple', True)\n        assert_set_by_user_with_value(namespace, 'staple', True)\n\n    def _check_server_conflict_message(self, parser_args, conflicting_args):\n        try:\n            self.parse(parser_args)\n            self.fail(  # pragma: no cover\n                \"The following flags didn't conflict with \"\n                '--server: {0}'.format(', '.join(conflicting_args)))\n        except errors.Error as error:\n            assert '--server' in str(error)\n            for arg in conflicting_args:\n                assert arg in str(error)\n\n    def test_staging_flag(self):\n        short_args = ['--staging']\n        namespace = self.parse(short_args)\n        assert_set_by_user_with_value(namespace, 'staging', True)\n        assert_set_by_user_with_value(namespace, 'server', constants.STAGING_URI)\n\n        short_args += '--server example.com'.split()\n        self._check_server_conflict_message(short_args, '--staging')\n\n    def _assert_dry_run_flag_worked(self, namespace, existing_account):\n        assert_set_by_user_with_value(namespace, 'dry_run', True)\n        assert_value_and_source(namespace, 'break_my_certs', True, ArgumentSource.RUNTIME)\n        assert_value_and_source(namespace, 'staging', True, ArgumentSource.RUNTIME)\n        assert_value_and_source(namespace, 'server', constants.STAGING_URI, ArgumentSource.RUNTIME)\n\n        if existing_account:\n            assert_value_and_source(namespace, 'tos', True, ArgumentSource.RUNTIME)\n            assert_value_and_source(namespace, 'register_unsafely_without_email', True, ArgumentSource.RUNTIME)\n        else:\n            assert_value_and_source(namespace, 'tos', False, ArgumentSource.DEFAULT)\n            assert_value_and_source(namespace, 'register_unsafely_without_email', False, ArgumentSource.DEFAULT)\n\n    def test_dry_run_flag(self):\n        config_dir = tempfile.mkdtemp()\n        short_args = '--dry-run --config-dir {0}'.format(config_dir).split()\n        with pytest.raises(errors.Error):\n            self.parse(short_args)\n\n        self._assert_dry_run_flag_worked(\n            self.parse(short_args + ['auth']), False)\n        self._assert_dry_run_flag_worked(\n            self.parse(short_args + ['certonly']), False)\n        self._assert_dry_run_flag_worked(\n            self.parse(short_args + ['renew']), False)\n\n        account_dir = os.path.join(config_dir, constants.ACCOUNTS_DIR)\n        filesystem.mkdir(account_dir)\n        filesystem.mkdir(os.path.join(account_dir, 'fake_account_dir'))\n\n        self._assert_dry_run_flag_worked(self.parse(short_args + ['auth']), True)\n        self._assert_dry_run_flag_worked(self.parse(short_args + ['renew']), True)\n        self._assert_dry_run_flag_worked(self.parse(short_args + ['certonly']), True)\n\n        short_args += ['certonly']\n\n        # `--dry-run --server example.com` should emit example.com\n        config = self.parse(short_args + ['--server', 'example.com'])\n        assert_set_by_user_with_value(config, 'server', 'example.com')\n\n        # `--dry-run --server STAGING_URI` should emit STAGING_URI\n        config = self.parse(short_args + ['--server', constants.STAGING_URI])\n        assert_set_by_user_with_value(config, 'server', constants.STAGING_URI)\n\n        # `--dry-run --server LIVE` should emit STAGING_URI\n        config = self.parse(short_args + ['--server', cli.flag_default(\"server\")])\n        assert_value_and_source(config, 'server', constants.STAGING_URI, ArgumentSource.RUNTIME)\n\n        # `--dry-run --server example.com --staging` should emit an error\n        conflicts = ['--staging']\n        self._check_server_conflict_message(short_args + ['--server', 'example.com', '--staging'],\n                                            conflicts)\n\n    def test_user_set_rsa_key_size(self):\n        key_size_option = 'rsa_key_size'\n        key_size_value = cli.flag_default(key_size_option)\n        config = self.parse('--rsa-key-size {0}'.format(key_size_value).split())\n\n        assert config.set_by_user(key_size_option)\n\n        config_dir_option = 'config_dir'\n        assert not config.set_by_user(\n            config_dir_option)\n        assert not config.set_by_user('authenticator')\n\n    def test_user_set_installer_and_authenticator(self):\n        config = self.parse('--apache')\n        assert config.set_by_user('installer')\n        assert config.set_by_user('authenticator')\n\n        config = self.parse('--installer webroot')\n        assert config.set_by_user('installer')\n        assert not config.set_by_user('authenticator')\n\n    def test_user_set_ecdsa_key_option(self):\n        elliptic_curve_option = 'elliptic_curve'\n        elliptic_curve_option_value = cli.flag_default(elliptic_curve_option)\n        config = self.parse('--elliptic-curve {0}'.format(elliptic_curve_option_value).split())\n        assert config.set_by_user(elliptic_curve_option)\n\n    def test_user_set_invalid_key_type(self):\n        key_type_option = 'key_type'\n        key_type_value = cli.flag_default(key_type_option)\n        config = self.parse('--key-type {0}'.format(key_type_value).split())\n        assert config.set_by_user(key_type_option)\n\n        with pytest.raises(SystemExit):\n            self.parse(\"--key-type foo\")\n\n    @mock.patch('certbot._internal.hooks.validate_hooks')\n    def test_user_set_deploy_hook(self, unused_mock_validate_hooks):\n        args = 'renew --deploy-hook foo'.split()\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n        assert config.set_by_user('deploy_hook')\n\n    @mock.patch('certbot._internal.plugins.webroot._validate_webroot')\n    def test_user_set_webroot_map(self, mock_validate_webroot):\n        args = 'renew -w /var/www/html -d example.com'.split()\n        mock_validate_webroot.return_value = '/var/www/html'\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n        assert config.set_by_user('webroot_map')\n\n    def test_encode_revocation_reason(self):\n        for reason, code in constants.REVOCATION_REASONS.items():\n            namespace = self.parse(['--reason', reason])\n            assert namespace.reason == code\n        for reason, code in constants.REVOCATION_REASONS.items():\n            namespace = self.parse(['--reason', reason.upper()])\n            assert namespace.reason == code\n\n    def test_force_interactive(self):\n        with pytest.raises(errors.Error):\n            self.parse(\"renew --force-interactive\".split())\n        with pytest.raises(errors.Error):\n            self.parse(\"-n --force-interactive\".split())\n\n    def test_deploy_hook_conflict(self):\n        namespace = self.parse([\"--renew-hook\", \"foo\",\n                                \"--deploy-hook\", \"bar\",\n                                \"--disable-hook-validation\"])\n        assert_set_by_user_with_value(namespace, 'deploy_hook', \"bar\")\n\n    def test_renew_hook_conflict(self):\n        namespace = self.parse([\"--deploy-hook\", \"foo\",\n                                \"--renew-hook\", \"bar\",\n                                \"--disable-hook-validation\"])\n        assert_set_by_user_with_value(namespace, 'deploy_hook', \"bar\")\n\n    def test_deploy_hook_matches_renew_hook(self):\n        value = \"foo\"\n        namespace = self.parse([\"--renew-hook\", value,\n                                \"--deploy-hook\", value,\n                                \"--disable-hook-validation\"])\n        assert_set_by_user_with_value(namespace, 'deploy_hook', value)\n\n    def test_renew_hook_sets_deploy_hook(self):\n        value = \"foo\"\n        namespace = self.parse(\n            [\"--renew-hook\", value, \"--disable-hook-validation\"])\n        assert_set_by_user_with_value(namespace, 'deploy_hook', value)\n\n    def test_renew_hook_matches_deploy_hook(self):\n        value = \"foo\"\n        namespace = self.parse([\"--deploy-hook\", value,\n                                \"--renew-hook\", value,\n                                \"--disable-hook-validation\"])\n        assert_set_by_user_with_value(namespace, 'deploy_hook', value)\n\n    def test_renew_hook_does_not_set_renew_hook(self):\n        value = \"foo\"\n        namespace = self.parse(\n            [\"--renew-hook\", value, \"--disable-hook-validation\"])\n        assert not hasattr(namespace, \"renew_hook\")\n        assert_set_by_user_with_value(namespace, 'deploy_hook', value)\n\n    def test_deploy_hook_does_not_set_renew_hook(self):\n        value = \"foo\"\n        namespace = self.parse(\n            [\"--deploy-hook\", value, \"--disable-hook-validation\"])\n        assert not hasattr(namespace, \"renew_hook\")\n        assert_set_by_user_with_value(namespace, 'deploy_hook', value)\n\n    def test_max_log_backups_error(self):\n        with mock.patch('certbot._internal.cli.sys.stderr'):\n            with pytest.raises(SystemExit):\n                self.parse(\"--max-log-backups foo\".split())\n            with pytest.raises(SystemExit):\n                self.parse(\"--max-log-backups -42\".split())\n\n    def test_max_log_backups_success(self):\n        value = \"42\"\n        namespace = self.parse([\"--max-log-backups\", value])\n        assert_set_by_user_with_value(namespace, 'max_log_backups', int(value))\n\n    def test_unchanging_defaults(self):\n        namespace = self.parse([])\n        assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT)\n        assert_value_and_source(namespace, 'pref_challs', [], ArgumentSource.DEFAULT)\n\n        namespace.pref_challs = [challenges.HTTP01.typ]\n        namespace.domains = [san.DNSName('example.com')]\n\n        namespace = self.parse([])\n        assert_value_and_source(namespace, 'domains', [], ArgumentSource.DEFAULT)\n        assert_value_and_source(namespace, 'pref_challs', [], ArgumentSource.DEFAULT)\n\n    def test_no_directory_hooks_set(self):\n        namespace = self.parse([\"--no-directory-hooks\"])\n        assert_set_by_user_with_value(namespace, 'directory_hooks', False)\n\n    def test_no_directory_hooks_unset(self):\n        namespace = self.parse([])\n        assert_value_and_source(namespace, 'directory_hooks', True, ArgumentSource.DEFAULT)\n\n    def test_delete_after_revoke(self):\n        namespace = self.parse([\"--delete-after-revoke\"])\n        assert_set_by_user_with_value(namespace, 'delete_after_revoke', True)\n\n    def test_delete_after_revoke_default(self):\n        namespace = self.parse([])\n        assert namespace.delete_after_revoke is None\n        assert not namespace.set_by_user('delete_after_revoke')\n\n    def test_no_delete_after_revoke(self):\n        namespace = self.parse([\"--no-delete-after-revoke\"])\n        assert_set_by_user_with_value(namespace, 'delete_after_revoke', False)\n\n    def test_allow_subset_with_wildcard(self):\n        with pytest.raises(errors.Error):\n            self.parse(\"--allow-subset-of-names -d *.example.org\".split())\n\n    def test_route53_no_revert(self):\n        for help_flag in ['-h', '--help']:\n            for topic in ['all', 'plugins', 'dns-route53']:\n                assert 'certbot-route53:auth' not in self._help_output([help_flag, topic])\n\n    def test_parse_args_hosts_and_auto_hosts(self):\n        with pytest.raises(errors.Error):\n            self.parse(['--hsts', '--auto-hsts'])\n\n    def test_parse_with_multiple_argument_sources(self):\n        DEFAULT_VALUE = flag_default('server')\n        CONFIG_FILE_VALUE = 'configfile.biz'\n        COMMAND_LINE_VALUE = 'commandline.edu'\n\n        # check that the default is set\n        namespace = self.parse(['certonly'])\n        assert_value_and_source(namespace, 'server', DEFAULT_VALUE, ArgumentSource.DEFAULT)\n\n        with tempfile.NamedTemporaryFile() as tmp_config:\n            tmp_config.close()  # close now because of compatibility issues on Windows\n            with open(tmp_config.name, 'w') as file_h:\n                file_h.write(f'server = {CONFIG_FILE_VALUE}')\n\n            # first, just provide a value from a config file\n            namespace = self.parse([\n                'certonly',\n                '-c', tmp_config.name,\n            ])\n            assert_value_and_source(namespace, 'server', CONFIG_FILE_VALUE, ArgumentSource.CONFIG_FILE)\n\n            # now provide config file + command line values\n            namespace = self.parse([\n                'certonly',\n                '-c', tmp_config.name,\n                '--server', COMMAND_LINE_VALUE,\n            ])\n            assert_value_and_source(namespace, 'server', COMMAND_LINE_VALUE, ArgumentSource.COMMAND_LINE)\n\n    def test_abbreviated_arguments(self):\n        # Argparse's \"allow_abbrev\" option (which is True by default) allows\n        # for unambiguous partial arguments (e.g. \"--preferred-chal dns\" will be\n        # interpreted the same as \"--preferred-challenges dns\")\n        namespace = self.parse('--preferred-chal dns --no-dir')\n        assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01'])\n        assert_set_by_user_with_value(namespace, 'directory_hooks', False)\n\n        with tempfile.NamedTemporaryFile() as tmp_config:\n            tmp_config.close()  # close now because of compatibility issues on Windows\n            with open(tmp_config.name, 'w') as file_h:\n                file_h.write('preferred-chal = dns')\n\n            namespace = self.parse([\n                'certonly',\n                '--config', tmp_config.name,\n            ])\n            assert_set_by_user_with_value(namespace, 'pref_challs', ['dns-01'])\n\n    @mock.patch('certbot._internal.hooks.validate_hooks')\n    def test_argument_with_equals(self, unsused_mock_validate_hooks):\n        namespace = self.parse('-d=example.com')\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('example.com')])\n\n        # make sure it doesn't choke on equals signs being present in the argument value\n        plugins = disco.PluginsRegistry.find_all()\n        namespace = cli.prepare_and_parse_args(plugins, ['run', '--pre-hook=\"foo=bar\"'])\n        assert_set_by_user_with_value(namespace, 'pre_hook', '\"foo=bar\"')\n\n    def test_adjacent_short_args(self):\n        namespace = self.parse('-tv')\n        assert_set_by_user_with_value(namespace, 'text_mode', True)\n        assert_set_by_user_with_value(namespace, 'verbose_count', 1)\n\n        namespace = self.parse('-tvvv')\n        assert_set_by_user_with_value(namespace, 'text_mode', True)\n        assert_set_by_user_with_value(namespace, 'verbose_count', 3)\n\n        namespace = self.parse('-tvm foo@example.com')\n        assert_set_by_user_with_value(namespace, 'text_mode', True)\n        assert_set_by_user_with_value(namespace, 'verbose_count', 1)\n        assert_set_by_user_with_value(namespace, 'email', 'foo@example.com')\n\n    def test_arg_with_contained_spaces(self):\n        # This can happen if a user specifies an arg like \"-d foo.com\" enclosed\n        # in double quotes, or as its own line in a docker-compose.yml file (as\n        # in #9811)\n        namespace = self.parse(['certonly', '-d foo.com'])\n        assert_set_by_user_with_value(namespace, 'domains', [san.DNSName('foo.com')])\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/client_test.py",
    "content": "\"\"\"Tests for certbot._internal.client.\"\"\"\nimport contextlib\nimport datetime\nimport platform\nimport shutil\nimport sys\nimport tempfile\nimport unittest\nfrom unittest import mock\nfrom unittest.mock import MagicMock\n\nfrom josepy import interfaces\nimport pytest\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import account\nfrom certbot._internal import constants\nfrom certbot._internal import san\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\nKEY = test_util.load_vector(\"rsa512_key.pem\")\nCSR_SAN = test_util.load_vector(\"csr-san_512.pem\")\n\n# pylint: disable=line-too-long\n\n\nclass DetermineUserAgentTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.client.determine_user_agent.\"\"\"\n\n    def _call(self):\n        from certbot._internal.client import determine_user_agent\n        return determine_user_agent(self.config)\n\n    @mock.patch.dict(os.environ, {\"CERTBOT_DOCS\": \"1\"})\n    def test_docs_value(self):\n        self._test(expect_doc_values=True)\n\n    @mock.patch.dict(os.environ, {})\n    def test_real_values(self):\n        self._test(expect_doc_values=False)\n\n    def _test(self, expect_doc_values):\n        ua = self._call()\n\n        if expect_doc_values:\n            doc_value_check = self.assertIn\n            real_value_check = self.assertNotIn\n        else:\n            doc_value_check = self.assertNotIn\n            real_value_check = self.assertIn\n\n        doc_value_check(\"OS_NAME OS_VERSION\", ua)\n        doc_value_check(\"major.minor.patchlevel\", ua)\n        real_value_check(util.get_os_info_ua(), ua)\n        real_value_check(platform.python_version(), ua)\n\n\nclass RegisterTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.client.register.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config.rsa_key_size = 1024\n        self.config.register_unsafely_without_email = False\n        self.config.email = \"alias@example.com\"\n        self.account_storage = account.AccountMemoryStorage()\n        self.tos_cb = mock.MagicMock()\n        display_obj.set_display(MagicMock())\n\n    def _call(self):\n        from certbot._internal.client import register\n        return register(self.config, self.account_storage, self.tos_cb)\n\n    @staticmethod\n    def _public_key_mock():\n        m = mock.Mock(__class__=interfaces.JSONDeSerializable)\n        m.to_partial_json.return_value = '{\"a\": 1}'\n        return m\n\n    @staticmethod\n    def _new_acct_dir_mock():\n        return \"/acme/new-account\"\n\n    @staticmethod\n    def _true_mock():\n        return True\n\n    @staticmethod\n    def _false_mock():\n        return False\n\n    @staticmethod\n    @contextlib.contextmanager\n    def _patched_acme_client():\n        with mock.patch('certbot._internal.client.acme_client') as mock_acme_client:\n            yield mock_acme_client.ClientV2\n\n    def test_no_tos(self):\n        with self._patched_acme_client() as mock_client:\n            mock_client.new_account().terms_of_service = \"http://tos\"\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.prepare_subscription\") as mock_prepare:\n                mock_client().new_account.side_effect = errors.Error\n                with pytest.raises(errors.Error):\n                    self._call()\n                assert mock_prepare.called is False\n\n                mock_client().new_account.side_effect = None\n                self._call()\n                assert mock_prepare.called is True\n\n    @mock.patch('certbot._internal.eff.prepare_subscription')\n    def test_empty_meta(self, unused_mock_prepare):\n        # Test that we can handle an ACME server which does not implement the 'meta'\n        # directory object (for terms-of-service handling).\n        with self._patched_acme_client() as mock_client:\n            from acme.messages import Directory\n            mock_client().directory = Directory.from_json({})\n\n            mock_client().external_account_required.side_effect = self._false_mock\n\n            self._call()\n            assert self.tos_cb.called is False\n\n    @test_util.patch_display_util()\n    def test_it(self, unused_mock_get_utility):\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                self._call()\n            assert self.tos_cb.called is True\n\n    @mock.patch(\"certbot._internal.client.display_ops.get_email\")\n    def test_email_retry(self, mock_get_email):\n        from acme import messages\n        self.config.noninteractive_mode = False\n        msg = \"DNS problem: NXDOMAIN looking up MX for example.com\"\n        mx_err = messages.Error.with_code('invalidContact', detail=msg)\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.prepare_subscription\") as mock_prepare:\n                mock_client().new_account.side_effect = [mx_err, mock.MagicMock()]\n                self._call()\n                assert mock_get_email.call_count == 1\n                assert mock_prepare.called is True\n\n    def test_email_invalid_noninteractive(self):\n        from acme import messages\n        self.config.noninteractive_mode = True\n        msg = \"DNS problem: NXDOMAIN looking up MX for example.com\"\n        mx_err = messages.Error.with_code('invalidContact', detail=msg)\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                mock_client().new_account.side_effect = [mx_err, mock.MagicMock()]\n                with pytest.raises(errors.Error):\n                    self._call()\n\n    def test_no_email_is_chill(self):\n        self.config.email = None\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            self._call()\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_without_email(self, mock_logger):\n        with mock.patch(\"certbot._internal.eff.prepare_subscription\") as mock_prepare:\n            with self._patched_acme_client() as mock_client:\n                mock_client().external_account_required.side_effect = self._false_mock\n                self.config.email = None\n                self.config.register_unsafely_without_email = True\n                self.config.dry_run = False\n                self._call()\n                assert mock_prepare.called is True\n\n    @mock.patch(\"certbot._internal.client.display_ops.get_email\")\n    def test_dry_run_no_staging_account(self, mock_get_email):\n        \"\"\"Tests dry-run for no staging account, expect account created with no email\"\"\"\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                self.config.dry_run = True\n                self._call()\n                # check Certbot did not ask the user to provide an email\n                assert mock_get_email.called is False\n                # check Certbot created an account with no email. Contact should return empty\n                assert not mock_client().new_account.call_args[0][0].contact\n\n    @test_util.patch_display_util()\n    def test_with_eab_arguments(self, unused_mock_get_utility):\n        with self._patched_acme_client() as mock_client:\n            mock_client().client.directory.__getitem__ = mock.Mock(\n                side_effect=self._new_acct_dir_mock\n            )\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                target = \"certbot._internal.client.messages.ExternalAccountBinding.from_data\"\n                with mock.patch(target) as mock_eab_from_data:\n                    self.config.eab_kid = \"test-kid\"\n                    self.config.eab_hmac_key = \"J2OAqW4MHXsrHVa_PVg0Y-L_R4SYw0_aL1le6mfblbE\"\n                    self._call()\n\n                    assert mock_eab_from_data.called is True\n\n    @test_util.patch_display_util()\n    def test_without_eab_arguments(self, unused_mock_get_utility):\n        with self._patched_acme_client() as mock_client:\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                target = \"certbot._internal.client.messages.ExternalAccountBinding.from_data\"\n                with mock.patch(target) as mock_eab_from_data:\n                    self.config.eab_kid = None\n                    self.config.eab_hmac_key = None\n                    self._call()\n\n                    assert mock_eab_from_data.called is False\n\n    def test_external_account_required_without_eab_arguments(self):\n        with self._patched_acme_client() as mock_client:\n            mock_client().client.net.key.public_key = mock.Mock(side_effect=self._public_key_mock)\n            mock_client().external_account_required.side_effect = self._true_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\"):\n                with mock.patch(\"certbot._internal.client.messages.ExternalAccountBinding.from_data\"):\n                    self.config.eab_kid = None\n                    self.config.eab_hmac_key = None\n\n                    with pytest.raises(errors.Error):\n                        self._call()\n\n    def test_unsupported_error(self):\n        from acme import messages\n        msg = \"Test\"\n        mx_err = messages.Error.with_code(\"malformed\", detail=msg, title=\"title\")\n        with self._patched_acme_client() as mock_client:\n            mock_client().client.directory.__getitem__ = mock.Mock(\n                side_effect=self._new_acct_dir_mock\n            )\n            mock_client().external_account_required.side_effect = self._false_mock\n            with mock.patch(\"certbot._internal.eff.handle_subscription\") as mock_handle:\n                mock_client().new_account.side_effect = [mx_err, mock.MagicMock()]\n                with pytest.raises(messages.Error):\n                    self._call()\n        assert mock_handle.called is False\n\n\nclass ClientTestCommon(test_util.ConfigTestCase):\n    \"\"\"Common base class for certbot._internal.client.Client tests.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config.no_verify_ssl = False\n        self.config.allow_subset_of_names = False\n\n        self.account = mock.MagicMock(**{\"key.pem\": KEY})\n\n        from certbot._internal.client import Client\n        with mock.patch(\"certbot._internal.client.acme_client\") as acme:\n            self.acme_client = acme.ClientV2\n            self.acme = self.acme_client.return_value = mock.MagicMock()\n            self.client_network = acme.ClientNetwork\n            self.client = Client(\n                config=self.config, account_=self.account,\n                auth=None, installer=None)\n\n\nclass ClientTest(ClientTestCommon):\n    \"\"\"Tests for certbot._internal.client.Client.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config.allow_subset_of_names = False\n        self.config.dry_run = False\n        self.config.strict_permissions = True\n        self.eg_domains = [\"example.com\", \"www.example.com\"]\n        self.eg_sans = list(map(san.DNSName, self.eg_domains))\n        self.eg_order = mock.MagicMock(\n            authorizations=[None],\n            csr_pem=mock.sentinel.csr_pem)\n\n    def test_init_acme_verify_ssl(self):\n        assert self.client_network.call_args[1]['verify_ssl'] is True\n\n    def _mock_obtain_certificate(self):\n        self.client.auth_handler = mock.MagicMock()\n        self.client.auth_handler.handle_authorizations.return_value = [None]\n        self.client.auth_handler.deactivate_valid_authorizations.return_value = ([], [])\n        self.acme.finalize_order.return_value = self.eg_order\n        self.acme.new_order.return_value = self.eg_order\n        self.eg_order.update.return_value = self.eg_order\n\n    def _check_obtain_certificate(self, auth_count=1):\n        if auth_count == 1:\n            self.client.auth_handler.handle_authorizations.assert_called_once_with(\n                self.eg_order,\n                self.config,\n                self.config.allow_subset_of_names)\n        else:\n            assert self.client.auth_handler.handle_authorizations.call_count == auth_count\n\n        self.acme.finalize_order.assert_called_once_with(\n            self.eg_order, mock.ANY,\n            fetch_alternative_chains=self.config.preferred_chain is not None)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_obtain_certificate_from_csr(self, mock_logger, mock_crypto_util):\n        self._mock_obtain_certificate()\n        test_csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        auth_handler = self.client.auth_handler\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        orderr = self.acme.new_order(test_csr.data)\n        auth_handler.handle_authorizations(orderr, self.config, False)\n        assert (mock.sentinel.cert, mock.sentinel.chain) == \\\n            self.client.obtain_certificate_from_csr(\n                test_csr,\n                orderr=orderr)\n        mock_crypto_util.find_chain_with_issuer.assert_not_called()\n        # and that the cert was obtained correctly\n        self._check_obtain_certificate()\n\n        # Test that --preferred-chain results in chain selection\n        self.config.preferred_chain = \"some issuer\"\n        assert (mock.sentinel.cert, mock.sentinel.chain) == \\\n            self.client.obtain_certificate_from_csr(\n                test_csr,\n                orderr=orderr)\n        mock_crypto_util.find_chain_with_issuer.assert_called_once_with(\n            [orderr.fullchain_pem] + orderr.alternative_fullchains_pem,\n            \"some issuer\", True)\n        self.config.preferred_chain = None\n\n        # Test for default issuance_timeout\n        expected_deadline = \\\n            datetime.datetime.now() + datetime.timedelta(\n                seconds=constants.CLI_DEFAULTS[\"issuance_timeout\"])\n        self.client.obtain_certificate_from_csr(test_csr, orderr=orderr)\n        ((_, deadline), _) = self.client.acme.finalize_order.call_args\n        assert abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1)\n\n        # Test for specific issuance_timeout (300 seconds)\n        expected_deadline = \\\n            datetime.datetime.now() + datetime.timedelta(seconds=300)\n        self.config.issuance_timeout = 300\n        self.client.obtain_certificate_from_csr(test_csr, orderr=orderr)\n        ((_, deadline), _) = self.client.acme.finalize_order.call_args\n        assert abs(expected_deadline - deadline) <= datetime.timedelta(seconds=1)\n\n        # Test for orderr=None\n        assert (mock.sentinel.cert, mock.sentinel.chain) == \\\n            self.client.obtain_certificate_from_csr(\n                test_csr,\n                orderr=None)\n        auth_handler.handle_authorizations.assert_called_with(self.eg_order, self.config, False)\n\n        # Test for no auth_handler\n        self.client.auth_handler = None\n        with pytest.raises(errors.Error):\n            self.client.obtain_certificate_from_csr(test_csr)\n        mock_logger.error.assert_called_once_with(mock.ANY)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n\n        mock_crypto_util.generate_key.assert_called_once_with(\n            key_size=self.config.rsa_key_size,\n            key_dir=None,\n            key_type=self.config.key_type,\n            elliptic_curve=\"secp256r1\",\n            strict_permissions=True,\n        )\n        mock_crypto_util.generate_csr.assert_called_once_with(\n            mock.sentinel.key, self.eg_domains, None, False, True, ipaddrs=[])\n        mock_crypto_util.cert_and_chain_from_fullchain.assert_called_once_with(\n            self.eg_order.fullchain_pem)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_no_profile_preference(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self.client.config.required_profile = None\n        self.client.config.preferred_profile = None\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n        self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_required_profile(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self.client.config.required_profile = \"exampleProfile\"\n        self.client.config.preferred_profile = None\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n        self.acme.new_order.assert_called_once_with(mock.ANY, profile=\"exampleProfile\")\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_preferred_profile_exists(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self.client.config.required_profile = None\n        self.client.config.preferred_profile = \"exampleProfile\"\n\n        from acme.messages import Directory\n        self.acme.directory = Directory.from_json({\n            'meta': {\n                'profiles': {\n                    'exampleProfile': 'here is some descriptive text, very informative',\n                }\n            }\n        })\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n        self.acme.new_order.assert_called_once_with(mock.ANY, profile=\"exampleProfile\")\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_preferred_profile_does_not_exist(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self.client.config.required_profile = None\n        self.client.config.preferred_profile = \"thisProfileDoesNotExist\"\n\n        from acme.messages import Directory\n        self.acme.directory = Directory.from_json({\n            'meta': {\n                'profiles': {\n                    'example': 'profiles!',\n                }\n            }\n        })\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n        self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_preferred_profile_no_profiles_exist(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = mock.sentinel.key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self.client.config.required_profile = None\n        self.client.config.preferred_profile = \"thisProfileDoesNotExist\"\n\n        from acme.messages import Directory\n        self.acme.directory = Directory.from_json({})\n\n        self._test_obtain_certificate_common(mock.sentinel.key, csr)\n        self.acme.new_order.assert_called_once_with(mock.ANY, profile=None)\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_partial_success(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        authzr = (self._authzr_from_sans([san.DNSName(\"example.com\")]))\n        self.config.allow_subset_of_names = True\n        self._test_obtain_certificate_common(key, csr, authzr_ret=authzr, auth_count=2)\n\n        assert mock_crypto_util.generate_key.call_count == 2\n        assert mock_crypto_util.generate_csr.call_count == 2\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_finalize_order_partial_success(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        identifier = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com')\n        subproblem = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier)\n        error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem])\n        self.client.acme.finalize_order.side_effect = [error_with_subproblems, mock.DEFAULT]\n\n        self.config.allow_subset_of_names = True\n\n        with test_util.patch_display_util():\n            result = self.client.obtain_certificate(self.eg_sans)\n\n        assert result == \\\n            (mock.sentinel.cert, mock.sentinel.chain, key, csr)\n        assert self.client.auth_handler.handle_authorizations.call_count == 2\n        assert self.acme.finalize_order.call_count == 2\n\n        assert mock_crypto_util.generate_key.call_count == 2\n        successful_domains = [d for d in self.eg_domains if d != 'example.com']\n        mock_crypto_util.generate_csr.assert_has_calls([\n            mock.call(key, self.eg_domains, None, self.config.must_staple, self.config.strict_permissions, ipaddrs=[]),\n            mock.call(key, successful_domains, None, self.config.must_staple, self.config.strict_permissions, ipaddrs=[])])\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_finalize_order_no_retryable_domains(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        identifier1 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com')\n        identifier2 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='www.example.com')\n        subproblem1 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier1)\n        subproblem2 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier2)\n        error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem1, subproblem2])\n        self.client.acme.finalize_order.side_effect = error_with_subproblems\n\n        self.config.allow_subset_of_names = True\n\n        with pytest.raises(messages.Error):\n            self.client.obtain_certificate(self.eg_sans)\n        assert self.client.auth_handler.handle_authorizations.call_count == 1\n        assert self.acme.finalize_order.call_count == 1\n        assert mock_crypto_util.generate_key.call_count == 1\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_finalize_order_rejected_identifier_no_subproblems(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        error = messages.Error.with_code('caa', detail='foo', title='title')\n        self.client.acme.finalize_order.side_effect = error\n\n        self.config.allow_subset_of_names = True\n\n        with pytest.raises(messages.Error):\n            self.client.obtain_certificate(self.eg_sans)\n        assert self.client.auth_handler.handle_authorizations.call_count == 1\n        assert self.acme.finalize_order.call_count == 1\n        assert mock_crypto_util.generate_key.call_count == 1\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_get_order_partial_success(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        identifier = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com')\n        subproblem = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier)\n        error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem])\n        self.client.acme.new_order.side_effect = [error_with_subproblems, mock.DEFAULT]\n\n        self.config.allow_subset_of_names = True\n\n        with test_util.patch_display_util():\n            result = self.client.obtain_certificate(self.eg_sans)\n\n        assert result == \\\n            (mock.sentinel.cert, mock.sentinel.chain, key, csr)\n        assert self.client.auth_handler.handle_authorizations.call_count == 1\n        assert self.acme.new_order.call_count == 2\n\n        successful_domains = [d for d in self.eg_domains if d != 'example.com']\n        assert mock_crypto_util.generate_key.call_count == 2\n        mock_crypto_util.generate_csr.assert_has_calls([\n            mock.call(key, self.eg_domains, None, self.config.must_staple, self.config.strict_permissions, ipaddrs=[]),\n            mock.call(key, successful_domains, None, self.config.must_staple, self.config.strict_permissions, ipaddrs=[])])\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 1\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_get_order_no_retryable_domains(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        identifier1 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='example.com')\n        identifier2 = messages.Identifier(typ=messages.IDENTIFIER_FQDN, value='www.example.com')\n        subproblem1 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier1)\n        subproblem2 = messages.Error.with_code('caa', detail='bar', title='title', identifier=identifier2)\n        error_with_subproblems = messages.Error.with_code('malformed', detail='foo', title='title', subproblems=[subproblem1, subproblem2])\n        self.client.acme.new_order.side_effect = error_with_subproblems\n\n        self.config.allow_subset_of_names = True\n\n        with pytest.raises(messages.Error):\n            self.client.obtain_certificate(self.eg_sans)\n        assert self.client.auth_handler.handle_authorizations.call_count == 0\n        assert self.acme.new_order.call_count == 1\n        assert mock_crypto_util.generate_key.call_count == 1\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_get_order_rejected_identifier_no_subproblems(self, mock_crypto_util):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        key = util.CSR(form=\"pem\", file=mock.sentinel.key_file, data=CSR_SAN)\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        authzr = self._authzr_from_sans(self.eg_sans)\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        error = messages.Error.with_code('caa', detail='foo', title='title')\n        self.client.acme.new_order.side_effect = error\n\n        self.config.allow_subset_of_names = True\n\n        with pytest.raises(messages.Error):\n            self.client.obtain_certificate(self.eg_sans)\n        assert self.client.auth_handler.handle_authorizations.call_count == 0\n        assert self.acme.new_order.call_count == 1\n        assert mock_crypto_util.generate_key.call_count == 1\n        assert mock_crypto_util.cert_and_chain_from_fullchain.call_count == 0\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    @mock.patch(\"certbot._internal.client.acme_crypto_util\")\n    def test_obtain_certificate_dry_run(self, mock_acme_crypto, mock_crypto):\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_acme_crypto.make_csr.return_value = CSR_SAN\n        mock_crypto.make_key.return_value = mock.sentinel.key_pem\n        key = util.Key(file=None, pem=mock.sentinel.key_pem)\n        self._set_mock_from_fullchain(mock_crypto.cert_and_chain_from_fullchain)\n\n        self.client.config.dry_run = True\n        self._test_obtain_certificate_common(key, csr)\n\n        mock_crypto.make_key.assert_called_once_with(\n            bits=self.config.rsa_key_size,\n            elliptic_curve=\"secp256r1\",\n            key_type=self.config.key_type,\n        )\n        # Assumes all of eg_sans are DNSNames.\n        eg_domains = list(map(str, self.eg_sans))\n        mock_acme_crypto.make_csr.assert_called_once_with(\n            mock.sentinel.key_pem, eg_domains, self.config.must_staple, ipaddrs=[])\n        mock_crypto.generate_key.assert_not_called()\n        mock_crypto.generate_csr.assert_not_called()\n        assert mock_crypto.cert_and_chain_from_fullchain.call_count == 1\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    @mock.patch(\"certbot._internal.client.acme_crypto_util\")\n    def test_obtain_certificate_dry_run_authz_deactivations_failed(self, mock_acme_crypto,\n                                                                   mock_crypto, mock_log):\n        from acme import messages\n        csr = util.CSR(form=\"pem\", file=None, data=CSR_SAN)\n        mock_acme_crypto.make_csr.return_value = CSR_SAN\n        mock_crypto.make_key.return_value = mock.sentinel.key_pem\n        key = util.Key(file=None, pem=mock.sentinel.key_pem)\n        self._set_mock_from_fullchain(mock_crypto.cert_and_chain_from_fullchain)\n\n        self._mock_obtain_certificate()\n        self.client.config.dry_run = True\n\n        # Two authzs that are already valid and should get deactivated (dry run)\n        authzrs = self._authzr_from_sans([san.DNSName(\"example.com\"), san.DNSName(\"www.example.com\")])\n        for authzr in authzrs:\n            authzr.body.status = messages.STATUS_VALID\n\n        # One deactivation succeeds, one fails\n        auth_handler = self.client.auth_handler\n        auth_handler.deactivate_valid_authorizations.return_value = ([authzrs[0]], [authzrs[1]])\n\n        # Certificate should get issued despite one failed deactivation\n        self.eg_order.authorizations = authzrs\n        self.client.auth_handler.handle_authorizations.return_value = authzrs\n        with test_util.patch_display_util():\n            result = self.client.obtain_certificate(self.eg_sans)\n        assert result == (mock.sentinel.cert, mock.sentinel.chain, key, csr)\n        self._check_obtain_certificate(1)\n\n        # Deactivation success/failure should have been handled properly\n        assert auth_handler.deactivate_valid_authorizations.call_count == 1, \\\n                        \"Deactivate authorizations should be called\"\n        assert self.acme.new_order.call_count == 2, \\\n                        \"Order should be recreated due to successfully deactivated authorizations\"\n        mock_log.warning.assert_called_with(\"Certbot was unable to obtain fresh authorizations for\"\n                                            \" every domain. The dry run will continue, but results\"\n                                            \" may not be accurate.\")\n\n    @mock.patch(\"certbot._internal.client.crypto_util\")\n    def test_obtain_certificate_reuse_key_with_allow_subset_of_names(self, mock_crypto_util):\n        csr = util.CSR(form=\"pem\", file=mock.sentinel.csr_file, data=CSR_SAN)\n        old_key = util.Key(file=\"old_key_file\", pem=\"old_key_pem\")\n        new_key = util.Key(file=\"new_key_file\", pem=\"new_key_pem\")\n        mock_crypto_util.generate_csr.return_value = csr\n        mock_crypto_util.generate_key.return_value = new_key\n        self._set_mock_from_fullchain(mock_crypto_util.cert_and_chain_from_fullchain)\n\n        authzr = self._authzr_from_sans([san.DNSName(\"example.com\")])\n        self.config.allow_subset_of_names = True\n        self.config.reuse_key = True\n\n        self._mock_obtain_certificate()\n\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        with test_util.patch_display_util():\n            mocked_open = mock.mock_open(read_data=\"old_key_pem\")\n            with mock.patch('builtins.open', mocked_open):\n                result = self.client.obtain_certificate(self.eg_sans, \"old_key_file\")\n\n        assert result == \\\n            (mock.sentinel.cert, mock.sentinel.chain, old_key, csr)\n        self._check_obtain_certificate(2)\n\n        assert mock_crypto_util.generate_key.call_count == 0\n\n    def _set_mock_from_fullchain(self, mock_from_fullchain):\n        mock_cert = mock.Mock()\n        mock_cert.encode.return_value = mock.sentinel.cert\n        mock_chain = mock.Mock()\n        mock_chain.encode.return_value = mock.sentinel.chain\n        mock_from_fullchain.return_value = (mock_cert, mock_chain)\n\n    def _authzr_from_sans(self, sans):\n        authzr = []\n\n        # domain ordering should not be affected by authorization order\n        for s in reversed(sans):\n            if type(s) is not san.DNSName:\n                raise TypeError(f\"expected DNSName but got {type(s)}\")\n            authzr.append(\n                mock.MagicMock(\n                    body=mock.MagicMock(\n                        identifier=mock.MagicMock(\n                            value=str(s),\n                            typ=\"dns\"))))\n        return authzr\n\n    def _test_obtain_certificate_common(self, key, csr, authzr_ret=None, auth_count=1):\n        self._mock_obtain_certificate()\n\n        # return_value is essentially set to (None, None) in\n        # _mock_obtain_certificate(), which breaks this test.\n        # Thus fixed by the next line.\n        authzr = authzr_ret or self._authzr_from_sans(self.eg_sans)\n\n        self.eg_order.authorizations = authzr\n        self.client.auth_handler.handle_authorizations.return_value = authzr\n\n        with test_util.patch_display_util():\n            result = self.client.obtain_certificate(self.eg_sans)\n\n        assert result == \\\n            (mock.sentinel.cert, mock.sentinel.chain, key, csr)\n        self._check_obtain_certificate(auth_count)\n\n    @mock.patch('certbot._internal.client.Client.obtain_certificate')\n    @mock.patch('certbot._internal.storage.RenewableCert.new_lineage')\n    def test_obtain_and_enroll_certificate(self,\n                                           mock_storage, mock_obtain_certificate):\n        sans = [san.DNSName(\"*.example.com\"), san.DNSName(\"example.com\")]\n        mock_obtain_certificate.return_value = (mock.MagicMock(),\n                                                mock.MagicMock(), mock.MagicMock(), None)\n\n        self.client.config.dry_run = False\n        assert self.client.obtain_and_enroll_certificate(sans, \"example_cert\")\n\n        assert self.client.obtain_and_enroll_certificate(sans, None)\n        assert self.client.obtain_and_enroll_certificate(sans[1:], None)\n\n        self.client.config.dry_run = True\n\n        assert not self.client.obtain_and_enroll_certificate(sans, None)\n\n        names = [call[0][0] for call in mock_storage.call_args_list]\n        assert names == [\"example_cert\", \"example.com\", \"example.com\"]\n\n    @mock.patch(\"certbot._internal.cli.helpful_parser\")\n    def test_save_certificate(self, mock_parser):\n        certs = [\"cert_512.pem\", \"cert-san_512.pem\"]\n        tmp_path = tempfile.mkdtemp()\n\n        cert_pem = test_util.load_vector(certs[0])\n        chain_pem = (test_util.load_vector(certs[0]) + test_util.load_vector(certs[1]))\n        candidate_cert_path = os.path.join(tmp_path, \"certs\", \"cert_512.pem\")\n        candidate_chain_path = os.path.join(tmp_path, \"chains\", \"chain.pem\")\n        candidate_fullchain_path = os.path.join(tmp_path, \"chains\", \"fullchain.pem\")\n        mock_parser.verb = \"certonly\"\n        mock_parser.args = [\"--cert-path\", candidate_cert_path,\n                            \"--chain-path\", candidate_chain_path,\n                            \"--fullchain-path\", candidate_fullchain_path]\n\n        cert_path, chain_path, fullchain_path = self.client.save_certificate(\n            cert_pem, chain_pem, candidate_cert_path, candidate_chain_path,\n            candidate_fullchain_path)\n\n        assert os.path.dirname(cert_path) == \\\n                         os.path.dirname(candidate_cert_path)\n        assert os.path.dirname(chain_path) == \\\n                         os.path.dirname(candidate_chain_path)\n        assert os.path.dirname(fullchain_path) == \\\n                         os.path.dirname(candidate_fullchain_path)\n\n        with open(cert_path, \"rb\") as cert_file:\n            cert_contents = cert_file.read()\n        assert cert_contents == test_util.load_vector(certs[0])\n\n        with open(chain_path, \"rb\") as chain_file:\n            chain_contents = chain_file.read()\n        assert chain_contents == test_util.load_vector(certs[0]) + \\\n                         test_util.load_vector(certs[1])\n\n        shutil.rmtree(tmp_path)\n\n    @test_util.patch_display_util()\n    def test_deploy_certificate_success(self, mock_util):\n        with pytest.raises(errors.Error):\n            self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n\n        installer = mock.MagicMock()\n        self.client.installer = installer\n\n        self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n        installer.deploy_cert.assert_called_once_with(\n            cert_path=os.path.abspath(\"cert\"),\n            chain_path=os.path.abspath(\"chain\"),\n            domain='foo.bar',\n            fullchain_path='fullchain',\n            key_path=os.path.abspath(\"key\"))\n        assert installer.save.call_count == 2\n        installer.restart.assert_called_once_with()\n\n    @mock.patch('certbot._internal.client.display_util.notify')\n    @test_util.patch_display_util()\n    def test_deploy_certificate_failure(self, mock_util, mock_notify):\n        installer = mock.MagicMock()\n        self.client.installer = installer\n        self.config.installer = \"foobar\"\n\n        installer.deploy_cert.side_effect = errors.PluginError\n        with pytest.raises(errors.PluginError):\n            self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n        installer.recovery_routine.assert_called_once_with()\n\n        mock_notify.assert_any_call('Deploying certificate')\n\n\n    @test_util.patch_display_util()\n    def test_deploy_certificate_save_failure(self, mock_util):\n        installer = mock.MagicMock()\n        self.client.installer = installer\n\n        installer.save.side_effect = errors.PluginError\n        with pytest.raises(errors.PluginError):\n            self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n        installer.recovery_routine.assert_called_once_with()\n\n    @mock.patch('certbot._internal.client.display_util.notify')\n    @test_util.patch_display_util()\n    def test_deploy_certificate_restart_failure(self, mock_get_utility, mock_notify):\n        installer = mock.MagicMock()\n        installer.restart.side_effect = [errors.PluginError, None]\n        self.client.installer = installer\n\n        with pytest.raises(errors.PluginError):\n            self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n        mock_notify.assert_called_with(\n            'We were unable to install your certificate, however, we successfully restored '\n            'your server to its prior configuration.')\n        installer.rollback_checkpoints.assert_called_once_with()\n        assert installer.restart.call_count == 2\n\n    @mock.patch('certbot._internal.client.logger')\n    @test_util.patch_display_util()\n    def test_deploy_certificate_restart_failure2(self, mock_get_utility, mock_logger):\n        installer = mock.MagicMock()\n        installer.restart.side_effect = errors.PluginError\n        installer.rollback_checkpoints.side_effect = errors.ReverterError\n        self.client.installer = installer\n\n        with pytest.raises(errors.PluginError):\n            self.client.deploy_certificate([san.DNSName(\"foo.bar\")], \"key\", \"cert\", \"chain\", \"fullchain\")\n        assert mock_logger.error.call_count == 1\n        assert 'An error occurred and we failed to restore your config' in \\\n            mock_logger.error.call_args[0][0]\n        installer.rollback_checkpoints.assert_called_once_with()\n        assert installer.restart.call_count == 1\n\n    def test_choose_lineage_name(self):\n        sep = os.path.sep\n        invalid_domains = [san.DNSName(f\"exam{sep}ple.com\")]\n        valid_domains = [san.DNSName(\"example.com\")]\n        invalid_certname = f\"foo{sep}.bar\"\n        valid_certname = \"foo.bar\"\n        invalid_wildcard_domain = [san.DNSName(f\"*.exam{sep}ple.com\")]\n        # Verify errors are raised when invalid lineagename is chosen.\n        with pytest.raises(errors.Error):\n            self.client._choose_lineagename(invalid_domains, None)\n        with pytest.raises(errors.Error):\n            self.client._choose_lineagename(invalid_domains, invalid_certname)\n        with pytest.raises(errors.Error):\n            self.client._choose_lineagename(valid_domains, invalid_certname)\n        with pytest.raises(errors.Error):\n            self.client._choose_lineagename(invalid_wildcard_domain, None)\n        # Verify no error is raised when invalid domain is overridden by valid certname.\n        self.client._choose_lineagename(invalid_domains, valid_certname)\n\n\nclass EnhanceConfigTest(ClientTestCommon):\n    \"\"\"Tests for certbot._internal.client.Client.enhance_config.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config.hsts = False\n        self.config.redirect = False\n        self.config.staple = False\n        self.config.uir = False\n        self.domain = \"example.org\"\n\n    def test_no_installer(self):\n        with pytest.raises(errors.Error):\n            self.client.enhance_config([san.DNSName(self.domain)], None)\n\n    def test_unsupported(self):\n        self.client.installer = mock.MagicMock()\n        self.client.installer.supported_enhancements.return_value = []\n\n        self.config.redirect = None\n        self.config.hsts = True\n        with mock.patch(\"certbot._internal.client.logger\") as mock_logger:\n            self.client.enhance_config([self.domain], None)\n        assert mock_logger.error.call_count == 1\n        self.client.installer.enhance.assert_not_called()\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_already_exists_header(self, mock_log):\n        self.config.hsts = True\n        self._test_with_already_existing()\n        assert mock_log.info.called is True\n        assert mock_log.info.call_args[0][1] == \\\n                          'Strict-Transport-Security'\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_already_exists_redirect(self, mock_log):\n        self.config.redirect = True\n        self._test_with_already_existing()\n        assert mock_log.info.called is True\n        assert mock_log.info.call_args[0][1] == \\\n                          'redirect'\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_config_set_no_warning_redirect(self, mock_log):\n        self.config.redirect = False\n        self._test_with_already_existing()\n        assert mock_log.warning.called is False\n\n    @mock.patch(\"certbot._internal.client.logger\")\n    def test_no_warn_redirect(self, mock_log):\n        self.config.redirect = None\n        self._test_with_all_supported()\n        assert mock_log.warning.called is False\n\n    def test_no_ask_hsts(self):\n        self.config.hsts = True\n        self._test_with_all_supported()\n        self.client.installer.enhance.assert_called_with(\n            self.domain, \"ensure-http-header\", \"Strict-Transport-Security\")\n\n    def test_no_ask_redirect(self):\n        self.config.redirect = True\n        self._test_with_all_supported()\n        self.client.installer.enhance.assert_called_with(\n            self.domain, \"redirect\", None)\n\n    def test_no_ask_staple(self):\n        self.config.staple = True\n        self._test_with_all_supported()\n        self.client.installer.enhance.assert_called_with(\n            self.domain, \"staple-ocsp\", None)\n\n    def test_no_ask_uir(self):\n        self.config.uir = True\n        self._test_with_all_supported()\n        self.client.installer.enhance.assert_called_with(\n            self.domain, \"ensure-http-header\", \"Upgrade-Insecure-Requests\")\n\n    def test_enhance_failure(self):\n        self.client.installer = mock.MagicMock()\n        self.client.installer.enhance.side_effect = errors.PluginError\n        self._test_error(enhance_error=True)\n        self.client.installer.recovery_routine.assert_called_once_with()\n\n    def test_save_failure(self):\n        self.client.installer = mock.MagicMock()\n        self.client.installer.save.side_effect = errors.PluginError\n        self._test_error()\n        self.client.installer.recovery_routine.assert_called_once_with()\n        self.client.installer.save.assert_called_once_with(mock.ANY)\n\n    def test_restart_failure(self):\n        self.client.installer = mock.MagicMock()\n        self.client.installer.restart.side_effect = [errors.PluginError, None]\n        self._test_error_with_rollback()\n\n    def test_restart_failure2(self):\n        installer = mock.MagicMock()\n        installer.restart.side_effect = errors.PluginError\n        installer.rollback_checkpoints.side_effect = errors.ReverterError\n        self.client.installer = installer\n        self._test_error_with_rollback()\n\n    def _test_error_with_rollback(self):\n        self._test_error()\n        assert self.client.installer.restart.called is True\n\n    def _test_error(self, enhance_error=False, restart_error=False):\n        self.config.redirect = True\n        with mock.patch('certbot._internal.client.logger') as mock_logger, \\\n             test_util.patch_display_util():\n            with pytest.raises(errors.PluginError):\n                self._test_with_all_supported()\n\n        if enhance_error:\n            assert mock_logger.error.call_count == 1\n            assert 'Unable to set the %s enhancement for %s.' == mock_logger.error.call_args_list[0][0][0]\n        if restart_error:\n            mock_logger.critical.assert_called_with(\n                'Rolling back to previous server configuration...')\n\n    def _test_with_all_supported(self):\n        if self.client.installer is None:\n            self.client.installer = mock.MagicMock()\n        self.client.installer.supported_enhancements.return_value = [\n            \"ensure-http-header\", \"redirect\", \"staple-ocsp\"]\n        self.client.enhance_config([san.DNSName(self.domain)], None)\n        assert self.client.installer.save.call_count == 1\n        assert self.client.installer.restart.call_count == 1\n\n    def _test_with_already_existing(self):\n        self.client.installer = mock.MagicMock()\n        self.client.installer.supported_enhancements.return_value = [\n            \"ensure-http-header\", \"redirect\", \"staple-ocsp\"]\n        self.client.installer.enhance.side_effect = errors.PluginEnhancementAlreadyPresent()\n        self.client.enhance_config([san.DNSName(self.domain)], None)\n\n\nclass RollbackTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.client.rollback.\"\"\"\n\n    def setUp(self):\n        self.m_install = mock.MagicMock()\n\n    @classmethod\n    def _call(cls, checkpoints, side_effect):\n        from certbot._internal.client import rollback\n        with mock.patch(\"certbot._internal.client.plugin_selection.pick_installer\") as mpi:\n            mpi.side_effect = side_effect\n            rollback(None, checkpoints, {}, mock.MagicMock())\n\n    def test_no_problems(self):\n        self._call(1, self.m_install)\n        assert self.m_install().rollback_checkpoints.call_count == 1\n        assert self.m_install().restart.call_count == 1\n\n    def test_no_installer(self):\n        self._call(1, None)  # Just make sure no exceptions are raised\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/compat/__init__.py",
    "content": ""
  },
  {
    "path": "certbot/src/certbot/_internal/tests/compat/filesystem_test.py",
    "content": "\"\"\"Tests for certbot.compat.filesystem\"\"\"\nimport contextlib\nimport errno\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import util\nfrom certbot._internal import lock\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\nfrom certbot.tests.util import TempDirTestCase\n\ntry:\n    import ntsecuritycon\n    import win32api\n    import win32security\n    POSIX_MODE = False\nexcept ImportError:\n    POSIX_MODE = True\n\n\n\nEVERYBODY_SID = 'S-1-1-0'\nSYSTEM_SID = 'S-1-5-18'\nADMINS_SID = 'S-1-5-32-544'\n\n\n@unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security')\nclass WindowsChmodTests(TempDirTestCase):\n    \"\"\"Unit tests for Windows chmod function in filesystem module\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.probe_path = _create_probe(self.tempdir)\n\n    def test_symlink_resolution(self):\n        link_path = os.path.join(self.tempdir, 'link')\n        os.symlink(self.probe_path, link_path)\n\n        ref_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n        ref_dacl_link = _get_security_dacl(link_path).GetSecurityDescriptorDacl()\n\n        filesystem.chmod(link_path, 0o700)\n\n        # Assert the real file is impacted, not the link.\n        cur_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n        cur_dacl_link = _get_security_dacl(link_path).GetSecurityDescriptorDacl()\n        assert not filesystem._compare_dacls(ref_dacl_probe, cur_dacl_probe)  # pylint: disable=protected-access\n        assert filesystem._compare_dacls(ref_dacl_link, cur_dacl_link)  # pylint: disable=protected-access\n\n    def test_world_permission(self):\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        filesystem.chmod(self.probe_path, 0o700)\n        dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n        filesystem.chmod(self.probe_path, 0o704)\n        dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n\n        assert [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                         if dacl.GetAce(index)[2] == everybody]\n\n    def test_group_permissions_noop(self):\n        filesystem.chmod(self.probe_path, 0o700)\n        ref_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n\n        filesystem.chmod(self.probe_path, 0o740)\n        cur_dacl_probe = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n\n        assert filesystem._compare_dacls(ref_dacl_probe, cur_dacl_probe)  # pylint: disable=protected-access\n\n    def test_admin_permissions(self):\n        system = win32security.ConvertStringSidToSid(SYSTEM_SID)\n        admins = win32security.ConvertStringSidToSid(ADMINS_SID)\n\n        filesystem.chmod(self.probe_path, 0o400)\n        dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n\n        system_aces = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                       if dacl.GetAce(index)[2] == system]\n        admin_aces = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                      if dacl.GetAce(index)[2] == admins]\n\n        assert len(system_aces) == 1\n        assert len(admin_aces) == 1\n\n        assert system_aces[0][1] == ntsecuritycon.FILE_ALL_ACCESS\n        assert admin_aces[0][1] == ntsecuritycon.FILE_ALL_ACCESS\n\n    def test_read_flag(self):\n        self._test_flag(4, ntsecuritycon.FILE_GENERIC_READ)\n\n    def test_execute_flag(self):\n        self._test_flag(1, ntsecuritycon.FILE_GENERIC_EXECUTE)\n\n    def test_write_flag(self):\n        self._test_flag(2, (ntsecuritycon.FILE_ALL_ACCESS\n                            ^ ntsecuritycon.FILE_GENERIC_READ\n                            ^ ntsecuritycon.FILE_GENERIC_EXECUTE))\n\n    def test_full_flag(self):\n        self._test_flag(7, ntsecuritycon.FILE_ALL_ACCESS)\n\n    def _test_flag(self, everyone_mode, windows_flag):\n        # Note that flag is tested against `everyone`, not `user`, because practically these unit\n        # tests are executed with admin privilege, so current user is effectively the admins group,\n        # and so will always have all rights.\n        filesystem.chmod(self.probe_path, 0o700 | everyone_mode)\n        dacl = _get_security_dacl(self.probe_path).GetSecurityDescriptorDacl()\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        acls_everybody = [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n        assert len(acls_everybody) == 1\n\n        acls_everybody = acls_everybody[0]\n\n        assert acls_everybody[1] == windows_flag\n\n    def test_user_admin_dacl_consistency(self):\n        # Set ownership of target to authenticated user\n        authenticated_user, _, _ = win32security.LookupAccountName(\"\", win32api.GetUserName())\n        security_owner = _get_security_owner(self.probe_path)\n        _set_owner(self.probe_path, security_owner, authenticated_user)\n\n        filesystem.chmod(self.probe_path, 0o700)\n\n        security_dacl = _get_security_dacl(self.probe_path)\n        # We expect three ACE: one for admins, one for system, and one for the user\n        assert security_dacl.GetSecurityDescriptorDacl().GetAceCount() == 3\n\n        # Set ownership of target to Administrators user group\n        admin_user = win32security.ConvertStringSidToSid(ADMINS_SID)\n        security_owner = _get_security_owner(self.probe_path)\n        _set_owner(self.probe_path, security_owner, admin_user)\n\n        filesystem.chmod(self.probe_path, 0o700)\n\n        security_dacl = _get_security_dacl(self.probe_path)\n        # We expect only two ACE: one for admins, one for system,\n        # since the user is also the admins group\n        assert security_dacl.GetSecurityDescriptorDacl().GetAceCount() == 2\n\n\nclass UmaskTest(TempDirTestCase):\n    def test_umask_on_dir(self):\n        previous_umask = filesystem.umask(0o022)\n\n        try:\n            dir1 = os.path.join(self.tempdir, 'probe1')\n            filesystem.mkdir(dir1)\n            assert filesystem.check_mode(dir1, 0o755) is True\n\n            filesystem.umask(0o077)\n\n            dir2 = os.path.join(self.tempdir, 'dir2')\n            filesystem.mkdir(dir2)\n            assert filesystem.check_mode(dir2, 0o700) is True\n\n            dir3 = os.path.join(self.tempdir, 'dir3')\n            filesystem.mkdir(dir3, mode=0o777)\n            assert filesystem.check_mode(dir3, 0o700) is True\n        finally:\n            filesystem.umask(previous_umask)\n\n    def test_umask_on_file(self):\n        previous_umask = filesystem.umask(0o022)\n\n        try:\n            file1 = os.path.join(self.tempdir, 'probe1')\n            UmaskTest._create_file(file1)\n            assert filesystem.check_mode(file1, 0o755) is True\n\n            filesystem.umask(0o077)\n\n            file2 = os.path.join(self.tempdir, 'probe2')\n            UmaskTest._create_file(file2)\n            assert filesystem.check_mode(file2, 0o700) is True\n\n            file3 = os.path.join(self.tempdir, 'probe3')\n            UmaskTest._create_file(file3)\n            assert filesystem.check_mode(file3, 0o700) is True\n        finally:\n            filesystem.umask(previous_umask)\n\n    @staticmethod\n    def _create_file(path, mode=0o777):\n        file_desc = None\n        try:\n            file_desc = filesystem.open(path, flags=os.O_CREAT, mode=mode)\n        finally:\n            if file_desc:\n                os.close(file_desc)\n\n\nclass ComputePrivateKeyModeTest(TempDirTestCase):\n    def setUp(self):\n        super().setUp()\n        self.probe_path = _create_probe(self.tempdir)\n\n    def test_compute_private_key_mode(self):\n        filesystem.chmod(self.probe_path, 0o777)\n        new_mode = filesystem.compute_private_key_mode(self.probe_path, 0o600)\n\n        if POSIX_MODE:\n            # On Linux RWX permissions for group and R permission for world\n            # are persisted from the existing moe\n            assert new_mode == 0o674\n        else:\n            # On Windows no permission is persisted\n            assert new_mode == 0o600\n\n\n@unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows security')\nclass WindowsOpenTest(TempDirTestCase):\n    def test_new_file_correct_permissions(self):\n        path = os.path.join(self.tempdir, 'file')\n\n        desc = filesystem.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, 0o700)\n        os.close(desc)\n\n        dacl = _get_security_dacl(path).GetSecurityDescriptorDacl()\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n    def test_existing_file_correct_permissions(self):\n        path = os.path.join(self.tempdir, 'file')\n        open(path, 'w').close()\n\n        desc = filesystem.open(path, os.O_EXCL | os.O_RDWR, 0o700)\n        os.close(desc)\n\n        dacl = _get_security_dacl(path).GetSecurityDescriptorDacl()\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n    def test_create_file_on_open(self):\n        # os.O_CREAT | os.O_EXCL + file not exists = OK\n        self._test_one_creation(1, file_exist=False, flags=(os.O_CREAT | os.O_EXCL))\n\n        # os.O_CREAT | os.O_EXCL + file exists = EEXIST OS exception\n        with pytest.raises(OSError) as exc_info:\n            self._test_one_creation(2, file_exist=True, flags=(os.O_CREAT | os.O_EXCL))\n        assert exc_info.value.errno == errno.EEXIST\n\n        # os.O_CREAT + file not exists = OK\n        self._test_one_creation(3, file_exist=False, flags=os.O_CREAT)\n\n        # os.O_CREAT + file exists = OK\n        self._test_one_creation(4, file_exist=True, flags=os.O_CREAT)\n\n        # os.O_CREAT + file exists (locked) = EACCES OS exception\n        path = os.path.join(self.tempdir, '5')\n        open(path, 'w').close()\n        filelock = lock.LockFile(path)\n        try:\n            with pytest.raises(OSError) as exc_info:\n                self._test_one_creation(5, file_exist=True, flags=os.O_CREAT)\n            assert exc_info.value.errno == errno.EACCES\n        finally:\n            filelock.release()\n\n        # os.O_CREAT not set + file not exists = OS exception\n        with pytest.raises(OSError):\n            self._test_one_creation(6, file_exist=False, flags=os.O_RDONLY)\n\n    def _test_one_creation(self, num, file_exist, flags):\n        one_file = os.path.join(self.tempdir, str(num))\n        if file_exist and not os.path.exists(one_file):\n            with open(one_file, 'w'):\n                pass\n\n        handler = None\n        try:\n            handler = filesystem.open(one_file, flags)\n        finally:\n            if handler:\n                os.close(handler)\n\n\nclass TempUmaskTests(test_util.TempDirTestCase):\n    \"\"\"Tests for using the TempUmask class in `with` statements\"\"\"\n    def _check_umask(self):\n        old_umask = filesystem.umask(0)\n        filesystem.umask(old_umask)\n        return old_umask\n\n    def test_works_normally(self):\n        filesystem.umask(0o0022)\n        assert self._check_umask() == 0o0022\n        with filesystem.temp_umask(0o0077):\n            assert self._check_umask() == 0o0077\n        assert self._check_umask() == 0o0022\n\n    def test_resets_umask_after_exception(self):\n        filesystem.umask(0o0022)\n        assert self._check_umask() == 0o0022\n        try:\n            with filesystem.temp_umask(0o0077):\n                assert self._check_umask() == 0o0077\n                raise Exception()\n        except:\n            assert self._check_umask() == 0o0022\n\n\n@unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')\nclass WindowsMkdirTests(test_util.TempDirTestCase):\n    \"\"\"Unit tests for Windows mkdir + makedirs functions in filesystem module\"\"\"\n    def test_mkdir_correct_permissions(self):\n        path = os.path.join(self.tempdir, 'dir')\n\n        filesystem.mkdir(path, 0o700)\n\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        dacl = _get_security_dacl(path).GetSecurityDescriptorDacl()\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n    def test_makedirs_correct_permissions(self):\n        path = os.path.join(self.tempdir, 'dir')\n        subpath = os.path.join(path, 'subpath')\n\n        filesystem.makedirs(subpath, 0o700)\n\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n\n        dacl = _get_security_dacl(subpath).GetSecurityDescriptorDacl()\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n    def test_makedirs_switch_os_mkdir(self):\n        path = os.path.join(self.tempdir, 'dir')\n        import os as std_os  # pylint: disable=os-module-forbidden\n        original_mkdir = std_os.mkdir\n\n        filesystem.makedirs(path)\n        assert original_mkdir == std_os.mkdir\n\n        try:\n            filesystem.makedirs(path)  # Will fail because path already exists\n        except OSError:\n            pass\n        assert original_mkdir == std_os.mkdir\n\n\nclass MakedirsTests(test_util.TempDirTestCase):\n    \"\"\"Unit tests for makedirs function in filesystem module\"\"\"\n    def test_makedirs_correct_permissions(self):\n        path = os.path.join(self.tempdir, 'dir')\n        subpath = os.path.join(path, 'subpath')\n\n        previous_umask = filesystem.umask(0o022)\n\n        try:\n            filesystem.makedirs(subpath, 0o700)\n\n            assert filesystem.check_mode(path, 0o700)\n            assert filesystem.check_mode(subpath, 0o700)\n        finally:\n            filesystem.umask(previous_umask)\n\n\nclass CopyOwnershipAndModeTest(test_util.TempDirTestCase):\n    \"\"\"Tests about copy_ownership_and_apply_mode, copy_ownership_and_mode and has_same_ownership\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.probe_path = _create_probe(self.tempdir)\n\n    @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')\n    def test_copy_ownership_and_apply_mode_windows(self):\n        system = win32security.ConvertStringSidToSid(SYSTEM_SID)\n        security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR\n        security.SetSecurityDescriptorOwner(system, False)\n\n        with mock.patch('win32security.GetFileSecurity') as mock_get:\n            with mock.patch('win32security.SetFileSecurity') as mock_set:\n                mock_get.return_value = security\n                filesystem.copy_ownership_and_apply_mode(\n                    'dummy', self.probe_path, 0o700, copy_user=True, copy_group=False)\n\n        assert mock_set.call_count == 2\n\n        first_call = mock_set.call_args_list[0]\n        security = first_call[0][2]\n        assert system == security.GetSecurityDescriptorOwner()\n\n        second_call = mock_set.call_args_list[1]\n        security = second_call[0][2]\n        dacl = security.GetSecurityDescriptorDacl()\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n        assert dacl.GetAceCount()\n        assert not [dacl.GetAce(index) for index in range(0, dacl.GetAceCount())\n                          if dacl.GetAce(index)[2] == everybody]\n\n    @unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security')\n    def test_copy_ownership_and_apply_mode_linux(self):\n        with mock.patch('os.chown') as mock_chown:\n            with mock.patch('os.chmod') as mock_chmod:\n                with mock.patch('os.stat') as mock_stat:\n                    mock_stat.return_value.st_uid = 50\n                    mock_stat.return_value.st_gid = 51\n                    filesystem.copy_ownership_and_apply_mode(\n                        'dummy', self.probe_path, 0o700, copy_user=True, copy_group=True)\n\n        mock_chown.assert_called_once_with(self.probe_path, 50, 51)\n        mock_chmod.assert_called_once_with(self.probe_path, 0o700)\n\n    def test_has_same_ownership(self):\n        path1 = os.path.join(self.tempdir, 'test1')\n        path2 = os.path.join(self.tempdir, 'test2')\n\n        util.safe_open(path1, 'w').close()\n        util.safe_open(path2, 'w').close()\n\n        assert filesystem.has_same_ownership(path1, path2) is True\n\n    @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')\n    def test_copy_ownership_and_mode_windows(self):\n        src = self.probe_path\n        dst = _create_probe(self.tempdir, name='dst')\n\n        filesystem.chmod(src, 0o700)\n        assert filesystem.check_mode(src, 0o700) is True\n        assert filesystem.check_mode(dst, 0o744) is True\n\n        # Checking an actual change of owner is tricky during a unit test, since we do not know\n        # if any user exists beside the current one. So we mock _copy_win_ownership. It's behavior\n        # have been checked theoretically with test_copy_ownership_and_apply_mode_windows.\n        with mock.patch('certbot.compat.filesystem._copy_win_ownership') as mock_copy_owner:\n            filesystem.copy_ownership_and_mode(src, dst)\n\n        mock_copy_owner.assert_called_once_with(src, dst)\n        assert filesystem.check_mode(dst, 0o700) is True\n\n\nclass CheckPermissionsTest(test_util.TempDirTestCase):\n    \"\"\"Tests relative to functions that check modes.\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.probe_path = _create_probe(self.tempdir)\n\n    def test_check_mode(self):\n        assert filesystem.check_mode(self.probe_path, 0o744) is True\n\n        filesystem.chmod(self.probe_path, 0o700)\n        assert not filesystem.check_mode(self.probe_path, 0o744)\n\n    @unittest.skipIf(POSIX_MODE, reason='Test specific to Windows security')\n    def test_check_owner_windows(self):\n        assert filesystem.check_owner(self.probe_path) is True\n\n        system = win32security.ConvertStringSidToSid(SYSTEM_SID)\n        security = win32security.SECURITY_ATTRIBUTES().SECURITY_DESCRIPTOR\n        security.SetSecurityDescriptorOwner(system, False)\n\n        with mock.patch('win32security.GetFileSecurity') as mock_get:\n            mock_get.return_value = security\n            assert not filesystem.check_owner(self.probe_path)\n\n    @unittest.skipUnless(POSIX_MODE, reason='Test specific to Linux security')\n    def test_check_owner_linux(self):\n        assert filesystem.check_owner(self.probe_path) is True\n\n        import os as std_os  # pylint: disable=os-module-forbidden\n\n        # See related inline comment in certbot.compat.filesystem.check_owner method\n        # that explains why MyPy/PyLint check disable is needed here.\n        uid = std_os.getuid()\n\n        with mock.patch('os.getuid') as mock_uid:\n            mock_uid.return_value = uid + 1\n            assert not filesystem.check_owner(self.probe_path)\n\n    def test_check_permissions(self):\n        assert filesystem.check_permissions(self.probe_path, 0o744) is True\n\n        with mock.patch('certbot.compat.filesystem.check_mode') as mock_mode:\n            mock_mode.return_value = False\n            assert not filesystem.check_permissions(self.probe_path, 0o744)\n\n        with mock.patch('certbot.compat.filesystem.check_owner') as mock_owner:\n            mock_owner.return_value = False\n            assert not filesystem.check_permissions(self.probe_path, 0o744)\n\n    def test_check_min_permissions(self):\n        filesystem.chmod(self.probe_path, 0o744)\n        assert filesystem.has_min_permissions(self.probe_path, 0o744) is True\n\n        filesystem.chmod(self.probe_path, 0o700)\n        assert not filesystem.has_min_permissions(self.probe_path, 0o744)\n\n        filesystem.chmod(self.probe_path, 0o741)\n        assert not filesystem.has_min_permissions(self.probe_path, 0o744)\n\n    def test_is_world_reachable(self):\n        filesystem.chmod(self.probe_path, 0o744)\n        assert filesystem.has_world_permissions(self.probe_path) is True\n\n        filesystem.chmod(self.probe_path, 0o700)\n        assert not filesystem.has_world_permissions(self.probe_path)\n\n\nclass OsReplaceTest(test_util.TempDirTestCase):\n    \"\"\"Test to ensure consistent behavior of rename method\"\"\"\n    def test_os_replace_to_existing_file(self):\n        \"\"\"Ensure that replace will effectively rename src into dst for all platforms.\"\"\"\n        src = os.path.join(self.tempdir, 'src')\n        dst = os.path.join(self.tempdir, 'dst')\n        open(src, 'w').close()\n        open(dst, 'w').close()\n\n        # On Windows, a direct call to os.rename would fail because dst already exists.\n        filesystem.replace(src, dst)\n\n        assert not os.path.exists(src)\n        assert os.path.exists(dst) is True\n\n\nclass RealpathTest(test_util.TempDirTestCase):\n    \"\"\"Tests for realpath method\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.probe_path = _create_probe(self.tempdir)\n\n    def test_symlink_resolution(self):\n        # Remove any symlinks already in probe_path\n        self.probe_path = filesystem.realpath(self.probe_path)\n        # Absolute resolution\n        link_path = os.path.join(self.tempdir, 'link_abs')\n        os.symlink(self.probe_path, link_path)\n\n        assert self.probe_path == filesystem.realpath(self.probe_path)\n        assert self.probe_path == filesystem.realpath(link_path)\n\n        # Relative resolution\n        curdir = os.getcwd()\n        link_path = os.path.join(self.tempdir, 'link_rel')\n        probe_name = os.path.basename(self.probe_path)\n        try:\n            os.chdir(os.path.dirname(self.probe_path))\n            os.symlink(probe_name, link_path)\n\n            assert self.probe_path == filesystem.realpath(probe_name)\n            assert self.probe_path == filesystem.realpath(link_path)\n        finally:\n            os.chdir(curdir)\n\n    def test_symlink_loop_mitigation(self):\n        link1_path = os.path.join(self.tempdir, 'link1')\n        link2_path = os.path.join(self.tempdir, 'link2')\n        link3_path = os.path.join(self.tempdir, 'link3')\n        os.symlink(link1_path, link2_path)\n        os.symlink(link2_path, link3_path)\n        os.symlink(link3_path, link1_path)\n\n        with pytest.raises(RuntimeError, match='link1 is a loop!'):\n            filesystem.realpath(link1_path)\n\n\nclass IsExecutableTest(test_util.TempDirTestCase):\n    \"\"\"Tests for is_executable method\"\"\"\n    def test_not_executable(self):\n        file_path = os.path.join(self.tempdir, \"foo\")\n\n        # On Windows a file created within Certbot will always have all permissions to the\n        # Administrators group set. Since the unit tests are typically executed under elevated\n        # privileges, it means that current user will always have effective execute rights on the\n        # hook script, and so the test will fail. To prevent that and represent a file created\n        # outside Certbot as typically a hook file is, we mock the _generate_dacl function in\n        # certbot.compat.filesystem to give rights only to the current user. This implies removing\n        # all ACEs except the first one from the DACL created by original _generate_dacl function.\n\n        from certbot.compat.filesystem import _generate_dacl\n\n        def _execute_mock(user_sid, mode, mask=None):\n            dacl = _generate_dacl(user_sid, mode, mask)\n            for _ in range(1, dacl.GetAceCount()):\n                dacl.DeleteAce(1)  # DeleteAce dynamically updates the internal index mapping.\n            return dacl\n\n        # create a non-executable file\n        with mock.patch(\"certbot.compat.filesystem._generate_dacl\", side_effect=_execute_mock):\n            os.close(filesystem.open(file_path, os.O_CREAT | os.O_WRONLY, 0o666))\n\n        assert not filesystem.is_executable(file_path)\n\n    @mock.patch(\"certbot.compat.filesystem.os.path.isfile\")\n    @mock.patch(\"certbot.compat.filesystem.os.access\")\n    def test_full_path(self, mock_access, mock_isfile):\n        with _fix_windows_runtime():\n            mock_access.return_value = True\n            mock_isfile.return_value = True\n            assert filesystem.is_executable(\"/path/to/exe\") is True\n\n    @mock.patch(\"certbot.compat.filesystem.os.path.isfile\")\n    @mock.patch(\"certbot.compat.filesystem.os.access\")\n    def test_rel_path(self, mock_access, mock_isfile):\n        with _fix_windows_runtime():\n            mock_access.return_value = True\n            mock_isfile.return_value = True\n            assert filesystem.is_executable(\"exe\") is True\n\n    @mock.patch(\"certbot.compat.filesystem.os.path.isfile\")\n    @mock.patch(\"certbot.compat.filesystem.os.access\")\n    def test_not_found(self, mock_access, mock_isfile):\n        with _fix_windows_runtime():\n            mock_access.return_value = True\n            mock_isfile.return_value = False\n            assert not filesystem.is_executable(\"exe\")\n\n\nclass ReadlinkTest(unittest.TestCase):\n    @unittest.skipUnless(POSIX_MODE, reason='Tests specific to Linux')\n    @mock.patch(\"certbot.compat.filesystem.os.readlink\")\n    def test_path_posix(self, mock_readlink):\n        mock_readlink.return_value = \"/normal/path\"\n        assert filesystem.readlink(\"dummy\") == \"/normal/path\"\n\n    @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows')\n    @mock.patch(\"certbot.compat.filesystem.os.readlink\")\n    def test_normal_path_windows(self, mock_readlink):\n        # test the standard path format\n        mock_readlink.return_value = \"C:\\\\short\\\\path\"\n        assert filesystem.readlink(\"dummy\") == \"C:\\\\short\\\\path\"\n\n        # test the extended form\n        mock_readlink.return_value = \"\\\\\\\\?\\\\C:\\\\short\\\\path\"\n        assert filesystem.readlink(\"dummy\") == \"C:\\\\short\\\\path\"\n\n    @unittest.skipIf(POSIX_MODE, reason='Tests specific to Windows')\n    @mock.patch(\"certbot.compat.filesystem.os.readlink\")\n    def test_extended_path_windows(self, mock_readlink):\n        # Following path is largely over the 260 characters permitted in the normal form.\n        mock_readlink.return_value = \"\\\\\\\\?\\\\C:\\\\long\" + 1000 * \"\\\\path\"\n        with pytest.raises(ValueError):\n            filesystem.readlink(\"dummy\")\n\n@contextlib.contextmanager\ndef _fix_windows_runtime():\n    if os.name != 'nt':\n        yield\n    else:\n        with mock.patch('win32security.GetFileSecurity') as mock_get:\n            dacl_mock = mock_get.return_value.GetSecurityDescriptorDacl\n            mode_mock = dacl_mock.return_value.GetEffectiveRightsFromAcl\n            mode_mock.return_value = ntsecuritycon.FILE_GENERIC_EXECUTE\n            yield\n\n\ndef _get_security_dacl(target):\n    return win32security.GetFileSecurity(target, win32security.DACL_SECURITY_INFORMATION)\n\n\ndef _get_security_owner(target):\n    return win32security.GetFileSecurity(target, win32security.OWNER_SECURITY_INFORMATION)\n\n\ndef _set_owner(target, security_owner, user):\n    security_owner.SetSecurityDescriptorOwner(user, False)\n    win32security.SetFileSecurity(\n        target, win32security.OWNER_SECURITY_INFORMATION, security_owner)\n\n\ndef _create_probe(tempdir, name='probe'):\n    filesystem.chmod(tempdir, 0o744)\n    probe_path = os.path.join(tempdir, name)\n    util.safe_open(probe_path, 'w', chmod=0o744).close()\n    return probe_path\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/compat/misc_test.py",
    "content": "\"\"\"Tests for certbot.compat.misc\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot.compat import os\n\n\nclass ExecuteStatusTest:\n    \"\"\"Tests for certbot.compat.misc.execute_command_status.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.compat.misc import execute_command_status\n        return execute_command_status(*args, **kwargs)\n\n    def _test_common(self, returncode, stdout, stderr):\n        given_command = \"foo\"\n        given_name = \"foo-hook\"\n        with mock.patch(\"certbot.compat.misc.subprocess.run\") as mock_run:\n            mock_run.return_value.stdout = stdout\n            mock_run.return_value.stderr = stderr\n            mock_run.return_value.returncode = returncode\n            with mock.patch(\"certbot.compat.misc.logger\") as mock_logger:\n                assert self._call(given_name, given_command) == (returncode, stderr, stdout)\n\n        executed_command = mock_run.call_args[1].get(\n            \"args\", mock_run.call_args[0][0])\n        if os.name == 'nt':\n            expected_command = ['powershell.exe', '-Command', given_command]\n        else:\n            expected_command = given_command\n        assert executed_command == expected_command\n        assert executed_command == expected_command\n\n        mock_logger.info.assert_any_call(\"Running %s command: %s\",\n                                         given_name, given_command)\n\n    def test_it(self):\n        for returncode in range(0, 2):\n            for stdout in (\"\", \"Hello World!\",):\n                for stderr in (\"\", \"Goodbye Cruel World!\"):\n                    self._test_common(returncode, stdout, stderr)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/compat/os_test.py",
    "content": "\"\"\"Unit test for os module.\"\"\"\nimport sys\n\nimport pytest\n\nfrom certbot.compat import os\n\n\ndef test_forbidden_methods():\n    # Checks for os module\n    for method in ['chmod', 'chown', 'open', 'mkdir', 'makedirs', 'rename',\n                   'replace', 'access', 'stat', 'fstat']:\n        with pytest.raises(RuntimeError):\n            getattr(os, method)()\n    # Checks for os.path module\n    for method in ['realpath']:\n        with pytest.raises(RuntimeError):\n            getattr(os.path, method)()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/configuration_test.py",
    "content": "\"\"\"Tests for certbot.configuration.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot._internal import constants\nfrom certbot.compat import misc\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\n\nclass NamespaceConfigTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot.configuration.NamespaceConfig.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config.foo = 'bar' # pylint: disable=blacklisted-name\n        self.config.server = 'https://acme-server.org:443/new'\n        self.config.https_port = 1234\n        self.config.http01_port = 4321\n\n    def test_init_same_ports(self):\n        self.config.https_port = 4321\n        from certbot.configuration import NamespaceConfig\n        with pytest.raises(errors.Error):\n            NamespaceConfig(self.config.namespace)\n\n    def test_proxy_getattr(self):\n        assert self.config.foo == 'bar'\n        assert self.config.work_dir == os.path.join(self.tempdir, 'work')\n\n    def test_server_path(self):\n        assert ['acme-server.org:443', 'new'] == \\\n                         self.config.server_path.split(os.path.sep)\n\n        self.config.server = ('http://user:pass@acme.server:443'\n                                 '/p/a/t/h;parameters?query#fragment')\n        assert ['user:pass@acme.server:443', 'p', 'a', 't', 'h'] == \\\n                         self.config.server_path.split(os.path.sep)\n\n    @mock.patch('certbot.configuration.constants')\n    def test_dynamic_dirs(self, mock_constants):\n        mock_constants.ACCOUNTS_DIR = 'acc'\n        mock_constants.BACKUP_DIR = 'backups'\n\n        mock_constants.IN_PROGRESS_DIR = '../p'\n        mock_constants.KEY_DIR = 'keys'\n        mock_constants.TEMP_CHECKPOINT_DIR = 't'\n\n        ref_path = misc.underscores_for_unsupported_characters_in_path(\n            'acc/acme-server.org:443/new')\n        assert os.path.normpath(self.config.accounts_dir) == \\\n            os.path.normpath(os.path.join(self.config.config_dir, ref_path))\n        assert os.path.normpath(self.config.backup_dir) == \\\n            os.path.normpath(os.path.join(self.config.work_dir, 'backups'))\n        assert os.path.normpath(self.config.in_progress_dir) == \\\n            os.path.normpath(os.path.join(self.config.work_dir, '../p'))\n        assert os.path.normpath(self.config.temp_checkpoint_dir) == \\\n            os.path.normpath(os.path.join(self.config.work_dir, 't'))\n\n    def test_absolute_paths(self):\n        from certbot.configuration import NamespaceConfig\n\n        config_base = \"foo\"\n        work_base = \"bar\"\n        logs_base = \"baz\"\n        server = \"mock.server\"\n\n        mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',\n                                              'logs_dir', 'http01_port',\n                                              'https_port',\n                                              'domains', 'server'])\n        mock_namespace.config_dir = config_base\n        mock_namespace.work_dir = work_base\n        mock_namespace.logs_dir = logs_base\n        mock_namespace.server = server\n        config = NamespaceConfig(mock_namespace)\n\n        assert os.path.isabs(config.config_dir)\n        assert config.config_dir == \\\n                         os.path.join(os.getcwd(), config_base)\n        assert os.path.isabs(config.work_dir)\n        assert config.work_dir == \\\n                         os.path.join(os.getcwd(), work_base)\n        assert os.path.isabs(config.logs_dir)\n        assert config.logs_dir == \\\n                         os.path.join(os.getcwd(), logs_base)\n        assert os.path.isabs(config.accounts_dir)\n        assert os.path.isabs(config.backup_dir)\n        assert os.path.isabs(config.in_progress_dir)\n        assert os.path.isabs(config.temp_checkpoint_dir)\n\n    @mock.patch('certbot.configuration.constants')\n    def test_renewal_dynamic_dirs(self, mock_constants):\n        mock_constants.ARCHIVE_DIR = 'a'\n        mock_constants.LIVE_DIR = 'l'\n        mock_constants.RENEWAL_CONFIGS_DIR = 'renewal_configs'\n\n        assert self.config.default_archive_dir == os.path.join(self.config.config_dir, 'a')\n        assert self.config.live_dir == os.path.join(self.config.config_dir, 'l')\n        assert self.config.renewal_configs_dir == os.path.join(\n                    self.config.config_dir, 'renewal_configs')\n\n    def test_renewal_absolute_paths(self):\n        from certbot.configuration import NamespaceConfig\n\n        config_base = \"foo\"\n        work_base = \"bar\"\n        logs_base = \"baz\"\n\n        mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',\n                                              'logs_dir', 'http01_port',\n                                              'https_port',\n                                              'domains', 'server'])\n        mock_namespace.config_dir = config_base\n        mock_namespace.work_dir = work_base\n        mock_namespace.logs_dir = logs_base\n        config = NamespaceConfig(mock_namespace)\n\n        assert os.path.isabs(config.default_archive_dir)\n        assert os.path.isabs(config.live_dir)\n        assert os.path.isabs(config.renewal_configs_dir)\n\n    def test_get_and_set_attr(self):\n        self.config.foo = 42\n        assert self.config.namespace.foo == 42\n        self.config.namespace.bar = 1337\n        assert self.config.bar == 1337\n\n    def test_hook_directories(self):\n        assert self.config.renewal_hooks_dir == \\\n                         os.path.join(self.config.config_dir,\n                                      constants.RENEWAL_HOOKS_DIR)\n        assert self.config.renewal_pre_hooks_dir == \\\n                         os.path.join(self.config.renewal_hooks_dir,\n                                      constants.RENEWAL_PRE_HOOKS_DIR)\n        assert self.config.renewal_deploy_hooks_dir == \\\n                         os.path.join(self.config.renewal_hooks_dir,\n                                      constants.RENEWAL_DEPLOY_HOOKS_DIR)\n        assert self.config.renewal_post_hooks_dir == \\\n                         os.path.join(self.config.renewal_hooks_dir,\n                                      constants.RENEWAL_POST_HOOKS_DIR)\n\n    def test_set_by_user_runtime_overrides(self):\n        assert not self.config.set_by_user('something')\n        self.config.something = 'a value'\n        assert self.config.set_by_user('something')\n\n    def test_set_by_user_exception(self):\n        from certbot.configuration import NamespaceConfig\n\n        # a newly created NamespaceConfig has no argument sources dict, so an\n        # exception is raised\n        config = NamespaceConfig(self.config.namespace)\n        with pytest.raises(RuntimeError):\n            config.set_by_user('whatever')\n\n        # now set an argument sources dict\n        config.set_argument_sources({})\n        assert not config.set_by_user('whatever')\n\n    def test_set_by_user_mutables(self):\n        assert not self.config.set_by_user('domains')\n        self.config.domains.append('example.org')\n        assert self.config.set_by_user('domains')\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/conftest.py",
    "content": "from unittest import mock\n\nimport pytest\n\n# This avoids a bug on mac where getfqdn errors after a long timeout.\n# See https://bugs.python.org/issue35164\n# and discussion at https://github.com/certbot/certbot/pull/10408\n@pytest.fixture(autouse=True)\ndef mock_getfqdn():\n    with mock.patch(\"socket.getfqdn\", return_value=\"server_name\") as mocked:\n        yield mocked\n\n@pytest.fixture(autouse=True)\ndef mock_sleep():\n    with mock.patch(\"time.sleep\") as mocked:\n        yield mocked\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/crypto_util_test.py",
    "content": "\"\"\"Tests for certbot.crypto_util.\"\"\"\nimport logging\nimport re\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.serialization import Encoding\n\nfrom acme import crypto_util as acme_crypto_util\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\nRSA512_KEY = test_util.load_vector('rsa512_key.pem')\nRSA512_KEY_PATH = test_util.vector_path('rsa512_key.pem')\nRSA2048_KEY = test_util.load_vector('rsa2048_key.pem')\nRSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')\nCERT_PATH = test_util.vector_path('cert_512.pem')\nCERT = test_util.load_vector('cert_512.pem')\nSS_CERT_PATH = test_util.vector_path('cert_2048.pem')\nSS_CERT = test_util.load_vector('cert_2048.pem')\nP256_KEY = test_util.load_vector('nistp256_key.pem')\nP256_KEY_PATH = test_util.vector_path('nistp256_key.pem')\nP256_CERT_PATH = test_util.vector_path('cert-nosans_nistp256.pem')\nP256_CERT = test_util.load_vector('cert-nosans_nistp256.pem')\n# CERT_LEAF is signed by CERT_ISSUER. CERT_ALT_ISSUER is a cross-sign of CERT_ISSUER.\nCERT_LEAF = test_util.load_vector('cert_leaf.pem')\nCERT_ISSUER = test_util.load_vector('cert_intermediate_1.pem')\nCERT_ALT_ISSUER = test_util.load_vector('cert_intermediate_2.pem')\n\n\nclass GenerateKeyTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.crypto_util.generate_key.\"\"\"\n    def setUp(self):\n        super().setUp()\n\n        self.workdir = os.path.join(self.tempdir, 'workdir')\n        filesystem.mkdir(self.workdir, mode=0o700)\n\n        logging.disable(logging.CRITICAL)\n\n    def tearDown(self):\n        super().tearDown()\n\n        logging.disable(logging.NOTSET)\n\n    @classmethod\n    def _call(cls, key_size, key_dir):\n        from certbot.crypto_util import generate_key\n        return generate_key(key_size, key_dir, 'key-certbot.pem', strict_permissions=True)\n\n    @mock.patch('certbot.crypto_util.make_key')\n    def test_success(self, mock_make):\n        mock_make.return_value = b'key_pem'\n        key = self._call(1024, self.workdir)\n        assert key.pem == b'key_pem'\n        assert 'key-certbot.pem' in key.file\n        assert os.path.exists(os.path.join(self.workdir, key.file))\n\n    @mock.patch('certbot.crypto_util.make_key')\n    def test_key_failure(self, mock_make):\n        mock_make.side_effect = ValueError\n        with pytest.raises(ValueError):\n            self._call(431, self.workdir)\n\n\nclass GenerateCSRTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.crypto_util.generate_csr.\"\"\"\n    @mock.patch('acme.crypto_util.make_csr')\n    @mock.patch('certbot.crypto_util.util.make_or_verify_dir')\n    def test_it(self, unused_mock_verify, mock_csr):\n        from certbot.crypto_util import generate_csr\n\n        mock_csr.return_value = b'csr_pem'\n\n        csr = generate_csr(\n            mock.Mock(pem='dummy_key'), 'example.com', self.tempdir, strict_permissions=True)\n\n        assert csr.data == b'csr_pem'\n        assert 'csr-certbot.pem' in csr.file\n\n\nclass ValidCSRTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.valid_csr.\"\"\"\n\n    @classmethod\n    def _call(cls, csr):\n        from certbot.crypto_util import valid_csr\n        return valid_csr(csr)\n\n    def test_valid_pem_true(self):\n        assert self._call(test_util.load_vector('csr_512.pem'))\n\n    def test_valid_pem_san_true(self):\n        assert self._call(test_util.load_vector('csr-san_512.pem'))\n\n    def test_valid_der_false(self):\n        assert not self._call(test_util.load_vector('csr_512.der'))\n\n    def test_empty_false(self):\n        assert not self._call('')\n\n    def test_random_false(self):\n        assert not self._call('foo bar')\n\n\nclass CSRMatchesPubkeyTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.csr_matches_pubkey.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import csr_matches_pubkey\n        return csr_matches_pubkey(*args, **kwargs)\n\n    def test_valid_true(self):\n        assert self._call(\n            test_util.load_vector('csr_512.pem'), RSA512_KEY)\n\n    def test_invalid_false(self):\n        assert not self._call(\n            test_util.load_vector('csr_512.pem'), P256_KEY)\n\n\nclass ReadCSRFileTest(unittest.TestCase):\n    \"\"\"Tests for certbot.certbot_util.read_csr_file.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import read_csr_file\n        return read_csr_file(*args, **kwargs)\n\n    def test_der_csr(self):\n        csrfile = test_util.vector_path('csr_512.der')\n        data = test_util.load_vector('csr_512.der')\n        data_pem = test_util.load_vector('csr_512.pem')\n\n        assert util.CSR(file=csrfile,\n                      data=data_pem,\n                      form=\"pem\") == \\\n            self._call(csrfile, data)\n\n    def test_pem_csr(self):\n        csrfile = test_util.vector_path('csr_512.pem')\n        data = test_util.load_vector('csr_512.pem')\n\n        assert util.CSR(file=csrfile,\n                      data=data,\n                      form=\"pem\") == \\\n            self._call(csrfile, data)\n\n    def test_bad_csr(self):\n        with pytest.raises(errors.Error):\n            self._call(test_util.vector_path('cert_512.pem'),\n                          test_util.load_vector('cert_512.pem'))\n\n\nclass ImportCSRFileTest(unittest.TestCase):\n    \"\"\"Tests for certbot.certbot_util.import_csr_file.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import import_csr_file\n        with pytest.warns(DeprecationWarning, match='import_csr_file is deprecated'):\n            return import_csr_file(*args, **kwargs)\n\n    def test_der_csr(self):\n        csrfile = test_util.vector_path('csr_512.der')\n        data = test_util.load_vector('csr_512.der')\n        data_pem = test_util.load_vector('csr_512.pem')\n\n        with pytest.warns(DeprecationWarning, match='Format is deprecated'):\n            assert (acme_crypto_util.Format.PEM,\n                util.CSR(file=csrfile,\n                        data=data_pem,\n                        form=\"pem\"),\n                [\"Example.com\"]) == \\\n                self._call(csrfile, data)\n\n    def test_pem_csr(self):\n        csrfile = test_util.vector_path('csr_512.pem')\n        data = test_util.load_vector('csr_512.pem')\n\n        with pytest.warns(DeprecationWarning, match='Format is deprecated'):\n            assert (acme_crypto_util.Format.PEM,\n                util.CSR(file=csrfile,\n                        data=data,\n                        form=\"pem\"),\n                [\"Example.com\"],) == \\\n                self._call(csrfile, data)\n\n    def test_bad_csr(self):\n        with pytest.raises(errors.Error):\n            self._call(test_util.vector_path('cert_512.pem'),\n                          test_util.load_vector('cert_512.pem'))\n\n\nclass MakeKeyTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.make_key.\"\"\"\n\n    def test_rsa(self):  # pylint: disable=no-self-use\n        # RSA Key Type Test\n        from certbot.crypto_util import make_key\n\n        # Do not test larger keys as it takes too long.\n        serialization.load_pem_private_key(make_key(2048), password=None)\n\n    def test_ec(self):  # pylint: disable=no-self-use\n        # ECDSA Key Type Tests\n        from certbot.crypto_util import make_key\n\n        for (name, bits) in [('secp256r1', 256), ('secp384r1', 384), ('secp521r1', 521)]:\n            pkey = serialization.load_pem_private_key(\n                make_key(elliptic_curve=name, key_type='ecdsa'),\n                password=None\n            )\n            assert isinstance(pkey, ec.EllipticCurvePrivateKey)\n            assert pkey.curve.key_size == bits\n\n    def test_bad_key_sizes(self):\n        from certbot.crypto_util import make_key\n\n        # Try a bad key size for RSA and ECDSA\n        with pytest.raises(errors.Error, match='Unsupported RSA key length: 1024'):\n            make_key(bits=1024, key_type='rsa')\n\n    def test_bad_elliptic_curve_name(self):\n        from certbot.crypto_util import make_key\n        with pytest.raises(errors.Error, match='Unsupported elliptic curve: nothere'):\n            make_key(elliptic_curve=\"nothere\", key_type='ecdsa')\n\n    def test_bad_key_type(self):\n        from certbot.crypto_util import make_key\n\n        # Try a bad --key-type\n        with pytest.raises(errors.Error,\n                           match=re.escape('Invalid key_type specified: unf.  Use [rsa|ecdsa]')):\n            make_key(2048, key_type='unf')\n\n    def test_for_pkcs8_format(self):\n        from certbot.crypto_util import make_key\n\n        # PKCS#1 format will instead have text like \"BEGIN RSA PRIVATE KEY\" or \"BEGIN EC PRIVATE\n        # KEY\"\n        assert b\"BEGIN PRIVATE KEY\" in make_key(2048)\n        assert b\"BEGIN PRIVATE KEY\" in make_key(elliptic_curve='secp256r1', key_type='ecdsa')\n\n\nclass VerifyCertSetup(unittest.TestCase):\n    \"\"\"Refactoring for verification tests.\"\"\"\n\n    def setUp(self):\n        self.renewable_cert = mock.MagicMock()\n        self.renewable_cert.cert_path = SS_CERT_PATH\n        self.renewable_cert.chain_path = SS_CERT_PATH\n        self.renewable_cert.key_path = RSA2048_KEY_PATH\n        self.renewable_cert.fullchain_path = test_util.vector_path('cert_fullchain_2048.pem')\n\n        self.bad_renewable_cert = mock.MagicMock()\n        self.bad_renewable_cert.chain_path = SS_CERT_PATH\n        self.bad_renewable_cert.cert_path = SS_CERT_PATH\n        self.bad_renewable_cert.fullchain_path = SS_CERT_PATH\n\n\nclass VerifyRenewableCertTest(VerifyCertSetup):\n    \"\"\"Tests for certbot.crypto_util.verify_renewable_cert.\"\"\"\n\n    def _call(self, renewable_cert):\n        from certbot.crypto_util import verify_renewable_cert\n        return verify_renewable_cert(renewable_cert)\n\n    def test_verify_renewable_cert(self):\n        assert self._call(self.renewable_cert) is None\n\n    @mock.patch('certbot.crypto_util.verify_renewable_cert_sig', side_effect=errors.Error(\"\"))\n    def test_verify_renewable_cert_failure(self, unused_verify_renewable_cert_sign):\n        with pytest.raises(errors.Error):\n            self._call(self.bad_renewable_cert)\n\n\nclass VerifyRenewableCertSigTest(VerifyCertSetup):\n    \"\"\"Tests for certbot.crypto_util.verify_renewable_cert.\"\"\"\n\n    def _call(self, renewable_cert):\n        from certbot.crypto_util import verify_renewable_cert_sig\n        return verify_renewable_cert_sig(renewable_cert)\n\n    def test_cert_sig_match(self):\n        assert self._call(self.renewable_cert) is None\n\n    def test_cert_sig_match_ec(self):\n        renewable_cert = mock.MagicMock()\n        renewable_cert.cert_path = P256_CERT_PATH\n        renewable_cert.chain_path = P256_CERT_PATH\n        renewable_cert.key_path = P256_KEY\n        assert self._call(renewable_cert) is None\n\n    def test_cert_sig_mismatch(self):\n        self.bad_renewable_cert.cert_path = test_util.vector_path('cert_512_bad.pem')\n        with pytest.raises(errors.Error):\n            self._call(self.bad_renewable_cert)\n\n\nclass VerifyFullchainTest(VerifyCertSetup):\n    \"\"\"Tests for certbot.crypto_util.verify_fullchain.\"\"\"\n\n    def _call(self, renewable_cert):\n        from certbot.crypto_util import verify_fullchain\n        return verify_fullchain(renewable_cert)\n\n    def test_fullchain_matches(self):\n        assert self._call(self.renewable_cert) is None\n\n    def test_fullchain_mismatch(self):\n        with pytest.raises(errors.Error):\n            self._call(self.bad_renewable_cert)\n\n    def test_fullchain_ioerror(self):\n        self.bad_renewable_cert.chain = \"dog\"\n        with pytest.raises(errors.Error):\n            self._call(self.bad_renewable_cert)\n\n\nclass VerifyCertMatchesPrivKeyTest(VerifyCertSetup):\n    \"\"\"Tests for certbot.crypto_util.verify_cert_matches_priv_key.\"\"\"\n\n    def _call(self, renewable_cert):\n        from certbot.crypto_util import verify_cert_matches_priv_key\n        return verify_cert_matches_priv_key(renewable_cert.cert, renewable_cert.privkey)\n\n    def test_cert_priv_key_match(self):\n        self.renewable_cert.cert = SS_CERT_PATH\n        self.renewable_cert.privkey = RSA2048_KEY_PATH\n        assert self._call(self.renewable_cert) is None\n\n    def test_cert_priv_key_mismatch(self):\n        self.bad_renewable_cert.privkey = P256_KEY_PATH\n        self.bad_renewable_cert.cert = SS_CERT_PATH\n\n        with pytest.raises(errors.Error):\n            self._call(self.bad_renewable_cert)\n\n\nclass ValidPrivkeyTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.valid_privkey.\"\"\"\n\n    @classmethod\n    def _call(cls, privkey):\n        from certbot.crypto_util import valid_privkey\n        return valid_privkey(privkey)\n\n    def test_valid_true(self):\n        assert self._call(RSA512_KEY)\n\n    def test_empty_false(self):\n        assert not self._call('')\n\n    def test_random_false(self):\n        assert not self._call('foo bar')\n\n\nclass GetSANsFromCertTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.get_sans_from_cert.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import get_sans_from_cert\n        with pytest.warns(DeprecationWarning, match='get_sans_from_cert is deprecated'):\n            return get_sans_from_cert(*args, **kwargs)\n\n    def test_single(self):\n        assert [] == self._call(test_util.load_vector('cert_512.pem'))\n\n    def test_san(self):\n        assert ['example.com', 'www.example.com'] == \\\n            self._call(test_util.load_vector('cert-san_512.pem'))\n\n\nclass GetNamesFromCertTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.get_names_from_cert.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import get_names_from_cert\n        with pytest.warns(DeprecationWarning, match='get_names_from_cert is deprecated'):\n            return get_names_from_cert(*args, **kwargs)\n\n    def test_single(self):\n        assert ['example.com'] == \\\n            self._call(test_util.load_vector('cert_512.pem'))\n\n    def test_san(self):\n        assert ['example.com', 'www.example.com'] == \\\n            self._call(test_util.load_vector('cert-san_512.pem'))\n\n    def test_common_name_sans_order(self):\n        # Tests that the common name comes first\n        # followed by the SANS in alphabetical order\n        assert ['example.com'] + ['{0}.example.com'.format(c) for c in 'abcd'] == \\\n            self._call(test_util.load_vector('cert-5sans_512.pem'))\n\n    def test_parse_non_cert(self):\n        with pytest.raises(ValueError):\n            self._call(b\"hello there\")\n\n\nclass GetNamesFromReqTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.get_names_from_req.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.crypto_util import get_names_from_req\n        with pytest.warns(DeprecationWarning, match='get_names_from_req is deprecated'):\n            return get_names_from_req(*args, **kwargs)\n\n    def test_nonames(self):\n        assert [] == \\\n            self._call(test_util.load_vector('csr-nonames_512.pem'))\n\n    def test_nosans(self):\n        assert ['example.com'] == \\\n            self._call(test_util.load_vector('csr-nosans_512.pem'))\n\n    def test_sans(self):\n        assert ['example.com', 'example.org', 'example.net', 'example.info',\n             'subdomain.example.com', 'other.subdomain.example.com'] == \\\n            self._call(test_util.load_vector('csr-6sans_512.pem'))\n\n    def test_der(self):\n        with pytest.warns(DeprecationWarning, match='Format is deprecated'):\n            assert ['Example.com'] == \\\n                self._call(test_util.load_vector('csr_512.der'), typ=acme_crypto_util.Format.DER)\n\n\nclass NotBeforeTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.notBefore\"\"\"\n\n    def test_notBefore(self):\n        from certbot.crypto_util import notBefore\n        assert notBefore(CERT_PATH).isoformat() == \\\n                         '2014-12-11T22:34:45+00:00'\n\n\nclass NotAfterTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.notAfter\"\"\"\n\n    def test_notAfter(self):\n        from certbot.crypto_util import notAfter\n        assert notAfter(CERT_PATH).isoformat() == \\\n                         '2014-12-18T22:34:45+00:00'\n\n\nclass Sha256sumTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.notAfter\"\"\"\n    def test_sha256sum(self):\n        from certbot.crypto_util import sha256sum\n        assert sha256sum(CERT_PATH) == \\\n            '914ffed8daf9e2c99d90ac95c77d54f32cbd556672facac380f0c063498df84e'\n\n\nclass CertAndChainFromFullchainTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.cert_and_chain_from_fullchain\"\"\"\n\n    def _parse_and_reencode_pem(self, cert_pem:str)->str:\n        cert = x509.load_pem_x509_certificate(cert_pem.encode())\n        return cert.public_bytes(Encoding.PEM).decode()\n\n    def test_cert_and_chain_from_fullchain(self):\n        cert_pem: str = CERT.decode()\n        chain_pem: str = cert_pem + SS_CERT.decode()\n        fullchain_pem: str = cert_pem + chain_pem\n        spacey_fullchain_pem: str = cert_pem + u'\\n' + chain_pem\n        crlf_fullchain_pem: str = fullchain_pem.replace(u'\\n', u'\\r\\n')\n\n        # In the ACME v1 code path, the fullchain is constructed by loading cert+chain DERs\n        # and using OpenSSL to dump them, so here we confirm that cryptography is producing certs\n        # that will be parseable by cert_and_chain_from_fullchain.\n        acmev1_fullchain_pem = self._parse_and_reencode_pem(cert_pem) + \\\n            self._parse_and_reencode_pem(cert_pem) + self._parse_and_reencode_pem(SS_CERT.decode())\n\n        from certbot.crypto_util import cert_and_chain_from_fullchain\n        for fullchain in (fullchain_pem, spacey_fullchain_pem, crlf_fullchain_pem,\n                          acmev1_fullchain_pem):\n            cert_out, chain_out = cert_and_chain_from_fullchain(fullchain)\n            assert cert_out == cert_pem\n            assert chain_out == chain_pem\n\n        with pytest.raises(errors.Error):\n            cert_and_chain_from_fullchain(cert_pem)\n\n\nclass FindChainWithIssuerTest(unittest.TestCase):\n    \"\"\"Tests for certbot.crypto_util.find_chain_with_issuer\"\"\"\n\n    @classmethod\n    def _call(cls, fullchains, issuer_cn, **kwargs):\n        from certbot.crypto_util import find_chain_with_issuer\n        return find_chain_with_issuer(fullchains, issuer_cn, kwargs)\n\n    def _all_fullchains(self):\n        return [CERT_LEAF.decode() + CERT_ISSUER.decode(),\n                CERT_LEAF.decode() + CERT_ALT_ISSUER.decode()]\n\n    def test_positive_match(self):\n        \"\"\"Correctly pick the chain based on the root's CN\"\"\"\n        fullchains = self._all_fullchains()\n        matched = self._call(fullchains, \"Pebble Root CA 0cc6f0\")\n        assert matched == fullchains[1]\n\n    @mock.patch('certbot.crypto_util.logger.info')\n    def test_intermediate_match(self, mock_info):\n        \"\"\"Don't pick a chain where only an intermediate matches\"\"\"\n        fullchains = self._all_fullchains()\n        # Make the second chain actually only contain \"Pebble Root CA 0cc6f0\"\n        # as an intermediate, not as the root. This wouldn't be a valid chain\n        # (the CERT_ISSUER cert didn't issue the CERT_ALT_ISSUER cert), but the\n        # function under test here doesn't care about that.\n        fullchains[1] = fullchains[1] + CERT_ISSUER.decode()\n        matched = self._call(fullchains, \"Pebble Root CA 0cc6f0\")\n        assert matched == fullchains[0]\n        mock_info.assert_not_called()\n\n    @mock.patch('certbot.crypto_util.logger.info')\n    def test_no_match(self, mock_info):\n        fullchains = self._all_fullchains()\n        matched = self._call(fullchains, \"non-existent issuer\")\n        assert matched == fullchains[0]\n        mock_info.assert_not_called()\n\n    @mock.patch('certbot.crypto_util.logger.warning')\n    def test_warning_on_no_match(self, mock_warning):\n        fullchains = self._all_fullchains()\n        matched = self._call(fullchains, \"non-existent issuer\",\n                             warn_on_no_match=True)\n        assert matched == fullchains[0]\n        mock_warning.assert_called_once_with(\"Certbot has been configured to prefer \"\n            \"certificate chains with issuer '%s', but no chain from the CA matched \"\n            \"this issuer. Using the default certificate chain instead.\",\n            \"non-existent issuer\")\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/__init__.py",
    "content": "\"\"\"Certbot Display Tests\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/completer_test.py",
    "content": "\"\"\"Test certbot._internal.display.completer.\"\"\"\nfrom importlib import reload as reload_module\nimport string\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\ntry:\n    import readline  # pylint: disable=import-error\nexcept ImportError:\n    import certbot._internal.display.dummy_readline as readline  # type: ignore\n\n\n\nclass CompleterTest(test_util.TempDirTestCase):\n    \"\"\"Test certbot._internal.display.completer.Completer.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        # directories must end with os.sep for completer to\n        # search inside the directory for possible completions\n        if self.tempdir[-1] != os.sep:\n            self.tempdir += os.sep\n\n        self.paths: list[str] = []\n        # create some files and directories in temp_dir\n        for c in string.ascii_lowercase:\n            path = os.path.join(self.tempdir, c)\n            self.paths.append(path)\n            if ord(c) % 2:\n                filesystem.mkdir(path)\n            else:\n                with open(path, 'w'):\n                    pass\n\n    def test_complete(self):\n        from certbot._internal.display import completer\n        my_completer = completer.Completer()\n        num_paths = len(self.paths)\n\n        for i in range(num_paths):\n            completion = my_completer.complete(self.tempdir, i)\n            assert completion in self.paths\n            self.paths.remove(completion)\n\n        assert len(self.paths) == 0\n        completion = my_completer.complete(self.tempdir, num_paths)\n        assert completion is None\n\n    @unittest.skipIf('readline' not in sys.modules,\n                     reason='Not relevant if readline is not available.')\n    def test_import_error(self):\n        original_readline = sys.modules['readline']\n        sys.modules['readline'] = None\n\n        self.test_context_manager_with_unmocked_readline()\n\n        sys.modules['readline'] = original_readline\n\n    def test_context_manager_with_unmocked_readline(self):\n        from certbot._internal.display import completer\n        reload_module(completer)\n\n        original_completer = readline.get_completer()\n        original_delims = readline.get_completer_delims()\n\n        with completer.Completer():\n            pass\n\n        assert readline.get_completer() == original_completer\n        assert readline.get_completer_delims() == original_delims\n\n    @mock.patch('certbot._internal.display.completer.readline', autospec=True)\n    def test_context_manager_libedit(self, mock_readline):\n        mock_readline.__doc__ = 'libedit'\n        self._test_context_manager_with_mock_readline(mock_readline)\n\n    @mock.patch('certbot._internal.display.completer.readline', autospec=True)\n    def test_context_manager_readline(self, mock_readline):\n        mock_readline.__doc__ = 'GNU readline'\n        self._test_context_manager_with_mock_readline(mock_readline)\n\n    def _test_context_manager_with_mock_readline(self, mock_readline):\n        from certbot._internal.display import completer\n\n        mock_readline.parse_and_bind.side_effect = enable_tab_completion\n\n        with completer.Completer():\n            pass\n\n        assert mock_readline.parse_and_bind.called is True\n\n\ndef enable_tab_completion(unused_command):\n    \"\"\"Enables readline tab completion using the system specific syntax.\"\"\"\n    libedit = readline.__doc__ is not None and 'libedit' in readline.__doc__\n    command = 'bind ^I rl_complete' if libedit else 'tab: complete'\n    readline.parse_and_bind(command)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/internal_util_test.py",
    "content": "\"\"\"Test :mod:`certbot._internal.display.util`.\"\"\"\nimport io\nimport socket\nimport sys\nimport tempfile\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import messages as acme_messages\nfrom certbot import errors\n\n\nclass WrapLinesTest(unittest.TestCase):\n    def test_wrap_lines(self):\n        from certbot._internal.display.util import wrap_lines\n        msg = (\"This is just a weak test{0}\"\n               \"This function is only meant to be for easy viewing{0}\"\n               \"Test a really really really really really really really really \"\n               \"really really really really long line...\".format('\\n'))\n        text = wrap_lines(msg)\n\n        assert text.count('\\n') == 3\n\n\nclass PlaceParensTest(unittest.TestCase):\n    @classmethod\n    def _call(cls, label):\n        from certbot._internal.display.util import parens_around_char\n        return parens_around_char(label)\n\n    def test_single_letter(self):\n        assert \"(a)\" == self._call(\"a\")\n\n    def test_multiple(self):\n        assert \"(L)abel\" == self._call(\"Label\")\n        assert \"(y)es please\" == self._call(\"yes please\")\n\n\nclass InputWithTimeoutTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.display.util.input_with_timeout.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.display.util import input_with_timeout\n        return input_with_timeout(*args, **kwargs)\n\n    def test_eof(self):\n        with tempfile.TemporaryFile(\"r+\") as f:\n            with mock.patch(\"certbot._internal.display.util.sys.stdin\", new=f):\n                with pytest.raises(EOFError):\n                    self._call()\n\n    def test_input(self, prompt=None):\n        expected = \"foo bar\"\n        stdin = io.StringIO(expected + \"\\n\")\n        with mock.patch(\"certbot.compat.misc.select.select\") as mock_select:\n            mock_select.return_value = ([stdin], [], [],)\n            assert self._call(prompt) == expected\n\n    @mock.patch(\"certbot._internal.display.util.sys.stdout\")\n    def test_input_with_prompt(self, mock_stdout):\n        prompt = \"test prompt: \"\n        self.test_input(prompt)\n        mock_stdout.write.assert_called_once_with(prompt)\n        mock_stdout.flush.assert_called_once_with()\n\n    def test_timeout(self):\n        stdin = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n        stdin.bind(('', 0))\n        stdin.listen(1)\n        with mock.patch(\"certbot._internal.display.util.sys.stdin\", stdin):\n            with pytest.raises(errors.Error):\n                self._call(timeout=0.001)\n        stdin.close()\n\n\nclass SeparateListInputTest(unittest.TestCase):\n    \"\"\"Test Module functions.\"\"\"\n    def setUp(self):\n        self.exp = [\"a\", \"b\", \"c\", \"test\"]\n\n    @classmethod\n    def _call(cls, input_):\n        from certbot._internal.display.util import separate_list_input\n        return separate_list_input(input_)\n\n    def test_commas(self):\n        assert self._call(\"a,b,c,test\") == self.exp\n\n    def test_spaces(self):\n        assert self._call(\"a b c test\") == self.exp\n\n    def test_both(self):\n        assert self._call(\"a, b, c, test\") == self.exp\n\n    def test_mess(self):\n        actual = [\n            self._call(\"  a , b    c \\t test\"),\n            self._call(\",a, ,, , b c  test  \"),\n            self._call(\",,,,, , a b,,, , c,test\"),\n        ]\n\n        for act in actual:\n            assert act == self.exp\n\n\nclass SummarizeSANsTest(unittest.TestCase):\n    @classmethod\n    def _call(cls, domains):\n        from certbot._internal.display.util import summarize_sans\n        return summarize_sans(domains)\n\n    def test_single_domain(self):\n        assert \"example.com\" == self._call([\"example.com\"])\n\n    def test_two_domains(self):\n        assert \"example.com and example.org\" == \\\n                         self._call([\"example.com\", \"example.org\"])\n\n    def test_many_domains(self):\n        assert \"example.com and 2 more\" == \\\n                         self._call([\"example.com\", \"example.org\", \"a.example.com\"])\n\n    def test_empty_domains(self):\n        assert \"\" == self._call([])\n\n\nclass DescribeACMEErrorTest(unittest.TestCase):\n    @classmethod\n    def _call(cls, typ: str = \"urn:ietf:params:acme:error:badCSR\",\n              title: str = \"Unacceptable CSR\",\n              detail: str = \"CSR contained unknown extensions\"):\n        from certbot._internal.display.util import describe_acme_error\n        return describe_acme_error(\n            acme_messages.Error(typ=typ, title=title, detail=detail))\n\n    def test_title_and_detail(self):\n        assert \"Unacceptable CSR :: CSR contained unknown extensions\" == self._call()\n\n    def test_detail(self):\n        assert \"CSR contained unknown extensions\" == self._call(title=None)\n\n    def test_description(self):\n        assert acme_messages.ERROR_CODES[\"badCSR\"] == self._call(title=None, detail=None)\n\n    def test_unknown_type(self):\n        assert \"urn:ietf:params:acme:error:unknownErrorType\" == \\\n            self._call(typ=\"urn:ietf:params:acme:error:unknownErrorType\", title=None, detail=None)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/obj_test.py",
    "content": "\"\"\"Test :mod:`certbot._internal.display.obj`.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.display import util as display_util\n\nCHOICES = [(\"First\", \"Description1\"), (\"Second\", \"Description2\")]\nTAGS = [\"tag1\", \"tag2\", \"tag3\"]\n\n\nclass FileOutputDisplayTest(unittest.TestCase):\n    \"\"\"Test stdout display.\n\n    Most of this class has to deal with visual output.  In order to test how the\n    functions look to a user, uncomment the test_visual function.\n\n    \"\"\"\n    def setUp(self):\n        super().setUp()\n        self.mock_stdout = mock.MagicMock()\n        self.displayer = display_obj.FileDisplay(self.mock_stdout, False)\n\n    @mock.patch(\"certbot._internal.display.obj.logger\")\n    def test_notification_no_pause(self, mock_logger):\n        self.displayer.notification(\"message\", False)\n        string = self.mock_stdout.write.call_args[0][0]\n\n        assert \"message\" in string\n        mock_logger.debug.assert_called_with(\"Notifying user: %s\", \"message\")\n\n    def test_notification_pause(self):\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        with mock.patch(input_with_timeout, return_value=\"enter\"):\n            self.displayer.notification(\"message\", force_interactive=True)\n\n        assert \"message\" in self.mock_stdout.write.call_args[0][0]\n\n    def test_notification_noninteractive(self):\n        self._force_noninteractive(self.displayer.notification, \"message\")\n        string = self.mock_stdout.write.call_args[0][0]\n        assert \"message\" in string\n\n    def test_notification_noninteractive2(self):\n        # The main purpose of this test is to make sure we only call\n        # logger.warning once which _force_noninteractive checks internally\n        self._force_noninteractive(self.displayer.notification, \"message\")\n        string = self.mock_stdout.write.call_args[0][0]\n        assert \"message\" in string\n\n        assert self.displayer.skipped_interaction\n\n        self._force_noninteractive(self.displayer.notification, \"message2\")\n        string = self.mock_stdout.write.call_args[0][0]\n        assert \"message2\" in string\n\n    def test_notification_decoration(self):\n        from certbot.compat import os\n        self.displayer.notification(\"message\", pause=False, decorate=False)\n        string = self.mock_stdout.write.call_args[0][0]\n        assert string == \"message\" + os.linesep\n\n        self.displayer.notification(\"message2\", pause=False)\n        string = self.mock_stdout.write.call_args[0][0]\n        assert \"- - - \" in string\n        assert \"message2\" + os.linesep in string\n\n    @mock.patch(\"certbot._internal.display.obj.\"\n                \"FileDisplay._get_valid_int_ans\")\n    def test_menu(self, mock_ans):\n        mock_ans.return_value = (display_util.OK, 1)\n        ret = self.displayer.menu(\"message\", CHOICES, force_interactive=True)\n        assert ret == (display_util.OK, 0)\n\n    def test_menu_noninteractive(self):\n        default = 0\n        result = self._force_noninteractive(\n            self.displayer.menu, \"msg\", CHOICES, default=default)\n        assert result == (display_util.OK, default)\n\n    def test_input_cancel(self):\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        with mock.patch(input_with_timeout, return_value=\"c\"):\n            code, _ = self.displayer.input(\"message\", force_interactive=True)\n\n        assert code, display_util.CANCEL\n\n    def test_input_normal(self):\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        with mock.patch(input_with_timeout, return_value=\"domain.com\"):\n            code, input_ = self.displayer.input(\"message\", force_interactive=True)\n\n        assert code == display_util.OK\n        assert input_ == \"domain.com\"\n\n    def test_input_noninteractive(self):\n        default = \"foo\"\n        code, input_ = self._force_noninteractive(\n            self.displayer.input, \"message\", default=default)\n\n        assert code == display_util.OK\n        assert input_ == default\n\n    def test_input_assertion_fail(self):\n        # If the call to util.assert_valid_call is commented out, an\n        # error.Error is raised, otherwise, an AssertionError is raised.\n        with pytest.raises(Exception):\n            self._force_noninteractive(self.displayer.input, \"message\", cli_flag=\"--flag\")\n\n    def test_input_assertion_fail2(self):\n        with mock.patch(\"certbot.display.util.assert_valid_call\"):\n            with pytest.raises(errors.Error):\n                self._force_noninteractive(self.displayer.input, \"msg\", cli_flag=\"--flag\")\n\n    def test_yesno(self):\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        with mock.patch(input_with_timeout, return_value=\"Yes\"):\n            assert self.displayer.yesno(\n                \"message\", force_interactive=True)\n        with mock.patch(input_with_timeout, return_value=\"y\"):\n            assert self.displayer.yesno(\n                \"message\", force_interactive=True)\n        with mock.patch(input_with_timeout, side_effect=[\"maybe\", \"y\"]):\n            assert self.displayer.yesno(\n                \"message\", force_interactive=True)\n        with mock.patch(input_with_timeout, return_value=\"No\"):\n            assert not self.displayer.yesno(\n                \"message\", force_interactive=True)\n        with mock.patch(input_with_timeout, side_effect=[\"cancel\", \"n\"]):\n            assert not self.displayer.yesno(\n                \"message\", force_interactive=True)\n\n        with mock.patch(input_with_timeout, return_value=\"a\"):\n            assert self.displayer.yesno(\n                \"msg\", yes_label=\"Agree\", force_interactive=True)\n\n    def test_yesno_noninteractive(self):\n        assert self._force_noninteractive(\n            self.displayer.yesno, \"message\", default=True)\n\n    @mock.patch(\"certbot._internal.display.util.input_with_timeout\")\n    def test_checklist_valid(self, mock_input):\n        mock_input.return_value = \"2 1\"\n        code, tag_list = self.displayer.checklist(\n            \"msg\", TAGS, force_interactive=True)\n        assert (code, set(tag_list)) == (display_util.OK, {\"tag1\", \"tag2\"})\n\n    @mock.patch(\"certbot._internal.display.util.input_with_timeout\")\n    def test_checklist_empty(self, mock_input):\n        mock_input.return_value = \"\"\n        code, tag_list = self.displayer.checklist(\"msg\", TAGS, force_interactive=True)\n        assert (code, set(tag_list)) == (display_util.OK, {\"tag1\", \"tag2\", \"tag3\"})\n\n    @mock.patch(\"certbot._internal.display.util.input_with_timeout\")\n    def test_checklist_miss_valid(self, mock_input):\n        mock_input.side_effect = [\"10\", \"tag1 please\", \"1\"]\n\n        ret = self.displayer.checklist(\"msg\", TAGS, force_interactive=True)\n        assert ret == (display_util.OK, [\"tag1\"])\n\n    @mock.patch(\"certbot._internal.display.util.input_with_timeout\")\n    def test_checklist_miss_quit(self, mock_input):\n        mock_input.side_effect = [\"10\", \"c\"]\n\n        ret = self.displayer.checklist(\"msg\", TAGS, force_interactive=True)\n        assert ret == (display_util.CANCEL, [])\n\n    def test_checklist_noninteractive(self):\n        default = TAGS\n        code, input_ = self._force_noninteractive(\n            self.displayer.checklist, \"msg\", TAGS, default=default)\n\n        assert code == display_util.OK\n        assert input_ == default\n\n    def test_scrub_checklist_input_valid(self):\n        # pylint: disable=protected-access\n        indices = [\n            [\"1\"],\n            [\"1\", \"2\", \"1\"],\n            [\"2\", \"3\"],\n        ]\n        exp = [\n            {\"tag1\"},\n            {\"tag1\", \"tag2\"},\n            {\"tag2\", \"tag3\"},\n        ]\n        for i, list_ in enumerate(indices):\n            set_tags = set(\n                self.displayer._scrub_checklist_input(list_, TAGS))\n            assert set_tags == exp[i]\n\n    def test_scrub_checklist_maintain_indices_order(self):\n        # pylint: disable=protected-access\n        source_tags = [\"T1\", \"T2\", \"T3\", \"T4\", \"T5\", \"T6\", \"T7\", \"T8\", \"T9\"]\n        indices = [\n            [\"4\", \"9\"],\n            [\"9\", \"4\"],\n            [\"4\", \"9\", \"4\"],\n            [\"9\", \"4\", \"9\"],\n        ]\n        exp = [\n            [\"T4\", \"T9\"],\n            [\"T9\", \"T4\"],\n            [\"T4\", \"T9\"],\n            [\"T9\", \"T4\"],\n        ]\n        for i, list_ in enumerate(indices):\n            tags = self.displayer._scrub_checklist_input(list_, source_tags)\n            assert tags == exp[i]\n\n    @mock.patch(\"certbot._internal.display.util.input_with_timeout\")\n    def test_directory_select(self, mock_input):\n        args = [\"msg\", \"/var/www/html\", \"--flag\", True]\n        user_input = \"/var/www/html\"\n        mock_input.return_value = user_input\n\n        returned = self.displayer.directory_select(*args)\n        assert returned == (display_util.OK, user_input)\n\n    def test_directory_select_noninteractive(self):\n        default = \"/var/www/html\"\n        code, input_ = self._force_noninteractive(\n            self.displayer.directory_select, \"msg\", default=default)\n\n        assert code == display_util.OK\n        assert input_ == default\n\n    def _force_noninteractive(self, func, *args, **kwargs):\n        skipped_interaction = self.displayer.skipped_interaction\n\n        with mock.patch(\"certbot._internal.display.obj.sys.stdin\") as mock_stdin:\n            mock_stdin.isatty.return_value = False\n            with mock.patch(\"certbot._internal.display.obj.logger\") as mock_logger:\n                result = func(*args, **kwargs)\n\n        if skipped_interaction:\n            assert mock_logger.warning.called is False\n        else:\n            assert mock_logger.warning.call_count == 1\n\n        return result\n\n    def test_scrub_checklist_input_invalid(self):\n        # pylint: disable=protected-access\n        indices = [\n            [\"0\"],\n            [\"4\"],\n            [\"tag1\"],\n            [\"1\", \"tag1\"],\n            [\"2\", \"o\"]\n        ]\n        for list_ in indices:\n            assert self.displayer._scrub_checklist_input(list_, TAGS) == []\n\n    def test_print_menu(self):\n        # pylint: disable=protected-access\n        # This is purely cosmetic... just make sure there aren't any exceptions\n        self.displayer._print_menu(\"msg\", CHOICES)\n        self.displayer._print_menu(\"msg\", TAGS)\n\n    def test_get_valid_int_ans_valid(self):\n        # pylint: disable=protected-access\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        with mock.patch(input_with_timeout, return_value=\"1\"):\n            assert self.displayer._get_valid_int_ans(1) == (display_util.OK, 1)\n        ans = \"2\"\n        with mock.patch(input_with_timeout, return_value=ans):\n            assert self.displayer._get_valid_int_ans(3) == \\\n                (display_util.OK, int(ans))\n\n    def test_get_valid_int_ans_invalid(self):\n        # pylint: disable=protected-access\n        answers = [\n            [\"0\", \"c\"],\n            [\"4\", \"one\", \"C\"],\n            [\"c\"],\n        ]\n        input_with_timeout = \"certbot._internal.display.util.input_with_timeout\"\n        for ans in answers:\n            with mock.patch(input_with_timeout, side_effect=ans):\n                assert self.displayer._get_valid_int_ans(3) == \\\n                    (display_util.CANCEL, -1)\n\n\nclass NoninteractiveDisplayTest(unittest.TestCase):\n    \"\"\"Test non-interactive display. These tests are pretty easy!\"\"\"\n    def setUp(self):\n        self.mock_stdout = mock.MagicMock()\n        self.displayer = display_obj.NoninteractiveDisplay(self.mock_stdout)\n\n    @mock.patch(\"certbot._internal.display.obj.logger\")\n    def test_notification_no_pause(self, mock_logger):\n        self.displayer.notification(\"message\", 10)\n        string = self.mock_stdout.write.call_args[0][0]\n\n        assert \"message\" in string\n        mock_logger.debug.assert_called_with(\"Notifying user: %s\", \"message\")\n\n    def test_notification_decoration(self):\n        from certbot.compat import os\n        self.displayer.notification(\"message\", pause=False, decorate=False)\n        string = self.mock_stdout.write.call_args[0][0]\n        assert string == \"message\" + os.linesep\n\n        self.displayer.notification(\"message2\", pause=False)\n        string = self.mock_stdout.write.call_args[0][0]\n        assert \"- - - \" in string\n        assert \"message2\" + os.linesep in string\n\n    def test_input(self):\n        d = \"an incomputable value\"\n        ret = self.displayer.input(\"message\", default=d)\n        assert ret == (display_util.OK, d)\n        with pytest.raises(errors.MissingCommandlineFlag):\n            self.displayer.input(\"message\")\n\n    def test_menu(self):\n        ret = self.displayer.menu(\"message\", CHOICES, default=1)\n        assert ret == (display_util.OK, 1)\n        with pytest.raises(errors.MissingCommandlineFlag):\n            self.displayer.menu(\"message\", CHOICES)\n\n    def test_yesno(self):\n        d = False\n        ret = self.displayer.yesno(\"message\", default=d)\n        assert ret == d\n        with pytest.raises(errors.MissingCommandlineFlag):\n            self.displayer.yesno(\"message\")\n\n    def test_checklist(self):\n        d = [1, 3]\n        ret = self.displayer.checklist(\"message\", TAGS, default=d)\n        assert ret == (display_util.OK, d)\n        with pytest.raises(errors.MissingCommandlineFlag):\n            self.displayer.checklist(\"message\", TAGS)\n\n    def test_directory_select(self):\n        default = \"/var/www/html\"\n        expected = (display_util.OK, default)\n        actual = self.displayer.directory_select(\"msg\", default)\n        assert expected == actual\n\n        with pytest.raises(errors.MissingCommandlineFlag):\n            self.displayer.directory_select(\"msg\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/ops_test.py",
    "content": "# coding=utf-8\n\"\"\"Test certbot.display.ops.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import messages\nfrom certbot import errors\nfrom certbot._internal import account\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import ops\nfrom certbot.display import util as display_util\nimport certbot.tests.util as test_util\n\nKEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass GetEmailTest(unittest.TestCase):\n    \"\"\"Tests for certbot.display.ops.get_email.\"\"\"\n\n    @classmethod\n    def _call(cls, **kwargs):\n        from certbot.display.ops import get_email\n        return get_email(**kwargs)\n\n    @test_util.patch_display_util()\n    def test_cancel_none(self, mock_get_utility):\n        mock_input = mock_get_utility().input\n        mock_input.return_value = (display_util.CANCEL, \"foo@bar.baz\")\n        with pytest.raises(errors.Error):\n            self._call()\n        with pytest.raises(errors.Error):\n            self._call()\n\n    @test_util.patch_display_util()\n    def test_ok_safe(self, mock_get_utility):\n        mock_input = mock_get_utility().input\n        mock_input.return_value = (display_util.OK, \"foo@bar.baz\")\n        with mock.patch(\"certbot.display.ops.util.safe_email\") as mock_safe_email:\n            mock_safe_email.return_value = True\n            assert self._call() == \"foo@bar.baz\"\n\n    @test_util.patch_display_util()\n    def test_ok_not_safe(self, mock_get_utility):\n        mock_input = mock_get_utility().input\n        mock_input.return_value = (display_util.OK, \"foo@bar.baz\")\n        with mock.patch(\"certbot.display.ops.util.safe_email\") as mock_safe_email:\n            mock_safe_email.side_effect = [False, True]\n            assert self._call() == \"foo@bar.baz\"\n\n    @test_util.patch_display_util()\n    def test_invalid_flag(self, mock_get_utility):\n        invalid_txt = \"The server reported a problem\"\n        mock_input = mock_get_utility().input\n        mock_input.return_value = (display_util.OK, \"foo@bar.baz\")\n        with mock.patch(\"certbot.display.ops.util.safe_email\") as mock_safe_email:\n            mock_safe_email.return_value = True\n            self._call()\n            assert invalid_txt not in mock_input.call_args[0][0]\n            self._call(invalid=True)\n            assert invalid_txt in mock_input.call_args[0][0]\n\n    @test_util.patch_display_util()\n    def test_optional_invalid_unsafe(self, mock_get_utility):\n        invalid_txt = \"There is a problem\"\n        mock_input = mock_get_utility().input\n        mock_input.return_value = (display_util.OK, \"foo@bar.baz\")\n        with mock.patch(\"certbot.display.ops.util.safe_email\") as mock_safe_email:\n            mock_safe_email.side_effect = [False, True]\n            self._call(invalid=True)\n            assert invalid_txt in mock_input.call_args[0][0]\n\n\nclass ChooseAccountTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.display.ops.choose_account.\"\"\"\n    def setUp(self):\n        super().setUp()\n\n        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))\n\n        self.account_keys_dir = os.path.join(self.tempdir, \"keys\")\n        filesystem.makedirs(self.account_keys_dir, 0o700)\n\n        self.config = mock.MagicMock(\n            accounts_dir=self.tempdir,\n            account_keys_dir=self.account_keys_dir,\n            server=\"certbot-demo.org\")\n        self.key = KEY\n\n        self.acc1 = account.Account(messages.RegistrationResource(\n            uri=None, body=messages.Registration.from_data(\n                email=\"email1@g.com\")), self.key)\n        self.acc2 = account.Account(messages.RegistrationResource(\n            uri=None, body=messages.Registration.from_data(\n                email=\"email2@g.com\", phone=\"phone\")), self.key)\n\n    @classmethod\n    def _call(cls, accounts):\n        return ops.choose_account(accounts)\n\n    @test_util.patch_display_util()\n    def test_one(self, mock_util):\n        mock_util().menu.return_value = (display_util.OK, 0)\n        assert self._call([self.acc1]) == self.acc1\n\n    @test_util.patch_display_util()\n    def test_two(self, mock_util):\n        mock_util().menu.return_value = (display_util.OK, 1)\n        assert self._call([self.acc1, self.acc2]) == self.acc2\n\n    @test_util.patch_display_util()\n    def test_cancel(self, mock_util):\n        mock_util().menu.return_value = (display_util.CANCEL, 1)\n        assert self._call([self.acc1, self.acc2]) is None\n\n\nclass GenHttpsNamesTest(unittest.TestCase):\n    \"\"\"Test _gen_https_names.\"\"\"\n    def setUp(self):\n        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))\n\n    @classmethod\n    def _call(cls, domains):\n        from certbot.display.ops import _gen_https_names\n        return _gen_https_names(domains)\n\n    def test_zero(self):\n        assert self._call([]) == \"\"\n\n    def test_one(self):\n        doms = [\n            \"example.com\",\n            \"asllkjsadfljasdf.c\",\n        ]\n        for dom in doms:\n            assert self._call([dom]) == \"https://%s\" % dom\n\n    def test_two(self):\n        domains_list = [\n            [\"foo.bar.org\", \"bar.org\"],\n            [\"paypal.google.facebook.live.com\", \"*.zombo.example.com\"],\n        ]\n        for doms in domains_list:\n            assert self._call(doms) == \\\n                \"https://{dom[0]} and https://{dom[1]}\".format(dom=doms)\n\n    def test_three(self):\n        doms = [\"a.org\", \"b.org\", \"c.org\"]\n        # We use an oxford comma\n        assert self._call(doms) == \\\n            \"https://{dom[0]}, https://{dom[1]}, and https://{dom[2]}\".format(\n                dom=doms)\n\n    def test_four(self):\n        doms = [\"a.org\", \"b.org\", \"c.org\", \"d.org\"]\n        exp = (\"https://{dom[0]}, https://{dom[1]}, https://{dom[2]}, \"\n               \"and https://{dom[3]}\".format(dom=doms))\n\n        assert self._call(doms) == exp\n\n\nclass ChooseNamesTest(unittest.TestCase):\n    \"\"\"Test choose names.\"\"\"\n    def setUp(self):\n        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))\n        self.mock_install = mock.MagicMock()\n\n    @classmethod\n    def _call(cls, installer, question=None):\n        from certbot.display.ops import choose_names\n        return choose_names(installer, question)\n\n    @mock.patch(\"certbot.display.ops._choose_names_manually\")\n    def test_no_installer(self, mock_manual):\n        self._call(None)\n        assert mock_manual.call_count == 1\n\n    @test_util.patch_display_util()\n    def test_no_installer_cancel(self, mock_util):\n        mock_util().input.return_value = (display_util.CANCEL, [])\n        assert self._call(None) == []\n\n    @test_util.patch_display_util()\n    def test_no_names_choose(self, mock_util):\n        self.mock_install().get_all_names.return_value = set()\n        domain = \"example.com\"\n        mock_util().input.return_value = (display_util.OK, domain)\n\n        actual_doms = self._call(self.mock_install)\n        assert mock_util().input.call_count == 1\n        assert actual_doms == [domain]\n\n    def test_sort_names_trivial(self):\n        from certbot.display.ops import _sort_names\n\n        #sort an empty list\n        assert _sort_names([]) == []\n\n        #sort simple domains\n        some_domains = [\"ex.com\", \"zx.com\", \"ax.com\"]\n        assert _sort_names(some_domains) == [\"ax.com\", \"ex.com\", \"zx.com\"]\n\n        #Sort subdomains of a single domain\n        domain = \".ex.com\"\n        unsorted_short = [\"e\", \"a\", \"z\", \"y\"]\n        unsorted_long = [us + domain for us in unsorted_short]\n\n        sorted_short = sorted(unsorted_short)\n        sorted_long = [us + domain for us in sorted_short]\n\n        assert _sort_names(unsorted_long) == sorted_long\n\n    def test_sort_names_many(self):\n        from certbot.display.ops import _sort_names\n\n        unsorted_domains = [\".cx.com\", \".bx.com\", \".ax.com\", \".dx.com\"]\n        unsorted_short = [\"www\", \"bnother.long.subdomain\", \"a\", \"a.long.subdomain\", \"z\", \"b\"]\n        #Of course sorted doesn't work here ;-)\n        sorted_short = [\"a\", \"b\", \"a.long.subdomain\", \"bnother.long.subdomain\", \"www\", \"z\"]\n\n        to_sort = []\n        for short in unsorted_short:\n            for domain in unsorted_domains:\n                to_sort.append(short+domain)\n        sortd = []\n        for domain in sorted(unsorted_domains):\n            for short in sorted_short:\n                sortd.append(short+domain)\n        assert _sort_names(to_sort) == sortd\n\n\n    @test_util.patch_display_util()\n    def test_filter_names_valid_return(self, mock_util):\n        self.mock_install.get_all_names.return_value = {\"example.com\"}\n        mock_util().checklist.return_value = (display_util.OK, [\"example.com\"])\n\n        names = self._call(self.mock_install)\n        assert names == [\"example.com\"]\n        assert mock_util().checklist.call_count == 1\n\n    @test_util.patch_display_util()\n    def test_filter_namees_override_question(self, mock_util):\n        self.mock_install.get_all_names.return_value = {\"example.com\"}\n        mock_util().checklist.return_value = (display_util.OK, [\"example.com\"])\n        names = self._call(self.mock_install, \"Custom\")\n        assert names == [\"example.com\"]\n        assert mock_util().checklist.call_count == 1\n        assert mock_util().checklist.call_args[0][0] == \"Custom\"\n\n    @test_util.patch_display_util()\n    def test_filter_names_nothing_selected(self, mock_util):\n        self.mock_install.get_all_names.return_value = {\"example.com\"}\n        mock_util().checklist.return_value = (display_util.OK, [])\n\n        assert self._call(self.mock_install) == []\n\n    @test_util.patch_display_util()\n    def test_filter_names_cancel(self, mock_util):\n        self.mock_install.get_all_names.return_value = {\"example.com\"}\n        mock_util().checklist.return_value = (\n            display_util.CANCEL, [\"example.com\"])\n\n        assert self._call(self.mock_install) == []\n\n    def test_get_valid_domains(self):\n        from certbot.display.ops import get_valid_domains\n        all_valid = [\"example.com\", \"second.example.com\",\n                     \"also.example.com\", \"under_score.example.com\",\n                     \"justtld\", \"*.wildcard.com\"]\n        all_invalid = [\"öóòps.net\", \"uniçodé.com\"]\n        two_valid = [\"example.com\", \"úniçøde.com\", \"also.example.com\"]\n        assert get_valid_domains(all_valid) == all_valid\n        assert get_valid_domains(all_invalid) == []\n        assert len(get_valid_domains(two_valid)) == 2\n\n    @test_util.patch_display_util()\n    def test_choose_manually(self, mock_util):\n        from certbot.display.ops import _choose_names_manually\n        utility_mock = mock_util()\n        # No retry\n        utility_mock.yesno.return_value = False\n        # IDN and no retry\n        utility_mock.input.return_value = (display_util.OK,\n                                          \"uniçodé.com\")\n        assert _choose_names_manually() == []\n        # IDN exception with previous mocks\n        with mock.patch(\n                \"certbot.display.ops.internal_display_util.separate_list_input\"\n        ) as mock_sli:\n            unicode_error = UnicodeEncodeError('mock', u'', 0, 1, 'mock')\n            mock_sli.side_effect = unicode_error\n            assert _choose_names_manually() == []\n        # Valid domains\n        utility_mock.input.return_value = (display_util.OK,\n                                          (\"example.com,\"\n                                           \"under_score.example.com,\"\n                                           \"justtld,\"\n                                           \"valid.example.com\"))\n        assert _choose_names_manually() == \\\n                         [\"example.com\", \"under_score.example.com\",\n                          \"justtld\", \"valid.example.com\"]\n\n    @test_util.patch_display_util()\n    def test_choose_manually_retry(self, mock_util):\n        from certbot.display.ops import _choose_names_manually\n        utility_mock = mock_util()\n        # Three iterations\n        utility_mock.input.return_value = (display_util.OK,\n                                          \"uniçodé.com\")\n        utility_mock.yesno.side_effect = [True, True, False]\n        _choose_names_manually()\n        assert utility_mock.yesno.call_count == 3\n\n\nclass SuccessInstallationTest(unittest.TestCase):\n    \"\"\"Test the success installation message.\"\"\"\n    @classmethod\n    def _call(cls, names):\n        from certbot.display.ops import success_installation\n        success_installation(names)\n\n    @test_util.patch_display_util()\n    @mock.patch(\"certbot.display.util.notify\")\n    def test_success_installation(self, mock_notify, mock_display):\n        mock_display().notification.return_value = None\n        names = [\"example.com\", \"abc.com\"]\n\n        self._call(names)\n\n        assert mock_notify.call_count == 1\n        arg = mock_notify.call_args_list[0][0][0]\n\n        for name in names:\n            assert name in arg\n\n\nclass SuccessRenewalTest(unittest.TestCase):\n    \"\"\"Test the success renewal message.\"\"\"\n    @classmethod\n    def _call(cls, names):\n        from certbot.display.ops import success_renewal\n        success_renewal(names)\n\n    @test_util.patch_display_util()\n    @mock.patch(\"certbot.display.util.notify\")\n    def test_success_renewal(self, mock_notify, mock_display):\n        mock_display().notification.return_value = None\n        names = [\"example.com\", \"abc.com\"]\n\n        self._call(names)\n\n        assert mock_notify.call_count == 1\n\n\nclass SuccessRevocationTest(unittest.TestCase):\n    \"\"\"Test the success revocation message.\"\"\"\n    @classmethod\n    def _call(cls, path):\n        from certbot.display.ops import success_revocation\n        success_revocation(path)\n\n    @test_util.patch_display_util()\n    @mock.patch(\"certbot.display.util.notify\")\n    def test_success_revocation(self, mock_notify, unused_mock_display):\n        path = \"/path/to/cert.pem\"\n        self._call(path)\n        mock_notify.assert_called_once_with(\n            \"Congratulations! You have successfully revoked the certificate \"\n            \"that was located at {0}.\".format(path)\n        )\n\n\nclass ValidatorTests(unittest.TestCase):\n    \"\"\"Tests for `validated_input` and `validated_directory`.\"\"\"\n\n    __ERROR = \"Must be non-empty\"\n\n    valid_input = \"asdf\"\n    valid_directory = \"/var/www/html\"\n\n    @staticmethod\n    def __validator(m):\n        if m == \"\":\n            raise errors.PluginError(ValidatorTests.__ERROR)\n\n    @test_util.patch_display_util()\n    def test_input_blank_with_validator(self, mock_util):\n        mock_util().input.side_effect = [(display_util.OK, \"\"),\n                                         (display_util.OK, \"\"),\n                                         (display_util.OK, \"\"),\n                                         (display_util.OK, self.valid_input)]\n\n        returned = ops.validated_input(self.__validator, \"message\", force_interactive=True)\n        assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0]\n        assert returned == (display_util.OK, self.valid_input)\n\n    @test_util.patch_display_util()\n    def test_input_validation_with_default(self, mock_util):\n        mock_util().input.side_effect = [(display_util.OK, self.valid_input)]\n\n        returned = ops.validated_input(self.__validator, \"msg\", default=\"other\")\n        assert returned == (display_util.OK, self.valid_input)\n\n    @test_util.patch_display_util()\n    def test_input_validation_with_bad_default(self, mock_util):\n        mock_util().input.side_effect = [(display_util.OK, self.valid_input)]\n\n        with pytest.raises(AssertionError):\n            ops.validated_input(self.__validator, \"msg\", default=\"\")\n\n    @test_util.patch_display_util()\n    def test_input_cancel_with_validator(self, mock_util):\n        mock_util().input.side_effect = [(display_util.CANCEL, \"\")]\n\n        code, unused_raw = ops.validated_input(self.__validator, \"message\", force_interactive=True)\n        assert code == display_util.CANCEL\n\n    @test_util.patch_display_util()\n    def test_directory_select_validation(self, mock_util):\n        mock_util().directory_select.side_effect = [(display_util.OK, \"\"),\n                                                    (display_util.OK, self.valid_directory)]\n\n        returned = ops.validated_directory(self.__validator, \"msg\", force_interactive=True)\n        assert ValidatorTests.__ERROR == mock_util().notification.call_args[0][0]\n        assert returned == (display_util.OK, self.valid_directory)\n\n    @test_util.patch_display_util()\n    def test_directory_select_validation_with_default(self, mock_util):\n        mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]\n\n        returned = ops.validated_directory(self.__validator, \"msg\", default=\"other\")\n        assert returned == (display_util.OK, self.valid_directory)\n\n    @test_util.patch_display_util()\n    def test_directory_select_validation_with_bad_default(self, mock_util):\n        mock_util().directory_select.side_effect = [(display_util.OK, self.valid_directory)]\n\n        with pytest.raises(AssertionError):\n            ops.validated_directory(self.__validator, \"msg\", default=\"\")\n\n\nclass ChooseValuesTest(unittest.TestCase):\n    \"\"\"Test choose_values.\"\"\"\n    @classmethod\n    def _call(cls, values, question):\n        from certbot.display.ops import choose_values\n        return choose_values(values, question)\n\n    @test_util.patch_display_util()\n    def test_choose_names_success(self, mock_util):\n        items = [\"first\", \"second\", \"third\"]\n        mock_util().checklist.return_value = (display_util.OK, [items[2]])\n        result = self._call(items, None)\n        assert result == [items[2]]\n        assert mock_util().checklist.called is True\n        assert mock_util().checklist.call_args[0][0] == \"\"\n\n    @test_util.patch_display_util()\n    def test_choose_names_success_question(self, mock_util):\n        items = [\"first\", \"second\", \"third\"]\n        question = \"Which one?\"\n        mock_util().checklist.return_value = (display_util.OK, [items[1]])\n        result = self._call(items, question)\n        assert result == [items[1]]\n        assert mock_util().checklist.called is True\n        assert mock_util().checklist.call_args[0][0] == question\n\n    @test_util.patch_display_util()\n    def test_choose_names_user_cancel(self, mock_util):\n        items = [\"first\", \"second\", \"third\"]\n        question = \"Want to cancel?\"\n        mock_util().checklist.return_value = (display_util.CANCEL, [])\n        result = self._call(items, question)\n        assert result == []\n        assert mock_util().checklist.called is True\n        assert mock_util().checklist.call_args[0][0] == question\n\n\n@mock.patch('certbot.display.ops.logger')\n@mock.patch('certbot.display.util.notify')\nclass ReportExecutedCommand(unittest.TestCase):\n    \"\"\"Test report_executed_command\"\"\"\n    @classmethod\n    def _call(cls, cmd_name: str, rc: int, out: str, err: str):\n        from certbot.display.ops import report_executed_command\n        report_executed_command(cmd_name, rc, out, err)\n\n    def test_mixed_success(self, mock_notify, mock_logger):\n        self._call(\"some-hook\", 0, \"Did a thing\", \"Some warning\")\n        assert mock_logger.warning.call_count == 1\n        assert mock_notify.call_count == 1\n\n    def test_mixed_error(self, mock_notify, mock_logger):\n        self._call(\"some-hook\", -127, \"Did a thing\", \"Some warning\")\n        assert mock_logger.warning.call_count == 2\n        assert mock_notify.call_count == 1\n\n    def test_empty_success(self, mock_notify, mock_logger):\n        self._call(\"some-hook\", 0, \"\\n\", \" \")\n        assert mock_logger.warning.call_count == 0\n        assert mock_notify.call_count == 0\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/display/util_test.py",
    "content": "\"\"\"Test :mod:`certbot.display.util`.\"\"\"\nimport sys\n\nimport pytest\n\nimport certbot.tests.util as test_util\n\n\n@test_util.patch_display_util()\ndef test_notify(mock_util):\n    from certbot.display.util import notify\n    notify(\"Hello World\")\n    mock_util().notification.assert_called_with(\n        \"Hello World\", pause=False, decorate=False, wrap=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_notification(mock_util):\n    from certbot.display.util import notification\n    notification(\"Hello World\")\n    mock_util().notification.assert_called_with(\n        \"Hello World\", pause=True, decorate=True, wrap=True, force_interactive=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_menu(mock_util):\n    from certbot.display.util import menu\n    menu(\"Hello World\", [\"one\", \"two\"], default=0)\n    mock_util().menu.assert_called_with(\n        \"Hello World\", [\"one\", \"two\"], default=0, cli_flag=None, force_interactive=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_input_text(mock_util):\n    from certbot.display.util import input_text\n    input_text(\"Hello World\", default=\"something\")\n    mock_util().input.assert_called_with(\n        \"Hello World\", default='something', cli_flag=None, force_interactive=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_yesno(mock_util):\n    from certbot.display.util import yesno\n    yesno(\"Hello World\", default=True)\n    mock_util().yesno.assert_called_with(\n        \"Hello World\", yes_label='Yes', no_label='No', default=True, cli_flag=None,\n        force_interactive=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_checklist(mock_util):\n    from certbot.display.util import checklist\n    checklist(\"Hello World\", [\"one\", \"two\"], default=\"one\")\n    mock_util().checklist.assert_called_with(\n        \"Hello World\", ['one', 'two'], default='one', cli_flag=None, force_interactive=False\n    )\n\n\n@test_util.patch_display_util()\ndef test_directory_select(mock_util):\n    from certbot.display.util import directory_select\n    directory_select(\"Hello World\", default=\"something\")\n    mock_util().directory_select.assert_called_with(\n        \"Hello World\", default='something', cli_flag=None, force_interactive=False\n    )\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/eff_test.py",
    "content": "\"\"\"Tests for certbot._internal.eff.\"\"\"\nimport datetime\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport josepy\nimport pytest\nimport requests\n\nfrom acme import messages\nfrom certbot._internal import account\nfrom certbot._internal import constants\nimport certbot.tests.util as test_util\n\n_KEY = josepy.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass SubscriptionTest(test_util.ConfigTestCase):\n    \"\"\"Abstract class for subscription tests.\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.account = account.Account(\n            regr=messages.RegistrationResource(\n                uri=None, body=messages.Registration(),\n                new_authzr_uri='hi'),\n            key=_KEY,\n            meta=account.Account.Meta(\n                creation_host='test.certbot.org',\n                creation_dt=datetime.datetime(\n                    2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc)))\n        self.config.email = 'certbot@example.org'\n        self.config.eff_email = None\n\n\nclass PrepareSubscriptionTest(SubscriptionTest):\n    \"\"\"Tests for certbot._internal.eff.prepare_subscription.\"\"\"\n    def _call(self):\n        from certbot._internal.eff import prepare_subscription\n        prepare_subscription(self.config, self.account)\n\n    @test_util.patch_display_util()\n    @mock.patch(\"certbot._internal.eff.display_util.notify\")\n    def test_failure(self, mock_notify, mock_get_utility):\n        self.config.email = None\n        self.config.eff_email = True\n        self._call()\n        actual = mock_notify.call_args[0][0]\n        expected_part = \"because you didn't provide an e-mail address\"\n        assert expected_part in actual\n        assert self.account.meta.register_to_eff is None\n\n    @test_util.patch_display_util()\n    def test_will_not_subscribe_with_no_prompt(self, mock_get_utility):\n        self.config.eff_email = False\n        self._call()\n        self._assert_no_get_utility_calls(mock_get_utility)\n        assert self.account.meta.register_to_eff is None\n\n    @test_util.patch_display_util()\n    def test_will_subscribe_with_no_prompt(self, mock_get_utility):\n        self.config.eff_email = True\n        self._call()\n        self._assert_no_get_utility_calls(mock_get_utility)\n        assert self.account.meta.register_to_eff == self.config.email\n\n    @test_util.patch_display_util()\n    def test_will_not_subscribe_with_prompt(self, mock_get_utility):\n        mock_get_utility().yesno.return_value = False\n        self._call()\n        assert not mock_get_utility().add_message.called\n        self._assert_correct_yesno_call(mock_get_utility)\n        assert self.account.meta.register_to_eff is None\n\n    @test_util.patch_display_util()\n    def test_will_subscribe_with_prompt(self, mock_get_utility):\n        mock_get_utility().yesno.return_value = True\n        self._call()\n        assert not mock_get_utility().add_message.called\n        self._assert_correct_yesno_call(mock_get_utility)\n        assert self.account.meta.register_to_eff == self.config.email\n\n    def _assert_no_get_utility_calls(self, mock_get_utility):\n        assert not mock_get_utility().yesno.called\n        assert not mock_get_utility().add_message.called\n\n    def _assert_correct_yesno_call(self, mock_get_utility):\n        assert mock_get_utility().yesno.called\n        call_args, call_kwargs = mock_get_utility().yesno.call_args\n        actual = call_args[0]\n        expected_part = 'Electronic Frontier Foundation'\n        assert expected_part in actual\n        assert not call_kwargs.get('default', True)\n\n\nclass HandleSubscriptionTest(SubscriptionTest):\n    \"\"\"Tests for certbot._internal.eff.handle_subscription.\"\"\"\n    def _call(self):\n        from certbot._internal.eff import handle_subscription\n        handle_subscription(self.config, self.account)\n\n    @mock.patch('certbot._internal.eff.subscribe')\n    def test_no_subscribe(self, mock_subscribe):\n        self._call()\n        assert mock_subscribe.called is False\n\n    @mock.patch('certbot._internal.eff.subscribe')\n    def test_subscribe(self, mock_subscribe):\n        self.account.meta = self.account.meta.update(register_to_eff=self.config.email)\n        self._call()\n        assert mock_subscribe.called\n        assert mock_subscribe.call_args[0][0] == self.config.email\n\n\nclass SubscribeTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.eff.subscribe.\"\"\"\n    def setUp(self):\n        self.email = 'certbot@example.org'\n        self.json = {'status': True}\n        self.response = mock.Mock(ok=True)\n        self.response.json.return_value = self.json\n        patcher = mock.patch(\"certbot._internal.eff.display_util.notify\")\n        self.mock_notify = patcher.start()\n        self.addCleanup(patcher.stop)\n\n    @mock.patch('certbot._internal.eff.requests.post')\n    def _call(self, mock_post):\n        mock_post.return_value = self.response\n\n        from certbot._internal.eff import subscribe\n        subscribe(self.email)\n        self._check_post_call(mock_post)\n\n    def _check_post_call(self, mock_post):\n        assert mock_post.call_count == 1\n        call_args, call_kwargs = mock_post.call_args\n        assert call_args[0] == constants.EFF_SUBSCRIBE_URI\n\n        data = call_kwargs.get('data')\n        assert data is not None\n        assert data.get('email') == self.email\n\n    def test_bad_status(self):\n        self.json['status'] = False\n        self._call()\n        actual = self._get_reported_message()\n        expected_part = 'because your e-mail address appears to be invalid.'\n        assert expected_part in actual\n\n    def test_not_ok(self):\n        self.response.ok = False\n        self.response.raise_for_status.side_effect = requests.exceptions.HTTPError\n        self._call()\n        actual = self._get_reported_message()\n        unexpected_part = 'because'\n        assert unexpected_part not in actual\n\n    def test_response_not_json(self):\n        self.response.json.side_effect = ValueError()\n        self._call()\n        actual = self._get_reported_message()\n        expected_part = 'problem'\n        assert expected_part in actual\n\n    def test_response_json_missing_status_element(self):\n        self.json.clear()\n        self._call()\n        actual = self._get_reported_message()\n        expected_part = 'problem'\n        assert expected_part in actual\n\n    def _get_reported_message(self):\n        assert self.mock_notify.called\n        return self.mock_notify.call_args[0][0]\n\n    @test_util.patch_display_util()\n    def test_subscribe(self, mock_get_utility):\n        self._call()\n        assert mock_get_utility.called is False\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/error_handler_test.py",
    "content": "\"\"\"Tests for certbot._internal.error_handler.\"\"\"\nimport contextlib\nimport signal\nimport sys\nfrom typing import Callable\nfrom typing import Union\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot.compat import os\n\n\ndef get_signals(signums):\n    \"\"\"Get the handlers for an iterable of signums.\"\"\"\n    return {s: signal.getsignal(s) for s in signums}\n\n\ndef set_signals(sig_handler_dict):\n    \"\"\"Set the signal (keys) with the handler (values) from the input dict.\"\"\"\n    for s, h in sig_handler_dict.items():\n        signal.signal(s, h)\n\n\n@contextlib.contextmanager\ndef signal_receiver(signums):\n    \"\"\"Context manager to catch signals\"\"\"\n    signals = []\n    prev_handlers: dict[int, Union[int, None, Callable]] = get_signals(signums)\n    set_signals({s: lambda s, _: signals.append(s) for s in signums})\n    yield signals\n    set_signals(prev_handlers)\n\n\ndef send_signal(signum):\n    \"\"\"Send the given signal\"\"\"\n    os.kill(os.getpid(), signum)\n\n\nclass ErrorHandlerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.error_handler.ErrorHandler.\"\"\"\n\n    def setUp(self):\n        from certbot._internal import error_handler\n\n        self.init_func = mock.MagicMock()\n        self.init_args = {42,}\n        self.init_kwargs = {'foo': 'bar'}\n        self.handler = error_handler.ErrorHandler(self.init_func,\n                                                  *self.init_args,\n                                                  **self.init_kwargs)\n\n        # pylint: disable=protected-access\n        self.signals = error_handler._SIGNALS\n\n    def test_context_manager(self):\n        exception_raised = False\n        try:\n            with self.handler:\n                raise ValueError\n        except ValueError:\n            exception_raised = True\n\n        assert exception_raised\n        self.init_func.assert_called_once_with(*self.init_args,\n                                               **self.init_kwargs)\n\n    def test_context_manager_with_signal(self):\n        if not self.signals:\n            self.skipTest(reason='Signals cannot be handled on Windows.')\n        init_signals = get_signals(self.signals)\n        with signal_receiver(self.signals) as signals_received:\n            with self.handler:\n                should_be_42 = 42\n                send_signal(self.signals[0])\n                should_be_42 *= 10\n\n        # check execution stopped when the signal was sent\n        assert 42 == should_be_42\n        # assert signals were caught\n        assert [self.signals[0]] == signals_received\n        # assert the error handling function was just called once\n        self.init_func.assert_called_once_with(*self.init_args,\n                                               **self.init_kwargs)\n        for signum in self.signals:\n            assert init_signals[signum] == signal.getsignal(signum)\n\n    def test_bad_recovery(self):\n        bad_func = mock.MagicMock(side_effect=[ValueError])\n        self.handler.register(bad_func)\n        try:\n            with self.handler:\n                raise ValueError\n        except ValueError:\n            pass\n        self.init_func.assert_called_once_with(*self.init_args,\n                                               **self.init_kwargs)\n        bad_func.assert_called_once_with()\n\n    def test_bad_recovery_with_signal(self):\n        if not self.signals:\n            self.skipTest(reason='Signals cannot be handled on Windows.')\n        sig1 = self.signals[0]\n        sig2 = self.signals[-1]\n        bad_func = mock.MagicMock(side_effect=lambda: send_signal(sig1))\n        self.handler.register(bad_func)\n        with signal_receiver(self.signals) as signals_received:\n            with self.handler:\n                send_signal(sig2)\n        assert [sig2, sig1] == signals_received\n        self.init_func.assert_called_once_with(*self.init_args,\n                                               **self.init_kwargs)\n        bad_func.assert_called_once_with()\n\n    def test_sysexit_ignored(self):\n        try:\n            with self.handler:\n                sys.exit(0)\n        except SystemExit:\n            pass\n        assert self.init_func.called is False\n\n    def test_regular_exit(self):\n        func = mock.MagicMock()\n        self.handler.register(func)\n        with self.handler:\n            pass\n        self.init_func.assert_not_called()\n        func.assert_not_called()\n\n\nclass ExitHandlerTest(ErrorHandlerTest):\n    \"\"\"Tests for certbot._internal.error_handler.ExitHandler.\"\"\"\n\n    def setUp(self):\n        from certbot._internal import error_handler\n        super().setUp()\n        self.handler = error_handler.ExitHandler(self.init_func,\n                                                 *self.init_args,\n                                                 **self.init_kwargs)\n\n    def test_regular_exit(self):\n        func = mock.MagicMock()\n        self.handler.register(func)\n        with self.handler:\n            pass\n        self.init_func.assert_called_once_with(*self.init_args,\n                                               **self.init_kwargs)\n        func.assert_called_once_with()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/errors_test.py",
    "content": "\"\"\"Tests for certbot.errors.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot.tests import acme_util\n\n\nclass FailedChallengesTest(unittest.TestCase):\n    \"\"\"Tests for certbot.errors.FailedChallenges.\"\"\"\n\n    def setUp(self):\n        from certbot.errors import FailedChallenges\n        self.error = FailedChallenges({achallenges.DNS(\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"example.com\"),\n            challb=messages.ChallengeBody(\n                chall=acme_util.DNS01, uri=None,\n                error=messages.Error.with_code(\"tls\", detail=\"detail\")))})\n\n    def test_str(self):\n        assert str(self.error).startswith(\n            \"Failed authorization procedure. example.com (dns-01): \"\n            \"urn:ietf:params:acme:error:tls\")\n\n    def test_unicode(self):\n        from certbot.errors import FailedChallenges\n        arabic_detail = u'\\u0639\\u062f\\u0627\\u0644\\u0629'\n        arabic_error = FailedChallenges({achallenges.DNS(\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"example.com\"),\n            challb=messages.ChallengeBody(\n                chall=acme_util.DNS01, uri=None,\n                error=messages.Error.with_code(\"tls\", detail=arabic_detail)))})\n\n        assert str(arabic_error).startswith(\n            \"Failed authorization procedure. example.com (dns-01): \"\n            \"urn:ietf:params:acme:error:tls\")\n\n\nclass StandaloneBindErrorTest(unittest.TestCase):\n    \"\"\"Tests for certbot.errors.StandaloneBindError.\"\"\"\n\n    def setUp(self):\n        from certbot.errors import StandaloneBindError\n        self.error = StandaloneBindError(mock.sentinel.error, 1234)\n\n    def test_instance_args(self):\n        assert mock.sentinel.error == self.error.socket_error\n        assert 1234 == self.error.port\n\n    def test_str(self):\n        assert str(self.error).startswith(\n            \"Problem binding to port 1234: \")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/helpful_test.py",
    "content": "\"\"\"Tests for certbot.helpful_parser\"\"\"\nimport sys\n\nimport pytest\n\nfrom certbot._internal.cli import HelpfulArgumentParser\n\n\nclass TestScanningFlags:\n    '''Test the prescan_for_flag method of HelpfulArgumentParser'''\n    def test_prescan_no_help_flag(self):\n        arg_parser = HelpfulArgumentParser(['run'], {})\n        detected_flag = arg_parser.prescan_for_flag('--help',\n                                                        ['all', 'certonly'])\n        assert detected_flag is False\n        detected_flag = arg_parser.prescan_for_flag('-h',\n                                                        ['all, certonly'])\n        assert detected_flag is False\n\n    def test_prescan_unvalid_topic(self):\n        arg_parser = HelpfulArgumentParser(['--help', 'all'], {})\n        detected_flag = arg_parser.prescan_for_flag('--help',\n                                                    ['potato'])\n        assert detected_flag is True\n        detected_flag = arg_parser.prescan_for_flag('-h',\n                                                    arg_parser.help_topics)\n        assert detected_flag is False\n\n    def test_prescan_valid_topic(self):\n        arg_parser = HelpfulArgumentParser(['-h', 'all'], {})\n        detected_flag = arg_parser.prescan_for_flag('-h',\n                                                    arg_parser.help_topics)\n        assert detected_flag == 'all'\n        detected_flag = arg_parser.prescan_for_flag('--help',\n                                                    arg_parser.help_topics)\n        assert detected_flag is False\n\nclass TestDetermineVerbs:\n    '''Tests for determine_verb methods of HelpfulArgumentParser'''\n    def test_determine_verb_wrong_verb(self):\n        arg_parser = HelpfulArgumentParser(['potato'], {})\n        assert arg_parser.verb == \"run\"\n        assert arg_parser.args == [\"potato\"]\n\n    def test_determine_verb_help(self):\n        arg_parser = HelpfulArgumentParser(['--help', 'everything'], {})\n        assert arg_parser.verb == \"help\"\n        assert arg_parser.args == [\"--help\", \"everything\"]\n        arg_parser = HelpfulArgumentParser(['-d', 'some_domain', '--help',\n                                               'all'], {})\n        assert arg_parser.verb == \"help\"\n        assert arg_parser.args == ['-d', 'some_domain', '--help',\n                                               'all']\n\n    def test_determine_verb(self):\n        arg_parser = HelpfulArgumentParser(['certonly'], {})\n        assert arg_parser.verb == 'certonly'\n        assert arg_parser.args == []\n\n        arg_parser = HelpfulArgumentParser(['auth'], {})\n        assert arg_parser.verb == 'certonly'\n        assert arg_parser.args == []\n\n        arg_parser = HelpfulArgumentParser(['everything'], {})\n        assert arg_parser.verb == 'run'\n        assert arg_parser.args == []\n\n\nclass TestAdd:\n    '''Tests for add method in HelpfulArgumentParser'''\n    def test_add_trivial_argument(self):\n        arg_parser = HelpfulArgumentParser(['run'], {})\n        arg_parser.add(None, \"--hello-world\")\n        parsed_args = arg_parser.parser.parse_args(['--hello-world',\n                                                    'Hello World!'])\n        assert parsed_args.hello_world == 'Hello World!'\n        assert not hasattr(parsed_args, 'potato')\n\n    def test_add_expected_argument(self):\n        arg_parser = HelpfulArgumentParser(['--help', 'run'], {})\n        arg_parser.add(\n                [None, \"run\", \"certonly\", \"register\"],\n                \"--eab-kid\", dest=\"eab_kid\", action=\"store\",\n                metavar=\"EAB_KID\",\n                help=\"Key Identifier for External Account Binding\")\n        parsed_args = arg_parser.parser.parse_args([\"--eab-kid\", None])\n        assert parsed_args.eab_kid is None\n        assert hasattr(parsed_args, 'eab_kid')\n\n\nclass TestAddGroup:\n    '''Test add_group method of HelpfulArgumentParser'''\n    def test_add_group_no_input(self):\n        arg_parser = HelpfulArgumentParser(['run'], {})\n        with pytest.raises(TypeError):\n            arg_parser.add_group()\n\n    def test_add_group_topic_not_visible(self):\n        # The user request help on run. A topic that given somewhere in the\n        # args won't be added to the groups in the parser.\n        arg_parser = HelpfulArgumentParser(['--help', 'run'], {})\n        arg_parser.add_group(\"auth\",\n                                         description=\"description of auth\")\n        assert arg_parser.groups == {}\n\n    def test_add_group_topic_requested_help(self):\n        arg_parser = HelpfulArgumentParser(['--help', 'run'], {})\n        arg_parser.add_group(\"run\",\n                                         description=\"description of run\")\n        assert arg_parser.groups[\"run\"]\n        arg_parser.add_group(\"certonly\", description=\"description of certonly\")\n        with pytest.raises(KeyError):\n            assert arg_parser.groups[\"certonly\"] is False\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/hook_test.py",
    "content": "\"\"\"Tests for certbot._internal.hooks.\"\"\"\nimport sys\nimport unittest\nfrom platform import python_version_tuple\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\n\ndef pyver_lt(major: int, minor: int):\n    pymajor = int(python_version_tuple()[0])\n    pyminor = int(python_version_tuple()[1])\n    if pymajor < major:\n        return True\n    elif pymajor > major:\n        return False\n    else:\n        return pyminor < minor\n\n\nclass ValidateHooksTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.hooks.validate_hooks.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import validate_hooks\n        return validate_hooks(*args, **kwargs)\n\n    @mock.patch(\"certbot._internal.hooks.validate_hook\")\n    def test_it(self, mock_validate_hook):\n        config = mock.MagicMock()\n        self._call(config)\n\n        types = [call[0][1] for call in mock_validate_hook.call_args_list]\n        assert {\"pre\", \"post\", \"deploy\",} == set(types)\n\n\nclass ValidateHookTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.hooks.validate_hook.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import validate_hook\n        return validate_hook(*args, **kwargs)\n\n    def test_hook_not_executable(self):\n        # prevent unnecessary modifications to PATH\n        with mock.patch(\"certbot._internal.hooks.plug_util.path_surgery\"):\n            # We just mock out filesystem.is_executable since on Windows, it is difficult\n            # to get a fully working test around executable permissions. See\n            # certbot.tests.compat.filesystem::NotExecutableTest for more in-depth tests.\n            with mock.patch(\"certbot._internal.hooks.filesystem.is_executable\", return_value=False):\n                with pytest.raises(errors.HookCommandNotFound):\n                    self._call('dummy', \"foo\")\n\n    @mock.patch(\"certbot._internal.hooks.util.exe_exists\")\n    def test_not_found(self, mock_exe_exists):\n        mock_exe_exists.return_value = False\n        with mock.patch(\"certbot._internal.hooks.plug_util.path_surgery\") as mock_ps:\n            with pytest.raises(errors.HookCommandNotFound):\n                self._call(\"foo\", \"bar\")\n        assert mock_ps.called\n\n    @mock.patch(\"certbot._internal.hooks._prog\")\n    def test_unset(self, mock_prog):\n        self._call(None, \"foo\")\n        assert mock_prog.called is False\n\n\nclass HookTest(test_util.ConfigTestCase):\n    \"\"\"Common base class for hook tests.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):  # pragma: no cover\n        \"\"\"Calls the method being tested with the given arguments.\"\"\"\n        raise NotImplementedError\n\n    @classmethod\n    def _call_with_mock_execute(cls, *args, **kwargs):\n        \"\"\"Calls self._call after mocking out certbot.compat.misc.execute_command_status.\n\n        The mock execute object is returned rather than the return value\n        of self._call.\n\n        \"\"\"\n        with mock.patch(\"certbot.compat.misc.execute_command_status\") as mock_execute:\n            mock_execute.return_value = (0, \"\", \"\")\n            cls._call(*args, **kwargs)\n        return mock_execute\n\n\nclass PreHookTest(HookTest):\n    \"\"\"Tests for certbot._internal.hooks.pre_hook.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import pre_hook\n        return pre_hook(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.config.pre_hook = \"foo\"\n\n        filesystem.makedirs(self.config.renewal_pre_hooks_dir)\n        self.dir_hook = os.path.join(self.config.renewal_pre_hooks_dir, \"bar\")\n        create_hook(self.dir_hook)\n\n        # Reset this value as it may have been modified by past tests\n        self._reset_pre_hook_already()\n\n    def tearDown(self):\n        # Reset this value so it's unmodified for future tests\n        self._reset_pre_hook_already()\n        super().tearDown()\n\n    def _reset_pre_hook_already(self):\n        from certbot._internal.hooks import executed_pre_hooks\n        executed_pre_hooks.clear()\n\n    def test_certonly(self):\n        self.config.verb = \"certonly\"\n        self._test_nonrenew_common()\n\n    def test_run(self):\n        self.config.verb = \"run\"\n        self._test_nonrenew_common()\n\n    def _test_nonrenew_common(self):\n        mock_execute = self._call_with_mock_execute(self.config)\n        mock_execute.assert_any_call(\"pre-hook\", self.dir_hook, env=mock.ANY)\n        mock_execute.assert_called_with(\"pre-hook\", self.config.pre_hook, env=mock.ANY)\n        self._test_no_executions_common()\n\n    def test_no_hooks(self):\n        self.config.pre_hook = None\n        self.config.verb = \"renew\"\n        os.remove(self.dir_hook)\n\n        with mock.patch(\"certbot._internal.hooks.logger\") as mock_logger:\n            mock_execute = self._call_with_mock_execute(self.config)\n        assert mock_execute.called is False\n        assert mock_logger.info.called is False\n\n    def test_renew_disabled_dir_hooks(self):\n        self.config.directory_hooks = False\n        mock_execute = self._call_with_mock_execute(self.config)\n        mock_execute.assert_called_once_with(\"pre-hook\", self.config.pre_hook, env=mock.ANY)\n        self._test_no_executions_common()\n\n    def test_renew_no_overlap(self):\n        self.config.verb = \"renew\"\n        mock_execute = self._call_with_mock_execute(self.config)\n        mock_execute.assert_any_call(\"pre-hook\", self.dir_hook, env=mock.ANY)\n        mock_execute.assert_called_with(\"pre-hook\", self.config.pre_hook, env=mock.ANY)\n        self._test_no_executions_common()\n\n    def test_renew_with_overlap(self):\n        self.config.pre_hook = self.dir_hook\n        self.config.verb = \"renew\"\n        mock_execute = self._call_with_mock_execute(self.config)\n        mock_execute.assert_called_once_with(\"pre-hook\", self.dir_hook, env=mock.ANY)\n        self._test_no_executions_common()\n\n    def _test_no_executions_common(self):\n        with mock.patch(\"certbot._internal.hooks.logger\") as mock_logger:\n            mock_execute = self._call_with_mock_execute(self.config)\n        assert mock_execute.called is False\n        assert mock_logger.info.called\n\n\nclass PostHookTest(HookTest):\n    \"\"\"Tests for certbot._internal.hooks.post_hook.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import post_hook\n        return post_hook(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n\n        self.config.post_hook = \"bar\"\n        filesystem.makedirs(self.config.renewal_post_hooks_dir)\n        self.dir_hook = os.path.join(self.config.renewal_post_hooks_dir, \"foo\")\n        create_hook(self.dir_hook)\n\n        # Reset this value as it may have been modified by past tests\n        self._reset_post_hook_eventually()\n\n    def tearDown(self):\n        # Reset this value so it's unmodified for future tests\n        self._reset_post_hook_eventually()\n        super().tearDown()\n\n    def _reset_post_hook_eventually(self):\n        from certbot._internal.hooks import post_hooks\n        del post_hooks[:]\n\n    def test_certonly_and_run_with_hook(self):\n        for verb in (\"certonly\", \"run\",):\n            self.config.verb = verb\n            mock_execute = self._call_with_mock_execute(self.config, [])\n            mock_execute.assert_any_call(\"post-hook\", self.dir_hook, env=mock.ANY)\n            mock_execute.assert_called_with(\"post-hook\", self.config.post_hook, env=mock.ANY)\n            assert not self._get_eventually()\n\n    def test_certonly_and_run_without_cli_hook(self):\n        self.config.post_hook = None\n        for verb in (\"certonly\", \"run\",):\n            self.config.verb = verb\n            mock_execute = self._call_with_mock_execute(self.config, [])\n            mock_execute.assert_called_once_with(\"post-hook\", self.dir_hook, env=mock.ANY)\n            assert not self._get_eventually()\n\n    def test_renew_env(self):\n        self.config.verb = \"certonly\"\n        args = self._call_with_mock_execute(self.config, [\"success.org\"]).call_args\n        assert args.kwargs['env'][\"RENEWED_DOMAINS\"] == \"success.org\"\n\n    def test_renew_disabled_dir_hooks(self):\n        self.config.directory_hooks = False\n        self._test_renew_common([self.config.post_hook])\n\n    def test_renew_no_config_hook(self):\n        self.config.post_hook = None\n        self._test_renew_common([self.dir_hook])\n\n    def test_renew_no_dir_hook(self):\n        os.remove(self.dir_hook)\n        self._test_renew_common([self.config.post_hook])\n\n    def test_renew_no_hooks(self):\n        self.config.post_hook = None\n        os.remove(self.dir_hook)\n        self._test_renew_common([])\n\n    def test_renew_no_overlap(self):\n        expected = [self.dir_hook, self.config.post_hook]\n        self._test_renew_common(expected)\n\n        self.config.post_hook = \"baz\"\n        expected.append(self.config.post_hook)\n        self._test_renew_common(expected)\n\n    def test_renew_with_overlap(self):\n        self.config.post_hook = self.dir_hook\n        self._test_renew_common([self.dir_hook])\n\n    def _test_renew_common(self, expected):\n        self.config.verb = \"renew\"\n\n        for _ in range(2):\n            self._call(self.config, [])\n            assert self._get_eventually() == expected\n\n    def _get_eventually(self):\n        from certbot._internal.hooks import post_hooks\n        return post_hooks\n\n\nclass RunSavedPostHooksTest(HookTest):\n    \"\"\"Tests for certbot._internal.hooks.run_saved_post_hooks.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import run_saved_post_hooks\n        renewed_domains = kwargs[\"renewed_domains\"] if \"renewed_domains\" in kwargs else args[0]\n        failed_domains = kwargs[\"failed_domains\"] if \"failed_domains\" in kwargs else args[1]\n\n        return run_saved_post_hooks(renewed_domains, failed_domains)\n\n    def _call_with_mock_execute_and_eventually(self, *args, **kwargs):\n        \"\"\"Call run_saved_post_hooks but mock out execute and eventually\n\n        certbot._internal.hooks.post_hooks is replaced with\n        self.eventually. The mock execute object is returned rather than\n        the return value of run_saved_post_hooks.\n\n        \"\"\"\n        eventually_path = \"certbot._internal.hooks.post_hooks\"\n        with mock.patch(eventually_path, new=self.eventually):\n            return self._call_with_mock_execute(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.eventually: list[str] = []\n\n    def test_empty(self):\n        assert not self._call_with_mock_execute_and_eventually([], []).called\n\n    def test_multiple(self):\n        self.eventually = [\"foo\", \"bar\", \"baz\", \"qux\"]\n        mock_execute = self._call_with_mock_execute_and_eventually([], [])\n\n        calls = mock_execute.call_args_list\n        for actual_call, expected_arg in zip(calls, self.eventually):\n            assert actual_call[0][1] == expected_arg\n\n    def test_single(self):\n        self.eventually = [\"foo\"]\n        mock_execute = self._call_with_mock_execute_and_eventually([], [])\n        mock_execute.assert_called_once_with(\"post-hook\", self.eventually[0], env=mock.ANY)\n\n    def test_env(self):\n        self.eventually = [\"foo\"]\n        mock_execute = self._call_with_mock_execute_and_eventually([\"success.org\"], [\"failed.org\"])\n        assert mock_execute.call_args.kwargs['env'][\"RENEWED_DOMAINS\"] == \"success.org\"\n        assert mock_execute.call_args.kwargs['env'][\"FAILED_DOMAINS\"] == \"failed.org\"\n\n\nclass RenewalHookTest(HookTest):\n    \"\"\"Common base class for testing deploy/renew hooks.\"\"\"\n    # Needed for https://github.com/PyCQA/pylint/issues/179\n    # pylint: disable=abstract-method\n\n    def _call_with_mock_execute(self, *args, **kwargs):\n        \"\"\"Calls self._call after mocking out certbot.compat.misc.execute_command_status.\n\n        The mock execute object is returned rather than the return value\n        of self._call. The mock execute object asserts that environment\n        variables were properly set.\n\n        \"\"\"\n        domains = kwargs[\"domains\"] if \"domains\" in kwargs else args[1]\n        lineage = kwargs[\"lineage\"] if \"lineage\" in kwargs else args[2]\n\n        def execute_side_effect(*unused_args, **unused_kwargs):\n            \"\"\"Assert environment variables are properly set.\n\n            :returns: two strings imitating no output from the hook\n            :rtype: `tuple` of `str`\n\n            \"\"\"\n            assert os.environ[\"RENEWED_DOMAINS\"] == \" \".join(domains)\n            assert os.environ[\"RENEWED_LINEAGE\"] == lineage\n            return (0, \"\", \"\")\n\n        with mock.patch(\"certbot.compat.misc.execute_command_status\") as mock_execute:\n            mock_execute.side_effect = execute_side_effect\n            self._call(*args, **kwargs)\n        return mock_execute\n\n    def setUp(self):\n        super().setUp()\n        self.vars_to_clear = {\n            var for var in (\"RENEWED_DOMAINS\", \"RENEWED_LINEAGE\",)\n            if var not in os.environ\n        }\n\n    def tearDown(self):\n        for var in self.vars_to_clear:\n            os.environ.pop(var, None)\n        super().tearDown()\n\n\nclass DeployHookTest(RenewalHookTest):\n    \"\"\"Tests for certbot._internal.hooks.deploy_hook.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import deploy_hook\n        return deploy_hook(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.config.deploy_hook = \"foo\"\n\n        filesystem.makedirs(self.config.renewal_deploy_hooks_dir)\n        self.dir_hook = os.path.join(self.config.renewal_deploy_hooks_dir,\n                                     \"bar\")\n        create_hook(self.dir_hook)\n\n    def test_no_hooks(self):\n        self.config.deploy_hook = None\n        os.remove(self.dir_hook)\n\n        with mock.patch(\"certbot._internal.hooks.logger\") as mock_logger:\n            mock_execute = self._call_with_mock_execute(\n                self.config, [\"example.org\"], \"/foo/bar\")\n        assert mock_execute.called is False\n        assert mock_logger.info.called is False\n\n    def test_success(self):\n        domains = [\"example.org\", \"example.net\"]\n        lineage = \"/foo/bar\"\n        self.config.deploy_hook = \"foo\"\n        mock_execute = self._call_with_mock_execute(\n            self.config, domains, lineage)\n        assert mock_execute.call_count == 2\n\n    def test_disabled_dir_hooks(self):\n        self.config.directory_hooks = False\n        mock_execute = self._call_with_mock_execute(\n            self.config, [\"example.org\"], \"/foo/bar\")\n        mock_execute.assert_called_once_with(\"deploy-hook\", self.config.deploy_hook, env=mock.ANY)\n\n    @mock.patch(\"certbot._internal.hooks.logger\")\n    def test_dry_run(self, mock_logger):\n        self.config.dry_run = True\n        mock_execute = self._call_with_mock_execute(\n            self.config, [\"example.org\"], \"/foo/bar\")\n        assert mock_execute.called is False\n        assert mock_logger.info.call_count == 2\n\n    def test_overlap(self):\n        self.config.deploy_hook = self.dir_hook\n        mock_execute = self._call_with_mock_execute(\n            self.config, [\"example.net\", \"example.org\"], \"/foo/bar\")\n        mock_execute.assert_called_once_with(\"deploy-hook\", self.dir_hook, env=mock.ANY)\n\n    def test_no_overlap(self):\n        mock_execute = self._call_with_mock_execute(\n            self.config, [\"example.org\"], \"/foo/bar\")\n        mock_execute.assert_any_call(\"deploy-hook\", self.dir_hook, env=mock.ANY)\n        mock_execute.assert_called_with(\"deploy-hook\", self.config.deploy_hook, env=mock.ANY)\n\n\nclass ListHooksTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.hooks.list_hooks.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.hooks import list_hooks\n        return list_hooks(*args, **kwargs)\n\n    def test_empty(self):\n        assert not self._call(self.tempdir)\n\n    def test_multiple(self):\n        names = sorted(\n            os.path.join(self.tempdir, basename)\n            for basename in (\"foo\", \"bar\", \"baz\", \"qux\")\n        )\n        for name in names:\n            create_hook(name)\n\n        assert self._call(self.tempdir) == names\n\n    def test_single(self):\n        name = os.path.join(self.tempdir, \"foo\")\n        create_hook(name)\n\n        assert self._call(self.tempdir) == [name]\n\n    def test_ignore_tilde(self):\n        name = os.path.join(self.tempdir, \"foo~\")\n        create_hook(name)\n\n        assert self._call(self.tempdir) == []\n\n\ndef create_hook(file_path):\n    \"\"\"Creates an executable file at the specified path.\n\n    :param str file_path: path to create the file at\n\n    \"\"\"\n    util.safe_open(file_path, mode=\"w\", chmod=0o744).close()\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/lock_test.py",
    "content": "\"\"\"Tests for certbot._internal.lock.\"\"\"\nimport functools\nimport importlib\nimport multiprocessing\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\nif importlib.util.find_spec('fcntl'):\n    POSIX_MODE = True\nelse:\n    POSIX_MODE = False\n\n\n\nclass LockDirTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.lock.lock_dir.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.lock import lock_dir\n        return lock_dir(*args, **kwargs)\n\n    def test_it(self):\n        assert_raises = functools.partial(\n            self.assertRaises, errors.LockError, self._call, self.tempdir)\n        lock_path = os.path.join(self.tempdir, '.certbot.lock')\n        test_util.lock_and_call(assert_raises, lock_path)\n\n\nclass LockFileTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.lock.LockFile.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.lock import LockFile\n        return LockFile(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.lock_path = os.path.join(self.tempdir, 'test.lock')\n\n    def test_acquire_without_deletion(self):\n        # acquire the lock in another process but don't delete the file\n        child = multiprocessing.Process(target=self._call,\n                                        args=(self.lock_path,))\n        child.start()\n        child.join()\n        assert child.exitcode == 0\n        assert os.path.exists(self.lock_path)\n\n        # Test we're still able to properly acquire and release the lock\n        self.test_removed()\n\n    def test_contention(self):\n        assert_raises = functools.partial(\n            self.assertRaises, errors.LockError, self._call, self.lock_path)\n        test_util.lock_and_call(assert_raises, self.lock_path)\n\n    def test_locked_repr(self):\n        lock_file = self._call(self.lock_path)\n        try:\n            locked_repr = repr(lock_file)\n            self._test_repr_common(lock_file, locked_repr)\n            assert 'acquired' in locked_repr\n        finally:\n            lock_file.release()\n\n    def test_released_repr(self):\n        lock_file = self._call(self.lock_path)\n        lock_file.release()\n        released_repr = repr(lock_file)\n        self._test_repr_common(lock_file, released_repr)\n        assert 'released' in released_repr\n\n    def _test_repr_common(self, lock_file, lock_repr):\n        assert lock_file.__class__.__name__ in lock_repr\n        assert self.lock_path in lock_repr\n\n    @test_util.skip_on_windows(\n        'Race conditions on lock are specific to the non-blocking file access approach on Linux.')\n    def test_race(self):\n        should_delete = [True, False]\n        # Normally os module should not be imported in certbot codebase except in certbot.compat\n        # for the sake of compatibility over Windows and Linux.\n        # We make an exception here, since test_race is a test function called only on Linux.\n        from os import stat  # pylint: disable=os-module-forbidden\n\n        def delete_and_stat(path):\n            \"\"\"Wrap os.stat and maybe delete the file first.\"\"\"\n            if path == self.lock_path and should_delete.pop(0):\n                os.remove(path)\n            return stat(path)\n\n        with mock.patch('certbot._internal.lock.filesystem.os.stat') as mock_stat:\n            mock_stat.side_effect = delete_and_stat\n            self._call(self.lock_path)\n        assert len(should_delete) == 0\n\n    def test_removed(self):\n        lock_file = self._call(self.lock_path)\n        lock_file.release()\n        assert not os.path.exists(self.lock_path)\n\n    def test_unexpected_lockf_or_locking_err(self):\n        if POSIX_MODE:\n            mocked_function = 'certbot._internal.lock.fcntl.lockf'\n        else:\n            mocked_function = 'certbot._internal.lock.msvcrt.locking'\n        msg = 'hi there'\n        with mock.patch(mocked_function) as mock_lock:\n            mock_lock.side_effect = OSError(msg)\n            try:\n                self._call(self.lock_path)\n            except OSError as err:\n                assert msg in str(err)\n            else:  # pragma: no cover\n                self.fail('IOError not raised')\n\n    def test_unexpected_os_err(self):\n        if POSIX_MODE:\n            mock_function = 'certbot._internal.lock.filesystem.os.stat'\n        else:\n            mock_function = 'certbot._internal.lock.msvcrt.locking'\n        # The only expected errno are ENOENT and EACCES in lock module.\n        msg = 'hi there'\n        with mock.patch(mock_function) as mock_os:\n            mock_os.side_effect = OSError(msg)\n            try:\n                self._call(self.lock_path)\n            except OSError as err:\n                assert msg in str(err)\n            else:  # pragma: no cover\n                self.fail('OSError not raised')\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/log_test.py",
    "content": "\"\"\"Tests for certbot._internal.log.\"\"\"\nimport io\nimport logging\nimport logging.handlers\nimport sys\nimport time\nfrom typing import Optional\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import messages\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\n\nclass PreArgParseSetupTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.pre_arg_parse_setup.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):  # pylint: disable=unused-argument\n        from certbot._internal.log import pre_arg_parse_setup\n        return pre_arg_parse_setup()\n\n    def tearDown(self):\n        # We need to call logging.shutdown() at the end of this test to\n        # properly clean up any resources created by pre_arg_parse_setup.\n        logging.shutdown()\n        super().tearDown()\n\n    @mock.patch('certbot._internal.log.sys')\n    @mock.patch('certbot._internal.log.pre_arg_parse_except_hook')\n    @mock.patch('certbot._internal.log.logging.getLogger')\n    @mock.patch('certbot._internal.log.util.atexit_register')\n    def test_it(self, mock_register, mock_get, mock_except_hook, mock_sys):\n        mock_sys.argv = ['--debug']\n        mock_sys.version_info = sys.version_info\n        self._call()\n\n        mock_root_logger = mock_get()\n        mock_root_logger.setLevel.assert_called_once_with(logging.DEBUG)\n        assert mock_root_logger.addHandler.call_count == 2\n\n        memory_handler: Optional[logging.handlers.MemoryHandler] = None\n        for call in mock_root_logger.addHandler.call_args_list:\n            handler = call[0][0]\n            if memory_handler is None and isinstance(handler, logging.handlers.MemoryHandler):\n                memory_handler = handler\n                target = memory_handler.target\n            else:\n                assert isinstance(handler, logging.StreamHandler)\n        assert isinstance(target, logging.StreamHandler)\n\n        mock_register.assert_called_once_with(logging.shutdown)\n        mock_sys.excepthook(1, 2, 3)\n        mock_except_hook.assert_called_once_with(\n            memory_handler, 1, 2, 3, debug=True, quiet=False, log_path=mock.ANY)\n\n\nclass PostArgParseSetupTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.log.post_arg_parse_setup.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.log import post_arg_parse_setup\n        return post_arg_parse_setup(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.config.debug = False\n        self.config.max_log_backups = 1000\n        self.config.quiet = False\n        self.config.verbose_count = constants.CLI_DEFAULTS['verbose_count']\n        self.devnull = open(os.devnull, 'w')\n\n        from certbot._internal.log import ColoredStreamHandler\n        self.stream_handler = ColoredStreamHandler(io.StringIO())\n        from certbot._internal.log import MemoryHandler\n        from certbot._internal.log import TempHandler\n        self.temp_handler = TempHandler()\n        self.temp_path = self.temp_handler.path\n        self.memory_handler = MemoryHandler(self.temp_handler)\n        self.root_logger = mock.MagicMock(\n            handlers=[self.memory_handler, self.stream_handler])\n\n    def tearDown(self):\n        self.memory_handler.close()\n        self.stream_handler.close()\n        self.temp_handler.close()\n        self.devnull.close()\n        super().tearDown()\n\n    def test_common(self):\n        with mock.patch('certbot._internal.log.logging.getLogger') as mock_get_logger:\n            mock_get_logger.return_value = self.root_logger\n            except_hook_path = 'certbot._internal.log.post_arg_parse_except_hook'\n            with mock.patch(except_hook_path) as mock_except_hook:\n                with mock.patch('certbot._internal.log.sys') as mock_sys:\n                    mock_sys.version_info = sys.version_info\n                    self._call(self.config)\n\n        log_path = os.path.join(self.config.logs_dir, 'letsencrypt.log')\n\n        self.root_logger.removeHandler.assert_called_once_with(\n            self.memory_handler)\n        assert self.root_logger.addHandler.called\n        assert os.path.exists(log_path)\n        assert not os.path.exists(self.temp_path)\n        mock_sys.excepthook(1, 2, 3)\n        mock_except_hook.assert_called_once_with(\n            1, 2, 3, debug=self.config.debug,\n            quiet=self.config.quiet, log_path=log_path)\n\n        level = self.stream_handler.level\n        if self.config.quiet:\n            assert level == constants.QUIET_LOGGING_LEVEL\n        else:\n            assert level == constants.DEFAULT_LOGGING_LEVEL\n\n    def test_debug(self):\n        self.config.debug = True\n        self.test_common()\n\n    def test_quiet(self):\n        self.config.quiet = True\n        self.test_common()\n\n\nclass SetupLogFileHandlerTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.log.setup_log_file_handler.\"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.log import setup_log_file_handler\n        return setup_log_file_handler(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        self.config.max_log_backups = 42\n\n    @mock.patch('certbot._internal.main.logging.handlers.RotatingFileHandler')\n    def test_failure(self, mock_handler):\n        mock_handler.side_effect = IOError\n\n        try:\n            self._call(self.config, 'test.log', '%(message)s')\n        except errors.Error as err:\n            assert '--logs-dir' in str(err)\n        else:  # pragma: no cover\n            self.fail('Error not raised.')\n\n    def test_success_with_rollover(self):\n        self._test_success_common(should_rollover=True)\n\n    def test_success_without_rollover(self):\n        self.config.max_log_backups = 0\n        self._test_success_common(should_rollover=False)\n\n    def _test_success_common(self, should_rollover):\n        log_file = 'test.log'\n        handler, log_path = self._call(self.config, log_file, '%(message)s')\n        handler.close()\n\n        assert handler.level == logging.DEBUG\n        assert handler.formatter.converter == time.localtime\n\n        expected_path = os.path.join(self.config.logs_dir, log_file)\n        assert log_path == expected_path\n\n        backup_path = os.path.join(self.config.logs_dir, log_file + '.1')\n        assert os.path.exists(backup_path) == should_rollover\n\n    @mock.patch('certbot._internal.log.logging.handlers.RotatingFileHandler')\n    def test_max_log_backups_used(self, mock_handler):\n        self._call(self.config, 'test.log', '%(message)s')\n        backup_count = mock_handler.call_args[1]['backupCount']\n        assert self.config.max_log_backups == backup_count\n\n\nclass ColoredStreamHandlerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.ColoredStreamHandler\"\"\"\n\n    def setUp(self):\n        self.stream = io.StringIO()\n        self.stream.isatty = lambda: True\n        self.logger = logging.getLogger()\n        self.logger.setLevel(logging.DEBUG)\n\n        from certbot._internal.log import ColoredStreamHandler\n        self.handler = ColoredStreamHandler(self.stream)\n        self.logger.addHandler(self.handler)\n\n    def tearDown(self):\n        self.handler.close()\n\n    def test_format(self):\n        msg = 'I did a thing'\n        self.logger.debug(msg)\n        assert self.stream.getvalue() == '{0}\\n'.format(msg)\n\n    def test_format_and_red_level(self):\n        msg = 'I did another thing'\n        self.handler.red_level = logging.DEBUG\n        self.logger.debug(msg)\n\n        assert self.stream.getvalue() == \\\n                         '{0}{1}{2}\\n'.format(util.ANSI_SGR_RED,\n                                              msg,\n                                              util.ANSI_SGR_RESET)\n\n\nclass MemoryHandlerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.MemoryHandler\"\"\"\n    def setUp(self):\n        self.logger = logging.getLogger(__name__)\n        self.logger.setLevel(logging.DEBUG)\n        self.msg = 'hi there'\n        self.stream = io.StringIO()\n\n        self.stream_handler = logging.StreamHandler(self.stream)\n        from certbot._internal.log import MemoryHandler\n        self.handler = MemoryHandler(self.stream_handler)\n        self.logger.addHandler(self.handler)\n\n    def tearDown(self):\n        self.handler.close()\n        self.stream_handler.close()\n\n    def test_flush(self):\n        self._test_log_debug()\n        self.handler.flush(force=True)\n        assert self.stream.getvalue() == self.msg + '\\n'\n\n    def test_not_flushed(self):\n        # By default, logging.ERROR messages and higher are flushed\n        self.logger.critical(self.msg)\n        self.handler.flush()\n        assert self.stream.getvalue() == ''\n\n    def test_target_reset(self):\n        self._test_log_debug()\n\n        new_stream = io.StringIO()\n        new_stream_handler = logging.StreamHandler(new_stream)\n        self.handler.setTarget(new_stream_handler)\n        self.handler.flush(force=True)\n        assert self.stream.getvalue() == ''\n        assert new_stream.getvalue() == self.msg + '\\n'\n        new_stream_handler.close()\n\n    def _test_log_debug(self):\n        self.logger.debug(self.msg)\n\n\nclass TempHandlerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.TempHandler.\"\"\"\n    def setUp(self):\n        self.closed = False\n        from certbot._internal.log import TempHandler\n        self.handler = TempHandler()\n\n    def tearDown(self):\n        self.handler.close()\n\n    def test_permissions(self):\n        assert filesystem.check_permissions(self.handler.path, 0o600)\n\n    def test_delete(self):\n        self.handler.close()\n        assert not os.path.exists(self.handler.path)\n\n    def test_no_delete(self):\n        self.handler.emit(mock.MagicMock())\n        self.handler.close()\n        assert os.path.exists(self.handler.path)\n        os.remove(self.handler.path)\n\n\nclass PreArgParseExceptHookTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.pre_arg_parse_except_hook.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.log import pre_arg_parse_except_hook\n        return pre_arg_parse_except_hook(*args, **kwargs)\n\n    @mock.patch('certbot._internal.log.post_arg_parse_except_hook')\n    def test_it(self, mock_post_arg_parse_except_hook):\n        memory_handler = mock.MagicMock()\n        args = ('some', 'args',)\n        kwargs = {'some': 'kwargs'}\n\n        self._call(memory_handler, *args, **kwargs)\n\n        mock_post_arg_parse_except_hook.assert_called_once_with(\n            *args, **kwargs)\n        memory_handler.flush.assert_called_once_with(force=True)\n\n\nclass PostArgParseExceptHookTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.log.post_arg_parse_except_hook.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.log import post_arg_parse_except_hook\n        return post_arg_parse_except_hook(*args, **kwargs)\n\n    def setUp(self):\n        self.error_msg = 'test error message'\n        self.log_path = 'foo.log'\n\n    def test_base_exception(self):\n        exc_type = BaseException\n        mock_logger, output = self._test_common(exc_type, debug=False)\n        self._assert_exception_logged(mock_logger.error, exc_type)\n        self._assert_logfile_output(output)\n\n    def test_debug(self):\n        exc_type = ValueError\n        mock_logger, output = self._test_common(exc_type, debug=True)\n        self._assert_exception_logged(mock_logger.error, exc_type)\n        self._assert_logfile_output(output)\n\n    def test_quiet(self):\n        exc_type = ValueError\n        mock_logger, output = self._test_common(exc_type, debug=True, quiet=True)\n        self._assert_exception_logged(mock_logger.error, exc_type)\n        assert 'See the logfile' not in output\n\n    def test_custom_error(self):\n        exc_type = errors.PluginError\n        mock_logger, output = self._test_common(exc_type, debug=False)\n        self._assert_exception_logged(mock_logger.debug, exc_type)\n        self._assert_quiet_output(mock_logger, output)\n\n    def test_acme_error(self):\n        # Get an arbitrary error code\n        acme_code = next(iter(messages.ERROR_CODES))\n\n        def get_acme_error(msg):\n            \"\"\"Wraps ACME errors so the constructor takes only a msg.\"\"\"\n            return messages.Error.with_code(acme_code, detail=msg)\n\n        mock_logger, output = self._test_common(get_acme_error, debug=False)\n        self._assert_exception_logged(mock_logger.debug, messages.Error)\n        self._assert_quiet_output(mock_logger, output)\n        assert messages.ERROR_PREFIX not in output\n\n    def test_other_error(self):\n        exc_type = ValueError\n        mock_logger, output = self._test_common(exc_type, debug=False)\n        self._assert_exception_logged(mock_logger.debug, exc_type)\n        self._assert_quiet_output(mock_logger, output)\n\n    def test_keyboardinterrupt(self):\n        exc_type = KeyboardInterrupt\n        mock_logger, output = self._test_common(exc_type, debug=False)\n        mock_logger.error.assert_called_once_with('Exiting due to user request.')\n\n    def _test_common(self, error_type, debug, quiet=False):\n        \"\"\"Returns the mocked logger and stderr output.\"\"\"\n        mock_err = io.StringIO()\n\n        def write_err(*args, **unused_kwargs):\n            \"\"\"Write error to mock_err.\"\"\"\n            mock_err.write(args[0])\n\n        try:\n            raise error_type(self.error_msg)\n        except BaseException:\n            exc_info = sys.exc_info()\n            with mock.patch('certbot._internal.log.logger') as mock_logger:\n                mock_logger.error.side_effect = write_err\n                with mock.patch('certbot._internal.log.sys.stderr', mock_err):\n                    try:\n                        self._call(\n                            *exc_info, debug=debug, quiet=quiet, log_path=self.log_path)\n                    except SystemExit as exit_err:\n                        mock_err.write(str(exit_err))\n                    else:  # pragma: no cover\n                        self.fail('SystemExit not raised.')\n\n        output = mock_err.getvalue()\n        return mock_logger, output\n\n    def _assert_exception_logged(self, log_func, exc_type):\n        assert log_func.called\n        call_kwargs = log_func.call_args[1]\n        assert 'exc_info' in call_kwargs\n\n        actual_exc_info = call_kwargs['exc_info']\n        expected_exc_info = (exc_type, mock.ANY, mock.ANY)\n        assert actual_exc_info == expected_exc_info\n\n    def _assert_logfile_output(self, output):\n        assert 'See the logfile' in output\n        assert self.log_path in output\n\n    def _assert_quiet_output(self, mock_logger, output):\n        assert mock_logger.exception.called is False\n        assert mock_logger.debug.called\n        assert self.error_msg in output\n\n\nclass ExitWithAdviceTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.log.exit_with_advice.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.log import exit_with_advice\n        return exit_with_advice(*args, **kwargs)\n\n    def test_log_file(self):\n        log_file = os.path.join(self.tempdir, 'test.log')\n        open(log_file, 'w').close()\n\n        err_str = self._test_common(log_file)\n        assert 'logfiles' not in err_str\n        assert log_file in err_str\n\n    def test_log_dir(self):\n        err_str = self._test_common(self.tempdir)\n        assert 'logfiles' in err_str\n        assert self.tempdir in err_str\n\n    # pylint: disable=inconsistent-return-statements\n    def _test_common(self, *args, **kwargs):\n        try:\n            self._call(*args, **kwargs)\n        except SystemExit as err:\n            return str(err)\n        self.fail('SystemExit was not raised.')  # pragma: no cover\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/main_test.py",
    "content": "# coding=utf-8\n\"\"\"Tests for certbot._internal.main.\"\"\"\n# pylint: disable=too-many-lines\nimport contextlib\nimport datetime\nfrom importlib import reload as reload_module\nimport io\nimport itertools\nimport json\nimport shutil\nimport sys\nimport tempfile\nimport traceback\nimport unittest\nfrom unittest import mock\n\nimport configobj\nfrom cryptography import x509\nimport josepy as jose\nimport pytest\n\nfrom acme.messages import Error as acme_error\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import account\nfrom certbot._internal import cli\nfrom certbot._internal import constants\nfrom certbot._internal import main\nfrom certbot._internal import updater\nfrom certbot._internal import san\nfrom certbot._internal.plugins import disco\nfrom certbot._internal.plugins import manual\nfrom certbot._internal.plugins import null\nfrom certbot._internal.plugins import standalone\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.plugins import enhancements\nimport certbot.tests.util as test_util\n\nCERT_PATH = test_util.vector_path('cert_512.pem')\nCERT = test_util.vector_path('cert_512.pem')\nCSR = test_util.vector_path('csr_512.der')\nJWK = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))\nRSA2048_KEY_PATH = test_util.vector_path('rsa2048_key.pem')\nP256_KEY_PATH = test_util.vector_path('p256_key.pem')\nSS_CERT_PATH = test_util.vector_path('cert_2048.pem')\n\n\nclass TestHandleCerts(unittest.TestCase):\n    \"\"\"Test for certbot._internal.main._handle_* methods\"\"\"\n    @mock.patch(\"certbot._internal.main._handle_unexpected_key_type_migration\")\n    def test_handle_identical_cert_request_pending(self, mock_handle_migration):\n        mock_lineage = mock.Mock()\n        mock_lineage.ensure_deployed.return_value = False\n        # pylint: disable=protected-access\n        ret = main._handle_identical_cert_request(mock.Mock(), mock_lineage)\n        assert ret == (\"reinstall\", mock_lineage)\n        assert mock_handle_migration.called\n\n    @mock.patch('certbot._internal.renewal.should_renew')\n    @mock.patch(\"certbot.display.util.menu\")\n    @mock.patch(\"certbot._internal.main._handle_unexpected_key_type_migration\")\n    def test_handle_identical_cert_key_type_change(self, mock_handle_migration, mock_menu,\n        mock_should_renew):\n        mock_handle_migration.return_value = True\n        mock_lineage = mock.Mock()\n        mock_lineage.ensure_deployed.return_value = True\n        mock_should_renew.return_value = False\n        ret = main._handle_identical_cert_request(mock.MagicMock(verb=\"run\", reinstall=False),\n                                                  mock_lineage)\n        assert mock_handle_migration.called\n        assert not mock_menu.called\n        assert ret == (\"renew\", mock_lineage)\n\n    @mock.patch(\"certbot._internal.main._handle_unexpected_key_type_migration\")\n    def test_handle_subset_cert_request(self, mock_handle_migration):\n        mock_config = mock.Mock()\n        mock_config.expand = True\n        mock_lineage = mock.Mock()\n        mock_lineage.sans.return_value = [san.DNSName(\"dummy1\"), san.DNSName(\"dummy2\")]\n        ret = main._handle_subset_cert_request(mock_config, [\"dummy1\"], mock_lineage)\n        assert ret == (\"renew\", mock_lineage)\n        assert mock_handle_migration.called\n\n    @mock.patch(\"certbot._internal.main.display_util.yesno\")\n    def test_handle_unexpected_key_type_migration(self, mock_yesno):\n        config = mock.Mock()\n        mock_set = mock.Mock()\n        config.set_by_user = mock_set\n        cert = mock.Mock()\n\n        # If the key types do not differ, it should be a no-op.\n        config.key_type = \"rsa\"\n        cert.private_key_type = \"rsa\"\n        main._handle_unexpected_key_type_migration(config, cert)\n        mock_yesno.assert_not_called()\n        assert config.key_type == cert.private_key_type\n\n        # If the user confirms the change interactively, the key change should proceed silently.\n        cert.private_key_type = \"ecdsa\"\n        mock_yesno.return_value = True\n        main._handle_unexpected_key_type_migration(config, cert)\n        assert mock_set.call_count == 2\n        assert config.key_type == \"rsa\"\n\n        # User does not interactively confirm the key type change.\n        mock_yesno.return_value = False\n\n        # If --key-type and --cert-name are both set, the key type change should proceed silently.\n        mock_set.return_value = True\n        main._handle_unexpected_key_type_migration(config, cert)\n        assert config.key_type == \"rsa\"\n\n        # If neither --key-type nor --cert-name are set, Certbot should keep the old key type.\n        mock_set.return_value = False\n        main._handle_unexpected_key_type_migration(config, cert)\n        assert config.key_type == \"ecdsa\"\n\n        # If --key-type is set and --cert-name isn't, Certbot should error.\n        config.key_type = \"rsa\"\n        mock_set.side_effect = lambda var: var != \"certname\"\n        with pytest.raises(errors.Error, match=\"Please provide both --cert-name and --key-type\"):\n            main._handle_unexpected_key_type_migration(config, cert)\n\n        # If --key-type is not set, Certbot should keep the old key type.\n        mock_set.side_effect = lambda var: var != \"key_type\"\n        main._handle_unexpected_key_type_migration(config, cert)\n        assert config.key_type == \"ecdsa\"\n\n\nclass RunTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.run.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.domain = 'example.org'\n        patches = [\n            mock.patch('certbot._internal.main._get_and_save_cert'),\n            mock.patch('certbot._internal.main.display_ops.success_installation'),\n            mock.patch('certbot._internal.main.display_ops.success_renewal'),\n            mock.patch('certbot._internal.main._init_le_client'),\n            mock.patch('certbot._internal.main._suggest_donation_if_appropriate'),\n            mock.patch('certbot._internal.main._report_new_cert'),\n            mock.patch('certbot._internal.main._find_cert'),\n            mock.patch('certbot._internal.eff.handle_subscription'),\n            mock.patch('certbot._internal.main._report_next_steps')\n        ]\n\n        self.mock_auth = patches[0].start()\n        self.mock_success_installation = patches[1].start()\n        self.mock_success_renewal = patches[2].start()\n        self.mock_init = patches[3].start()\n        self.mock_suggest_donation = patches[4].start()\n        self.mock_report_cert = patches[5].start()\n        self.mock_find_cert = patches[6].start()\n        self.mock_subscription = patches[7].start()\n        self.mock_report_next_steps = patches[8].start()\n        for patch in patches:\n            self.addCleanup(patch.stop)\n\n    def _call(self):\n        args = '-a webroot -i null -d {0}'.format(self.domain).split()\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n\n        from certbot._internal.main import run\n        run(config, plugins)\n\n    def test_newcert_success(self):\n        self.mock_auth.return_value = mock.Mock()\n        self.mock_find_cert.return_value = True, None\n        self._call()\n        self.mock_success_installation.assert_called_once_with([self.domain])\n        self.mock_report_next_steps.assert_called_once_with(mock.ANY, None, mock.ANY,\n            new_or_renewed_cert=True)\n\n    def test_reinstall_success(self):\n        self.mock_auth.return_value = mock.Mock()\n        self.mock_find_cert.return_value = False, mock.Mock()\n        self._call()\n        self.mock_success_installation.assert_called_once_with([self.domain])\n\n    def test_renewal_success(self):\n        self.mock_auth.return_value = mock.Mock()\n        self.mock_find_cert.return_value = True, mock.Mock()\n        self._call()\n        self.mock_success_renewal.assert_called_once_with([self.domain])\n\n    @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins')\n    def test_run_enhancement_not_supported(self, mock_choose):\n        mock_choose.return_value = (null.Installer(self.config, \"null\"), None)\n        plugins = disco.PluginsRegistry.find_all()\n        self.config.auto_hsts = True\n        with pytest.raises(errors.NotSupportedError):\n            main.run(self.config, plugins)\n\n    @mock.patch('certbot._internal.main._install_cert')\n    def test_cert_success_install_error(self, mock_install_cert):\n        mock_install_cert.side_effect = errors.PluginError(\"Fake installation error\")\n        self.mock_auth.return_value = mock.Mock()\n        self.mock_find_cert.return_value = True, None\n        with pytest.raises(errors.PluginError):\n            self._call()\n\n        # Next steps should contain both renewal advice and installation error\n        self.mock_report_next_steps.assert_called_once_with(\n            mock.ANY, mock_install_cert.side_effect, mock.ANY, new_or_renewed_cert=True)\n        # The final success message shouldn't be shown\n        self.mock_success_installation.assert_not_called()\n\n    @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins')\n    def test_run_must_staple_not_supported(self, mock_choose):\n        mock_choose.return_value = (null.Installer(self.config, \"null\"), None)\n        plugins = disco.PluginsRegistry.find_all()\n        self.config.must_staple = True\n        with pytest.raises(errors.NotSupportedError):\n            main.run(self.config, plugins)\n\n\nclass CertonlyTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.main.certonly.\"\"\"\n\n    def setUp(self):\n        self.get_utility_patch = test_util.patch_display_util()\n        self.mock_get_utility = self.get_utility_patch.start()\n\n    def tearDown(self):\n        self.get_utility_patch.stop()\n\n    def _call(self, args):\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n\n        with mock.patch('certbot._internal.main._init_le_client') as mock_init:\n            with mock.patch('certbot._internal.main._suggest_donation_if_appropriate'):\n                with mock.patch('certbot._internal.eff.handle_subscription'):\n                    main.certonly(config, plugins)\n\n        return mock_init()  # returns the client\n\n    @mock.patch('certbot._internal.main._find_cert')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    def test_no_reinstall_text_pause(self, unused_report, mock_auth, mock_find_cert):\n        mock_notification = self.mock_get_utility().notification\n        mock_notification.side_effect = self._assert_no_pause\n        mock_auth.return_value = mock.Mock()\n        mock_find_cert.return_value = False, None\n        self._call('certonly --webroot -d example.com'.split())\n\n    def _assert_no_pause(self, *args, **kwargs):  # pylint: disable=unused-argument\n        assert kwargs.get(\"pause\") is False\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.cert_manager.sans_for_certname')\n    @mock.patch('certbot._internal.renewal.renew_cert')\n    @mock.patch('certbot._internal.main._handle_unexpected_key_type_migration')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    def test_find_lineage_for_sans_and_certname(self, mock_report_cert,\n                                                mock_handle_type, mock_renew_cert, mock_sans_for_certname, mock_lineage, mock_report_next_steps):\n        domains = [san.DNSName('example.com'), san.DNSName('test.org')]\n        mock_sans_for_certname.return_value = domains\n        mock_lineage.sans.return_value = domains\n        self._call(('certonly --webroot -d example.com -d test.org '\n            '--cert-name example.com --no-directory-hooks').split())\n\n        assert mock_lineage.call_count == 1\n        assert mock_sans_for_certname.call_count == 1\n        assert mock_renew_cert.call_count == 1\n        assert mock_report_cert.call_count == 1\n        assert mock_handle_type.call_count == 1\n        mock_report_next_steps.assert_called_once_with(\n            mock.ANY, None, mock.ANY, new_or_renewed_cert=True)\n\n        # user confirms updating lineage with new domains\n        self._call(('certonly --webroot -d example.com -d test.com '\n            '--cert-name example.com --no-directory-hooks').split())\n        assert mock_lineage.call_count == 2\n        assert mock_sans_for_certname.call_count == 2\n        assert mock_renew_cert.call_count == 2\n        assert mock_report_cert.call_count == 2\n        assert mock_handle_type.call_count == 2\n\n        # error in _ask_user_to_confirm_new_names\n        self.mock_get_utility().yesno.return_value = False\n        with pytest.raises(errors.ConfigurationError):\n            self._call('certonly --webroot -d example.com -d test.com --cert-name example.com'\n                ' --no-directory-hooks'.split())\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.cert_manager.sans_for_certname')\n    @mock.patch('certbot.display.ops.choose_names')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    def test_find_lineage_for_sans_new_certname(self, mock_report_cert,\n                                                mock_lineage, mock_choose_names, mock_sans_for_certname, unused_mock_report_next_steps):\n        mock_lineage.return_value = None\n\n        # no lineage with this name but we specified domains so create a new cert\n        self._call(('certonly --webroot -d example.com -d test.com '\n            '--cert-name example.com --no-directory-hooks').split())\n        assert mock_lineage.call_count == 1\n        assert mock_report_cert.call_count == 1\n\n        # no lineage with this name and we didn't give domains\n        mock_choose_names.return_value = [\"somename\"]\n        mock_sans_for_certname.return_value = None\n        self._call(('certonly --webroot --cert-name example.com --no-directory-hooks').split())\n        assert mock_choose_names.called is True\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    @mock.patch('certbot._internal.main._csr_get_and_save_cert')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    def test_dryrun_next_steps_no_cert_saved(self, mock_lineage, mock_csr_get_cert,\n                                             unused_mock_get_cert, mock_report_next_steps):\n        \"\"\"certonly --dry-run shouldn't report creation of a certificate in NEXT STEPS.\"\"\"\n        mock_lineage.return_value = None\n        mock_csr_get_cert.return_value = (\"/cert\", \"/chain\", \"/fullchain\")\n        for flag in (f\"--csr {CSR}\", \"-d example.com\"):\n            self._call(f\"certonly {flag} --webroot --cert-name example.com --dry-run\".split())\n            mock_report_next_steps.assert_called_once_with(\n                mock.ANY, mock.ANY, mock.ANY, new_or_renewed_cert=False)\n            mock_report_next_steps.reset_mock()\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main._find_cert')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    def test_installer_runs_restart(self, mock_sel, mock_get_cert, mock_find_cert,\n                                    unused_report_new, unused_report_next):\n        mock_installer = mock.MagicMock()\n        mock_sel.return_value = (mock_installer, None)\n        mock_get_cert.return_value = mock.MagicMock()\n        mock_find_cert.return_value = (True, None)\n\n        self._call('certonly --nginx -d example.com'.split())\n        mock_installer.restart.assert_called_once()\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main._find_cert')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    def test_dryrun_installer_doesnt_restart(self, mock_sel, mock_get_cert, mock_find_cert,\n                                             unused_report_new, unused_report_next):\n        mock_installer = mock.MagicMock()\n        mock_sel.return_value = (mock_installer, None)\n        mock_get_cert.return_value = mock.MagicMock()\n        mock_find_cert.return_value = (True, None)\n\n        self._call('certonly --nginx -d example.com --dry-run'.split())\n        mock_installer.restart.assert_not_called()\n\n    @mock.patch('certbot._internal.main._report_next_steps')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main._find_cert')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    def test_invalid_installer(self, mock_get_cert, mock_find_cert,\n                               unused_report_new, unused_report_next):\n        mock_get_cert.return_value = mock.MagicMock()\n        mock_find_cert.return_value = (True, None)\n        self._call((f'certonly --webroot -w {tempfile.gettempdir()} ' +\n                    '-i standalone -d example.com').split())\n\n\nclass FindSansOrCertnameTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.main._find_sans_or_certname.\"\"\"\n\n    @mock.patch('certbot.display.ops.choose_names')\n    def test_display_ops(self, mock_choose_names):\n        mock_config = mock.Mock(domains=None, ip_addresses=None, certname=None)\n        mock_choose_names.return_value = [\"example.com\"]\n        # pylint: disable=protected-access\n        assert main._find_sans_or_certname(mock_config, None) == ([san.DNSName(\"example.com\")], None)\n\n    @mock.patch('certbot.display.ops.choose_names')\n    def test_no_results(self, mock_choose_names):\n        mock_config = mock.Mock(domains=None, ip_addresses=None, certname=None)\n        mock_choose_names.return_value = []\n        # pylint: disable=protected-access\n        with pytest.raises(errors.Error):\n            main._find_sans_or_certname(mock_config, None)\n\n    @mock.patch('certbot._internal.cert_manager.sans_for_certname')\n    def test_grab_domains(self, mock_domains):\n        mock_config = mock.Mock(domains=None, ip_addresses=None, certname=\"one.com\")\n        mock_domains.return_value = [\"one.com\", \"two.com\"]\n        # pylint: disable=protected-access\n        assert main._find_sans_or_certname(mock_config, None) == \\\n               ([\"one.com\", \"two.com\"], \"one.com\")\n\n\nclass RevokeTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.main.revoke.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        shutil.copy(CERT_PATH, self.tempdir)\n        self.tmp_cert_path = os.path.abspath(os.path.join(self.tempdir, 'cert_512.pem'))\n\n        patches = [\n            mock.patch('certbot._internal.client.acme_client'),\n            mock.patch('certbot._internal.client.Client'),\n            mock.patch('certbot._internal.main._determine_account'),\n            mock.patch('certbot._internal.main.display_ops.success_revocation')\n        ]\n        self.mock_acme_client = patches[0].start().ClientV2\n        patches[1].start()\n        self.mock_determine_account = patches[2].start()\n        self.mock_success_revoke = patches[3].start()\n        for patch in patches:\n            self.addCleanup(patch.stop)\n\n        from certbot._internal.account import Account\n\n        self.regr = mock.MagicMock()\n        self.meta = Account.Meta(\n            creation_host=\"test.certbot.org\",\n            creation_dt=datetime.datetime(\n                2015, 7, 4, 14, 4, 10, tzinfo=datetime.timezone.utc))\n        self.acc = Account(self.regr, JWK, self.meta)\n\n        self.mock_determine_account.return_value = (self.acc, None)\n\n    def _call(self, args=None):\n        if not args:\n            args = 'revoke --cert-path={0} '\n            args = args.format(self.tmp_cert_path).split()\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n\n        from certbot._internal.main import revoke\n        revoke(config, plugins)\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.main.client.acme_client')\n    def test_revoke_with_reason(self, mock_acme_client,\n            mock_delete_if_appropriate):\n        mock_delete_if_appropriate.return_value = False\n        mock_revoke = mock_acme_client.ClientV2().revoke\n        expected = []\n        for reason, code in constants.REVOCATION_REASONS.items():\n            args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path, reason).split()\n            self._call(args)\n            expected.append(mock.call(mock.ANY, code))\n            args = 'revoke --cert-path={0} --reason {1}'.format(self.tmp_cert_path,\n                    reason.upper()).split()\n            self._call(args)\n            expected.append(mock.call(mock.ANY, code))\n        assert expected == mock_revoke.call_args_list\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.client.create_acme_client')\n    def test_revoke_by_certname(self, mock_create_acme,\n                                unused_mock_renewal_file_for_certname, mock_cert,\n                                mock_delete_if_appropriate):\n        mock_create_acme.return_value = self.mock_acme_client\n        mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path,\n                                                server=\"https://acme.example\")\n        args = 'revoke --cert-name=example.com'.split()\n        mock_delete_if_appropriate.return_value = False\n        self._call(args)\n        assert mock_create_acme.call_args_list[0][0][0].server == \\\n                         'https://acme.example'\n        self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.client.create_acme_client')\n    def test_revoke_by_certname_and_server(self, mock_create_acme,\n                                           unused_mock_renewal_file_for_certname, mock_cert,\n                                           mock_delete_if_appropriate):\n        \"\"\"Revoking with --server should use the server from the CLI\"\"\"\n        mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path,\n                                                server=\"https://acme.example\")\n        args = 'revoke --cert-name=example.com --server https://other.example'.split()\n        mock_delete_if_appropriate.return_value = False\n        self._call(args)\n        assert mock_create_acme.call_args_list[0][0][0].server == \\\n                         'https://other.example'\n        self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.storage.RenewableCert')\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.client.create_acme_client')\n    def test_revoke_by_certname_empty_server(self, mock_create_acme,\n                                             unused_mock_renewal_file_for_certname,\n                                             mock_cert, mock_delete_if_appropriate):\n        \"\"\"Revoking with --cert-name where the lineage server is empty shouldn't crash \"\"\"\n        mock_cert.return_value = mock.MagicMock(cert_path=self.tmp_cert_path, server=None)\n        args = 'revoke --cert-name=example.com'.split()\n        mock_delete_if_appropriate.return_value = False\n        self._call(args)\n        assert mock_create_acme.call_args_list[0][0][0].server == \\\n                         constants.CLI_DEFAULTS['server']\n        self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    def test_revocation_success(self, mock_delete_if_appropriate):\n        self._call()\n        mock_delete_if_appropriate.return_value = False\n        self.mock_success_revoke.assert_called_once_with(self.tmp_cert_path)\n\n    def test_revocation_error(self):\n        from acme import errors as acme_errors\n        self.mock_acme_client.side_effect = acme_errors.ClientError()\n        with pytest.raises(acme_errors.ClientError):\n            self._call()\n        self.mock_success_revoke.assert_not_called()\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.cert_manager.delete')\n    @test_util.patch_display_util()\n    def test_revocation_with_prompt(self, mock_get_utility,\n            mock_delete, mock_delete_if_appropriate):\n        mock_get_utility().yesno.return_value = False\n        mock_delete_if_appropriate.return_value = False\n        self._call()\n        assert mock_delete.called is False\n\n\nclass ReconfigureTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.main.reconfigure\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.get_utility_patch = test_util.patch_display_util()\n        self.mock_get_utility = self.get_utility_patch.start()\n        self.patchers = {\n            'check_symlinks': mock.patch('certbot._internal.storage.RenewableCert._check_symlinks'),\n            'cert_sans': mock.patch('certbot._internal.storage.RenewableCert.sans'),\n            'pick_installer': mock.patch('certbot._internal.plugins.selection.pick_installer'),\n            'pick_auth': mock.patch('certbot._internal.plugins.selection.pick_authenticator'),\n            'find_init': mock.patch('certbot._internal.plugins.disco.PluginsRegistry.find_init'),\n            '_get_and_save_cert': mock.patch('certbot._internal.main._get_and_save_cert'),\n            '_init_le_client': mock.patch('certbot._internal.main._init_le_client'),\n            'list_hooks': mock.patch('certbot._internal.hooks.list_hooks'),\n        }\n        self.mocks = {k: v.start() for k, v in self.patchers.items()}\n        self.mocks['cert_sans'].return_value = [san.DNSName('example.com')]\n\n        self.config_dir = os.path.join(self.tempdir, 'config')\n        renewal_configs_dir = os.path.join(self.config_dir, 'renewal')\n        if not os.path.exists(renewal_configs_dir):\n            filesystem.makedirs(renewal_configs_dir)\n        self.renewal_file = os.path.join(renewal_configs_dir, 'example.com.conf')\n        original_config = \"\"\"\n            version = 1.32.0\n            archive_dir = /etc/letsencrypt/archive/example.com\n            cert = /etc/letsencrypt/live/example.com/cert.pem\n            privkey = /etc/letsencrypt/live/example.com/privkey.pem\n            chain = /etc/letsencrypt/live/example.com/chain.pem\n            fullchain = /etc/letsencrypt/live/example.com/fullchain.pem\n\n            # Options used in the renewal process\n            [renewalparams]\n            account = ee43634db0aa4e6804f152be39990e6a\n            server = https://acme-v02.api.letsencrypt.org/directory\n            authenticator = nginx\n            installer = nginx\n            key_type = rsa\n        \"\"\"\n        with open(self.renewal_file, 'w') as f:\n            f.write(original_config)\n        with open(self.renewal_file, 'r') as f:\n            self.original_config = configobj.ConfigObj(f,\n                encoding='utf-8', default_encoding='utf-8')\n\n\n    def tearDown(self):\n        super().tearDown()\n        self.get_utility_patch.stop()\n        for patch in self.patchers.values():\n            patch.stop()\n\n    def _call(self, passed_args):\n        full_args = passed_args + ['--config-dir', self.config_dir]\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, full_args)\n\n        from certbot._internal.main import reconfigure\n        reconfigure(config, plugins)\n\n        with open(self.renewal_file, 'r') as f:\n            updated_conf = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')\n\n        return updated_conf\n\n    def test_domains_set(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call('--cert-name cert1 -d one.cert.com'.split())\n\n    @mock.patch('certbot._internal.cert_manager.get_certnames')\n    def test_asks_for_certname(self, mock_cert_manager):\n        named_mock = mock.Mock()\n        named_mock.name = 'nginx'\n\n        self.mocks['pick_installer'].return_value = named_mock\n        self.mocks['pick_auth'].return_value = named_mock\n        self.mocks['find_init'].return_value = named_mock\n        mock_cert_manager.return_value = ['example.com']\n        self._call('--nginx'.split())\n        assert mock_cert_manager.call_count == 1\n\n    def test_update_configurator(self):\n        named_mock = mock.Mock()\n        named_mock.name = 'apache'\n\n        self.mocks['pick_installer'].return_value = named_mock\n        self.mocks['pick_auth'].return_value = named_mock\n        self.mocks['find_init'].return_value = named_mock\n\n        new_config = self._call('--cert-name example.com --apache'.split())\n        assert new_config['renewalparams']['authenticator'] == 'apache'\n\n    def test_only_intended_changes(self):\n        \"\"\" Check that we don't accidentally modify anything that we didn't mean to \"\"\"\n        named_mock = mock.Mock()\n        named_mock.name = 'apache'\n\n        self.mocks['pick_installer'].return_value = named_mock\n        self.mocks['pick_auth'].return_value = named_mock\n        self.mocks['find_init'].return_value = named_mock\n\n        new_config = self._call('--cert-name example.com --apache'.split())\n        # Undo the changes we made in calling and in testing\n        new_config['renewalparams']['authenticator'] = 'nginx'\n        new_config['renewalparams']['installer'] = 'nginx'\n        del new_config['renewalparams']['config_dir']\n        new_config['version'] = self.original_config['version']\n\n        assert new_config == self.original_config\n\n    @mock.patch('certbot._internal.hooks.validate_hooks')\n    def test_staging_used(self, unused_validate_hooks):\n        \"\"\" Check that we use the staging server for the dry run \"\"\"\n        assert self.original_config['renewalparams']['server'] == \\\n            'https://acme-v02.api.letsencrypt.org/directory'\n\n        self._call('--cert-name example.com --pre-hook'.split() + ['echo pre'])\n\n        assert 'staging' in self.mocks['_init_le_client'].call_args.args[0].server\n        assert 'staging' in self.mocks['_get_and_save_cert'].call_args.args[1].server\n\n    def test_new_account_or_server_errors(self):\n        \"\"\" Check that we error when attempting to change the account id or server,\n            but not when it's the same\n        \"\"\"\n        orig_account_id = self.original_config['renewalparams']['account']\n        orig_server = self.original_config['renewalparams']['server']\n\n        # new account\n        try:\n            self._call('--cert-name example.com --account newaccountid'.split())\n        except errors.ConfigurationError as err:\n            assert \"Using reconfigure to change the ACME account\" in str(err)\n\n        # check that config isn't modified\n        with open(self.renewal_file, 'r') as f:\n            new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')\n        assert new_config['renewalparams']['account'] == orig_account_id\n\n        # same account\n        new_config = self._call(f'--cert-name example.com --account {orig_account_id}'.split())\n        assert new_config['renewalparams']['account'] == orig_account_id\n\n        # new server\n        try:\n            self._call('--cert-name example.com --server x.com'.split())\n        except errors.ConfigurationError as err:\n            assert \"Using reconfigure to change the ACME account\" in str(err)\n\n        # check that config isn't modified\n        with open(self.renewal_file, 'r') as f:\n            new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')\n        assert new_config['renewalparams']['server'] == orig_server\n\n        # same server\n        new_config = self._call(f'--cert-name example.com --server {orig_server}'.split())\n        assert new_config['renewalparams']['server'] == orig_server\n\n    @mock.patch('certbot._internal.hooks.validate_hooks')\n    def test_update_hooks(self, unused_validate_hooks):\n        assert 'pre_hook' not in self.original_config\n        # test set\n        new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre'])\n        assert new_config['renewalparams']['pre_hook'] == 'echo pre'\n        # test update\n        new_config = self._call('--cert-name example.com --pre-hook'.split() + ['echo pre2'])\n        assert new_config['renewalparams']['pre_hook'] == 'echo pre2'\n\n        # test deploy hook is set even though we did a dry run\n        assert 'renew_hook' not in self.original_config\n        new_config = self._call('--cert-name example.com --deploy-hook'.split() + ['echo deploy'])\n        assert new_config['renewalparams']['renew_hook'] == 'echo deploy'\n\n    def test_dry_run_fails(self):\n        # set side effect of raising error\n        self.mocks['_get_and_save_cert'].side_effect = errors.Error\n\n        try:\n            self._call('--cert-name example.com --apache'.split())\n        except errors.Error:\n            pass\n\n        # check that config isn't modified\n        with open(self.renewal_file, 'r') as f:\n            new_config = configobj.ConfigObj(f, encoding='utf-8', default_encoding='utf-8')\n        assert new_config['renewalparams']['authenticator'] == 'nginx'\n\n    @mock.patch('certbot._internal.main.display_util.notify')\n    def test_report_results(self, mock_notify):\n        # make sure report results works when config has a webroot map\n        original_config = \"\"\"\n            version = 2.0.0\n            archive_dir = /etc/letsencrypt/archive/example.com\n            cert = /etc/letsencrypt/live/example.com/cert.pem\n            privkey = /etc/letsencrypt/live/example.com/privkey.pem\n            chain = /etc/letsencrypt/live/example.com/chain.pem\n            fullchain = /etc/letsencrypt/live/example.com/fullchain.pem\n\n            # Options used in the renewal process\n            [renewalparams]\n            account = ee43634db0aa4e6804f152be39990e6a\n            server = https://acme-staging-v02.api.letsencrypt.org/directory\n            authenticator = webroot\n            installer = nginx\n            key_type = ecdsa\n            webroot_path = /var/www/html,\n            [[webroot_map]]\n            example.com = /var/www/html\n        \"\"\"\n        with open(self.renewal_file, 'w') as f:\n            f.write(original_config)\n        with open(self.renewal_file, 'r') as f:\n            self.original_config = configobj.ConfigObj(f,\n                encoding='utf-8', default_encoding='utf-8')\n\n        named_mock = mock.Mock()\n        named_mock.name = 'nginx'\n\n        self.mocks['pick_auth'].return_value = named_mock\n        self.mocks['find_init'].return_value = named_mock\n\n        new_config = self._call('--cert-name example.com --nginx'.split())\n        assert new_config['renewalparams']['authenticator'] == 'nginx'\n        mock_notify.assert_called_with(\n            '\\nSuccessfully updated configuration.'+\n            '\\nChanges will apply when the certificate renews.')\n\n\nclass DeleteIfAppropriateTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main._delete_if_appropriate \"\"\"\n\n    def _call(self, mock_config):\n        from certbot._internal.main import _delete_if_appropriate\n        _delete_if_appropriate(mock_config)\n\n    def _test_delete_opt_out_common(self):\n        with mock.patch('certbot._internal.cert_manager.delete') as mock_delete:\n            self._call(self.config)\n        mock_delete.assert_not_called()\n\n    @test_util.patch_display_util()\n    def test_delete_flag_opt_out(self, unused_mock_get_utility):\n        self.config.delete_after_revoke = False\n        self._test_delete_opt_out_common()\n\n    @test_util.patch_display_util()\n    def test_delete_prompt_opt_out(self, mock_get_utility):\n        util_mock = mock_get_utility()\n        util_mock.yesno.return_value = False\n        self._test_delete_opt_out_common()\n\n    @mock.patch(\"certbot._internal.main.logger.warning\")\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.cert_manager.delete')\n    @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps')\n    @mock.patch('certbot._internal.storage.full_archive_path')\n    @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')\n    @test_util.patch_display_util()\n    def test_overlapping_archive_dirs(self, mock_get_utility,\n            mock_cert_path_to_lineage, mock_archive,\n            mock_match_and_check_overlaps, mock_delete,\n            mock_renewal_file_for_certname, mock_warning):\n        # pylint: disable = unused-argument\n        config = self.config\n        config.cert_path = \"/some/reasonable/path\"\n        config.certname = \"\"\n        mock_cert_path_to_lineage.return_value = \"example.com\"\n        mock_match_and_check_overlaps.side_effect = errors.OverlappingMatchFound()\n        self._call(config)\n        mock_delete.assert_not_called()\n        assert mock_warning.call_count == 1\n\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps')\n    @mock.patch('certbot._internal.storage.full_archive_path')\n    @mock.patch('certbot._internal.cert_manager.delete')\n    @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')\n    @test_util.patch_display_util()\n    def test_cert_path_only(self, mock_get_utility,\n            mock_cert_path_to_lineage, mock_delete, mock_archive,\n            mock_overlapping_archive_dirs, mock_renewal_file_for_certname):\n        # pylint: disable = unused-argument\n        config = self.config\n        config.cert_path = \"/some/reasonable/path\"\n        config.certname = \"\"\n        mock_cert_path_to_lineage.return_value = \"example.com\"\n        mock_overlapping_archive_dirs.return_value = False\n        self._call(config)\n        assert mock_delete.call_count == 1\n\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps')\n    @mock.patch('certbot._internal.storage.full_archive_path')\n    @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')\n    @mock.patch('certbot._internal.cert_manager.delete')\n    @test_util.patch_display_util()\n    def test_noninteractive_deletion(self, mock_get_utility, mock_delete,\n            mock_cert_path_to_lineage, mock_full_archive_dir,\n            mock_match_and_check_overlaps, mock_renewal_file_for_certname):\n        # pylint: disable = unused-argument\n        config = self.config\n        config.noninteractive_mode = True\n        config.cert_path = \"/some/reasonable/path\"\n        config.certname = \"\"\n        mock_cert_path_to_lineage.return_value = \"example.com\"\n        mock_full_archive_dir.return_value = \"\"\n        mock_match_and_check_overlaps.return_value = \"\"\n        self._call(config)\n        assert mock_delete.call_count == 1\n\n    @mock.patch('certbot._internal.storage.renewal_file_for_certname')\n    @mock.patch('certbot._internal.cert_manager.match_and_check_overlaps')\n    @mock.patch('certbot._internal.storage.full_archive_path')\n    @mock.patch('certbot._internal.cert_manager.cert_path_to_lineage')\n    @mock.patch('certbot._internal.cert_manager.delete')\n    @test_util.patch_display_util()\n    def test_opt_in_deletion(self, mock_get_utility, mock_delete,\n            mock_cert_path_to_lineage, mock_full_archive_dir,\n            mock_match_and_check_overlaps, mock_renewal_file_for_certname):\n        config = self.config\n        config.delete_after_revoke = True\n        config.cert_path = \"/some/reasonable/path\"\n        config.certname = \"\"\n        mock_cert_path_to_lineage.return_value = \"example.com\"\n        mock_full_archive_dir.return_value = \"\"\n        mock_match_and_check_overlaps.return_value = \"\"\n        self._call(config)\n        assert mock_delete.call_count == 1\n        assert not mock_get_utility().yesno.called\n\n\nclass DetermineAccountTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main._determine_account.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.config.account = None\n        self.config.email = None\n        self.config.register_unsafely_without_email = False\n        self.accs = [mock.MagicMock(id='x'), mock.MagicMock(id='y')]\n        self.account_storage = account.AccountMemoryStorage()\n        # For use in saving accounts: fake out the new_authz URL.\n        self.mock_client = mock.MagicMock()\n        self.mock_client.directory.new_authz = \"hi\"\n\n\n    def _call(self):\n        # pylint: disable=protected-access\n        from certbot._internal.main import _determine_account\n        with mock.patch('certbot._internal.main.account.AccountFileStorage') as mock_storage, \\\n             test_util.patch_display_util():\n            mock_storage.return_value = self.account_storage\n            return _determine_account(self.config)\n\n    @mock.patch('certbot._internal.client.register')\n    @mock.patch('certbot._internal.client.display_ops.get_email')\n    def _register_error_common(self, err_msg, exception, mock_get_email, mock_register):\n        mock_get_email.return_value = 'foo@bar.baz'\n        mock_register.side_effect = exception\n        try:\n            self._call()\n        except errors.Error as err:\n            assert f\"Unable to register an account with ACME server. {err_msg}\" == \\\n                             str(err)\n\n    def test_args_account_set(self):\n        self.account_storage.save(self.accs[1], self.mock_client)\n        self.config.account = self.accs[1].id\n        assert (self.accs[1], None) == self._call()\n        assert self.accs[1].id == self.config.account\n        assert self.config.email is None\n\n    def test_single_account(self):\n        self.account_storage.save(self.accs[0], self.mock_client)\n        assert (self.accs[0], None) == self._call()\n        assert self.accs[0].id == self.config.account\n        assert self.config.email is None\n\n    @mock.patch('certbot._internal.client.display_ops.choose_account')\n    def test_multiple_accounts(self, mock_choose_accounts):\n        for acc in self.accs:\n            self.account_storage.save(acc, self.mock_client)\n        mock_choose_accounts.return_value = self.accs[1]\n        assert (self.accs[1], None) == self._call()\n        assert set(mock_choose_accounts.call_args[0][0]) == set(self.accs)\n        assert self.accs[1].id == self.config.account\n        assert self.config.email is None\n\n    @mock.patch('certbot._internal.client.display_ops.choose_account')\n    def test_multiple_accounts_canceled(self, mock_choose_accounts):\n        for acc in self.accs:\n            self.account_storage.save(acc, self.mock_client)\n        mock_choose_accounts.return_value = None\n        try:\n            self._call()\n        except errors.Error as err:\n            assert \"No account has been chosen\" in str(err)\n\n    @mock.patch('certbot._internal.client.display_ops.get_email')\n    @mock.patch('certbot._internal.main.display_util.notify')\n    def test_no_accounts_no_email(self, mock_notify, mock_get_email):\n        mock_get_email.return_value = 'foo@bar.baz'\n\n        with mock.patch('certbot._internal.main.client') as client:\n            client.register.return_value = (\n                self.accs[0], mock.sentinel.acme)\n            assert (self.accs[0], mock.sentinel.acme) == self._call()\n        client.register.assert_called_once_with(\n            self.config, self.account_storage, tos_cb=mock.ANY)\n\n        assert self.accs[0].id == self.config.account\n        assert 'foo@bar.baz' == self.config.email\n        mock_notify.assert_called_once_with('Account registered.')\n\n    def test_no_accounts_email(self):\n        self.config.email = 'other email'\n        with mock.patch('certbot._internal.main.client') as client:\n            client.register.return_value = (self.accs[1], mock.sentinel.acme)\n            self._call()\n        assert self.accs[1].id == self.config.account\n        assert 'other email' == self.config.email\n\n    def test_register_error_certbot(self):\n        err_msg = \"Some error message raised by Certbot\"\n        self._register_error_common(err_msg, errors.Error(err_msg))\n\n    def test_register_error_acme_type_and_detail(self):\n        err_msg = (\"Error returned by the ACME server: must agree to terms of service\")\n        exception = acme_error(typ = \"urn:ietf:params:acme:error:malformed\",\n                               detail = \"must agree to terms of service\")\n        self._register_error_common(err_msg, exception)\n\n    def test_register_error_acme_type_only(self):\n        err_msg = (\"Error returned by the ACME server: The server experienced an internal error\")\n        exception = acme_error(typ = \"urn:ietf:params:acme:error:serverInternal\")\n        self._register_error_common(err_msg, exception)\n\n\nclass MainTest(test_util.ConfigTestCase):\n    \"\"\"Tests for different commands.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        filesystem.mkdir(self.config.logs_dir)\n        self.standard_args = ['--config-dir', self.config.config_dir,\n                              '--work-dir', self.config.work_dir,\n                              '--logs-dir', self.config.logs_dir, '--text']\n\n        self.mock_sleep = mock.patch('time.sleep').start()\n\n    def tearDown(self):\n        # Reset globals in cli\n        reload_module(cli)\n\n        super().tearDown()\n\n    def _call(self, args, stdout=None, mockisfile=False):\n        \"\"\"Run the cli with output streams, actual client and optionally\n        os.path.isfile() mocked out\"\"\"\n\n        if mockisfile:\n            orig_open = os.path.isfile\n\n            def mock_isfile(fn, *args, **kwargs):  # pylint: disable=unused-argument\n                \"\"\"Mock os.path.isfile()\"\"\"\n                if (fn.endswith(\"cert\") or\n                        fn.endswith(\"chain\") or\n                        fn.endswith(\"privkey\")):\n                    return True\n                return orig_open(fn)\n\n            with mock.patch(\"certbot.compat.os.path.isfile\") as mock_if:\n                mock_if.side_effect = mock_isfile\n                with mock.patch('certbot._internal.main.client') as client:\n                    ret, stdout, stderr = self._call_no_clientmock(args, stdout)\n                    return ret, stdout, stderr, client\n        else:\n            with mock.patch('certbot._internal.main.client') as client:\n                ret, stdout, stderr = self._call_no_clientmock(args, stdout)\n                return ret, stdout, stderr, client\n\n    def _call_no_clientmock(self, args, stdout=None):\n        \"\"\"Run the client with output streams mocked out\"\"\"\n        args = self.standard_args + args\n\n        toy_stdout = stdout if stdout else io.StringIO()\n        with mock.patch('certbot._internal.main.sys.stdout', new=toy_stdout):\n            with mock.patch('certbot._internal.main.sys.stderr') as stderr:\n                with mock.patch(\"certbot.util.atexit\"):\n                    ret = main.main(args[:])  # NOTE: parser can alter its args!\n        return ret, toy_stdout, stderr\n\n    def test_no_flags(self):\n        with mock.patch('certbot._internal.main.run') as mock_run:\n            self._call([])\n            assert 1 == mock_run.call_count\n\n    def test_version_string_program_name(self):\n        toy_out = io.StringIO()\n        toy_err = io.StringIO()\n        with mock.patch('certbot._internal.main.sys.stdout', new=toy_out):\n            with mock.patch('certbot._internal.main.sys.stderr', new=toy_err):\n                try:\n                    main.main([\"--version\"])\n                except SystemExit:\n                    pass\n                finally:\n                    output = toy_out.getvalue() or toy_err.getvalue()\n                    assert \"certbot\" in output, \"Output is {0}\".format(output)\n\n    def _cli_missing_flag(self, args, message):\n        \"Ensure that a particular error raises a missing cli flag error containing message\"\n        exc = None\n        try:\n            with mock.patch('certbot._internal.main.sys.stderr'):\n                main.main(self.standard_args + args[:])  # NOTE: parser can alter its args!\n        except errors.MissingCommandlineFlag as exc_:\n            exc = exc_\n            assert message in str(exc)\n        assert exc is not None\n\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    def test_noninteractive(self, _):\n        args = ['-n', 'certonly']\n        self._cli_missing_flag(args, \"specify a plugin\")\n\n    @mock.patch('certbot._internal.eff.handle_subscription')\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main._determine_account')\n    @mock.patch('certbot._internal.main.client.Client.obtain_and_enroll_certificate')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    def test_user_agent(self, gsc, _obt, det, _, __, ___):\n        # Normally the client is totally mocked out, but here we need more\n        # arguments to automate it...\n        args = [\"--standalone\", \"certonly\", \"-m\", \"none@none.com\",\n                \"-d\", \"example.com\", '--agree-tos'] + self.standard_args\n        det.return_value = mock.MagicMock(), None\n        gsc.return_value = mock.MagicMock()\n\n        with mock.patch('certbot._internal.main.client.acme_client') as acme_client:\n            acme_net = acme_client.ClientNetwork\n            self._call_no_clientmock(args)\n            os_ver = util.get_os_info_ua()\n            ua = acme_net.call_args[1][\"user_agent\"]\n            assert os_ver in ua\n            import platform\n            plat = platform.platform()\n            if \"linux\" in plat.lower():\n                assert util.get_os_info_ua() in ua\n\n        with mock.patch('certbot._internal.main.client.acme_client') as acme_client:\n            acme_net = acme_client.ClientNetwork\n            ua = \"bandersnatch\"\n            args += [\"--user-agent\", ua]\n            self._call_no_clientmock(args)\n            acme_net.assert_called_once_with(mock.ANY, account=mock.ANY, verify_ssl=True,\n                user_agent=ua, alg=jose.RS256)\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_installer_selection(self, mock_pick_installer, _rec):\n        self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',\n                    '--key-path', 'privkey', '--chain-path', 'chain'], mockisfile=True)\n        assert mock_pick_installer.call_count == 1\n\n    @mock.patch('certbot._internal.main._install_cert')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_installer_certname(self, _inst, _rec, mock_install):\n        mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'),\n                                      chain_path=test_util.temp_join('chain'),\n                                      fullchain_path=test_util.temp_join('chain'),\n                                      key_path=test_util.temp_join('privkey'))\n\n        with mock.patch(\"certbot._internal.cert_manager.lineage_for_certname\") as mock_getlin:\n            mock_getlin.return_value = mock_lineage\n            self._call(['install', '--cert-name', 'whatever'], mockisfile=True)\n            call_config = mock_install.call_args[0][0]\n            assert call_config.cert_path == test_util.temp_join('cert')\n            assert call_config.fullchain_path == test_util.temp_join('chain')\n            assert call_config.key_path == test_util.temp_join('privkey')\n\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    @mock.patch('certbot._internal.main._install_cert')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_installer_param_override(self, _inst, _rec, mock_install, _):\n        mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'),\n                                      chain_path=test_util.temp_join('chain'),\n                                      fullchain_path=test_util.temp_join('chain'),\n                                      key_path=test_util.temp_join('privkey'))\n        with mock.patch(\"certbot._internal.cert_manager.lineage_for_certname\") as mock_getlin:\n            mock_getlin.return_value = mock_lineage\n            self._call(['install', '--cert-name', 'whatever',\n                        '--key-path', test_util.temp_join('overriding_privkey')], mockisfile=True)\n            call_config = mock_install.call_args[0][0]\n            assert call_config.cert_path == test_util.temp_join('cert')\n            assert call_config.fullchain_path == test_util.temp_join('chain')\n            assert call_config.chain_path == test_util.temp_join('chain')\n            assert call_config.key_path == test_util.temp_join('overriding_privkey')\n\n            mock_install.reset()\n\n            self._call(['install', '--cert-name', 'whatever',\n                        '--cert-path', test_util.temp_join('overriding_cert')], mockisfile=True)\n            call_config = mock_install.call_args[0][0]\n            assert call_config.cert_path == test_util.temp_join('overriding_cert')\n            assert call_config.fullchain_path == test_util.temp_join('chain')\n            assert call_config.key_path == test_util.temp_join('privkey')\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_installer_param_error(self, _inst, _rec):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(['install', '--cert-name', 'notfound',\n                           '--key-path', 'invalid'])\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    @mock.patch('certbot._internal.cert_manager.get_certnames')\n    @mock.patch('certbot._internal.main._install_cert')\n    def test_installer_select_cert(self, mock_inst, mock_getcert, _inst, _rec):\n        mock_lineage = mock.MagicMock(cert_path=test_util.temp_join('cert'),\n                                      chain_path=test_util.temp_join('chain'),\n                                      fullchain_path=test_util.temp_join('chain'),\n                                      key_path=test_util.temp_join('privkey'))\n        with mock.patch(\"certbot._internal.cert_manager.lineage_for_certname\") as mock_getlin:\n            mock_getlin.return_value = mock_lineage\n            self._call(['install'], mockisfile=True)\n        assert mock_getcert.called\n        assert mock_inst.called\n\n    @mock.patch('certbot._internal.eff.handle_subscription')\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot.util.exe_exists')\n    def test_configurator_selection(self, mock_exe_exists, _, __, ___):\n        mock_exe_exists.return_value = True\n        real_plugins = disco.PluginsRegistry.find_all()\n        args = ['--apache', '--authenticator', 'standalone']\n\n        # This needed two calls to find_all(), which we're avoiding for now\n        # because of possible side effects:\n        # https://github.com/certbot/certbot/commit/51ed2b681f87b1eb29088dd48718a54f401e4855\n        # with mock.patch('certbot._internal.cli.plugins_testable') as plugins:\n        #    plugins.return_value = {\"apache\": True, \"nginx\": True}\n        #    ret, _, _, _ = self._call(args)\n        #    self.assertTrue(\"Too many flags setting\" in ret)\n\n        args = [\"install\", \"--nginx\", \"--cert-path\",\n                test_util.temp_join('blah'), \"--key-path\", test_util.temp_join('blah'),\n                \"--nginx-server-root\", \"/nonexistent/thing\", \"-d\",\n                \"example.com\", \"--debug\"]\n        if \"nginx\" in real_plugins:\n            # Sending nginx a non-existent conf dir will simulate misconfiguration\n            # (we can only do that if certbot-nginx is actually present)\n            ret, _, _, _ = self._call(args)\n            assert \"The nginx plugin is not working\" in ret\n            assert \"MisconfigurationError\" in ret\n\n        self._cli_missing_flag([\"--standalone\"], \"With the standalone plugin, you probably\")\n\n        with mock.patch(\"certbot._internal.main._init_le_client\") as mock_init:\n            with mock.patch(\"certbot._internal.main._get_and_save_cert\") as mock_gsc:\n                mock_gsc.return_value = mock.MagicMock()\n                self._call([\"certonly\", \"--manual\", \"-d\", \"foo.bar\"])\n                unused_config, auth, unused_installer = mock_init.call_args[0]\n                assert isinstance(auth, manual.Authenticator)\n\n        with mock.patch('certbot._internal.main.certonly') as mock_certonly:\n            self._call([\"auth\", \"--standalone\"])\n            assert 1 == mock_certonly.call_count\n\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    def test_rollback(self, _):\n        _, _, _, client = self._call(['rollback'])\n        assert 1 == client.rollback.call_count\n\n        _, _, _, client = self._call(['rollback', '--checkpoints', '123'])\n        client.rollback.assert_called_once_with(\n            mock.ANY, 123, mock.ANY, mock.ANY)\n\n    @mock.patch('certbot._internal.cert_manager.certificates')\n    def test_certificates(self, mock_cert_manager):\n        self._call_no_clientmock(['certificates'])\n        assert 1 == mock_cert_manager.call_count\n\n    @mock.patch('certbot._internal.cert_manager.delete')\n    def test_delete(self, mock_cert_manager):\n        self._call_no_clientmock(['delete'])\n        assert 1 == mock_cert_manager.call_count\n\n    @mock.patch('certbot._internal.main.plugins_disco')\n    @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    def test_plugins(self, _, _det, mock_disco):\n        flags = ['--init', '--prepare', '--authenticators', '--installers']\n        for args in itertools.chain(\n                *(itertools.combinations(flags, r)\n                  for r in range(len(flags)))):\n            self._call(['plugins'] + list(args))\n\n    @mock.patch('certbot._internal.main.plugins_disco')\n    @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')\n    def test_plugins_no_args(self, _det, mock_disco):\n        ifaces: list[interfaces.Plugin] = []\n        plugins = mock_disco.PluginsRegistry.find_all()\n\n        stdout = io.StringIO()\n        with test_util.patch_display_util_with_stdout(stdout=stdout):\n            _, stdout, _, _ = self._call(['plugins'], stdout)\n\n        plugins.visible.assert_called_once_with()\n        plugins.visible().ifaces.assert_called_once_with(ifaces)\n        filtered = plugins.visible().ifaces()\n        assert stdout.getvalue().strip() == str(filtered)\n\n    @mock.patch('certbot._internal.main.plugins_disco')\n    @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')\n    def test_plugins_no_args_unprivileged(self, _det, mock_disco):\n        ifaces: list[interfaces.Plugin] = []\n        plugins = mock_disco.PluginsRegistry.find_all()\n\n        def throw_error(directory, mode, strict):\n            \"\"\"Raises error.Error.\"\"\"\n            _, _, _ = directory, mode, strict\n            raise errors.Error()\n\n        stdout = io.StringIO()\n        with mock.patch('certbot.util.set_up_core_dir') as mock_set_up_core_dir:\n            with test_util.patch_display_util_with_stdout(stdout=stdout):\n                mock_set_up_core_dir.side_effect = throw_error\n                _, stdout, _, _ = self._call(['plugins'], stdout)\n\n        plugins.visible.assert_called_once_with()\n        plugins.visible().ifaces.assert_called_once_with(ifaces)\n        filtered = plugins.visible().ifaces()\n        assert stdout.getvalue().strip() == str(filtered)\n\n    @mock.patch('certbot._internal.main.plugins_disco')\n    @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')\n    def test_plugins_init(self, _det, mock_disco):\n        ifaces: list[interfaces.Plugin] = []\n        plugins = mock_disco.PluginsRegistry.find_all()\n\n        stdout = io.StringIO()\n        with test_util.patch_display_util_with_stdout(stdout=stdout):\n            _, stdout, _, _ = self._call(['plugins', '--init'], stdout)\n\n        plugins.visible.assert_called_once_with()\n        plugins.visible().ifaces.assert_called_once_with(ifaces)\n        filtered = plugins.visible().ifaces()\n        assert filtered.init.call_count == 1\n        assert stdout.getvalue().strip() == str(filtered)\n\n    @mock.patch('certbot._internal.main.plugins_disco')\n    @mock.patch('certbot._internal.main.cli.HelpfulArgumentParser.determine_help_topics')\n    def test_plugins_prepare(self, _det, mock_disco):\n        ifaces: list[interfaces.Plugin] = []\n        plugins = mock_disco.PluginsRegistry.find_all()\n\n        stdout = io.StringIO()\n        with test_util.patch_display_util_with_stdout(stdout=stdout):\n            _, stdout, _, _ = self._call(['plugins', '--init', '--prepare'], stdout)\n\n        plugins.visible.assert_called_once_with()\n        plugins.visible().ifaces.assert_called_once_with(ifaces)\n        filtered = plugins.visible().ifaces()\n        assert filtered.init.call_count == 1\n        filtered.prepare.assert_called_once_with()\n        filtered.available.assert_called_once_with()\n        available = filtered.available()\n        assert stdout.getvalue().strip() == str(available)\n\n    def test_certonly_abspath(self):\n        cert = 'cert'\n        key = 'key'\n        chain = 'chain'\n        fullchain = 'fullchain'\n\n        with mock.patch('certbot._internal.main.certonly') as mock_certonly:\n            self._call(['certonly', '--cert-path', cert, '--key-path', 'key',\n                        '--chain-path', 'chain',\n                        '--fullchain-path', 'fullchain'])\n\n        config, unused_plugins = mock_certonly.call_args[0]\n        assert config.cert_path == os.path.abspath(cert)\n        assert config.key_path == os.path.abspath(key)\n        assert config.chain_path == os.path.abspath(chain)\n        assert config.fullchain_path == os.path.abspath(fullchain)\n\n    def test_certonly_bad_args(self):\n        try:\n            self._call(['-a', 'bad_auth', 'certonly'])\n            pytest.fail(\"Exception should have been raised\")\n        except errors.PluginSelectionError as e:\n            assert 'The requested bad_auth plugin does not appear' in str(e)\n\n    def test_check_config_sanity_domain(self):\n        # FQDN\n        with pytest.raises(errors.ConfigurationError):\n            self._call(['-d', 'a' * 64])\n        # FQDN 2\n        with pytest.raises(errors.ConfigurationError):\n            self._call(['-d', (('a' * 50) + '.') * 10])\n        # Bare IP address (this is actually a different error message now)\n        with pytest.raises(errors.ConfigurationError):\n            self._call(['-d', '204.11.231.35'])\n        # Bare IPv6 address\n        with pytest.raises(errors.ConfigurationError):\n            self._call(['-d', '2001:db8:ac69:3ff:b1cb:c8c6:5a84:a31b'])\n\n    def test_csr_with_besteffort(self):\n        with pytest.raises(errors.Error):\n            self._call('certonly --csr {0} --allow-subset-of-names'.format(CSR).split())\n\n    def test_run_with_csr(self):\n        # This is an error because you can only use --csr with certonly\n        try:\n            self._call(['--csr', CSR])\n        except errors.Error as e:\n            assert \"Please try the certonly\" in repr(e)\n            return\n        pytest.fail(\"Expected supplying --csr to fail with default verb\")\n\n    def test_csr_with_no_domains(self):\n        with pytest.raises(errors.Error):\n            self._call('certonly --csr {0}'.format(\n                test_util.vector_path('csr-nonames_512.pem')).split())\n\n    def test_csr_with_inconsistent_domains(self):\n        with pytest.raises(errors.Error):\n            self._call('certonly -d example.org --csr {0}'.format(CSR).split())\n\n    def _certonly_new_request_common(self, mock_client, args=None):\n        with mock.patch('certbot._internal.main._find_lineage_for_sans_and_certname') \\\n            as mock_renewal:\n            mock_renewal.return_value = (\"newcert\", None)\n            with mock.patch('certbot._internal.main._init_le_client') as mock_init:\n                mock_init.return_value = mock_client\n                if args is None:\n                    args = []\n                args += '-d foo.bar -a standalone certonly'.split()\n                self._call(args)\n\n    @mock.patch('certbot._internal.main._report_new_cert')\n    def test_certonly_dry_run_new_request_success(self, mock_report):\n        mock_client = mock.MagicMock()\n        mock_client.obtain_and_enroll_certificate.return_value = None\n        self._certonly_new_request_common(mock_client, ['--dry-run'])\n        assert mock_client.obtain_and_enroll_certificate.call_count == 1\n        assert mock_report.call_count == 1\n        assert mock_report.call_args[0][0].dry_run is True\n\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main.util.atexit_register')\n    @mock.patch('certbot._internal.eff.handle_subscription')\n    @mock.patch('certbot.crypto_util.notAfter')\n    def test_certonly_new_request_success(self, mock_notAfter,\n                                          mock_subscription, mock_register, mock_report):\n        cert_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/foo.bar'))\n        key_path = os.path.normpath(os.path.join(self.config.config_dir, 'live/baz.qux'))\n        date = '1970-01-01'\n        mock_notAfter().date.return_value = date\n\n        mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path,\n                                      fullchain_path=cert_path, key_path=key_path)\n        mock_client = mock.MagicMock()\n        mock_client.obtain_and_enroll_certificate.return_value = mock_lineage\n        self._certonly_new_request_common(mock_client)\n        assert mock_client.obtain_and_enroll_certificate.call_count == 1\n        assert mock_report.call_count == 1\n        assert cert_path in mock_report.call_args[0][2]\n        assert key_path in mock_report.call_args[0][3]\n        assert 'donate' in mock_register.call_args[0][1]\n        assert mock_subscription.called is True\n\n    def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,\n                             args=None, should_renew=True, error_expected=False,\n                             quiet_mode=False, expiry_date=datetime.datetime.now(),\n                             reuse_key=False, new_key=False):\n        cert_path = test_util.vector_path('cert_512.pem')\n        chain_path = os.path.normpath(os.path.join(self.config.config_dir,\n                                                   'live/foo.bar/fullchain.pem'))\n        mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path,\n                                      cert_path=cert_path, fullchain_path=chain_path)\n        mock_lineage.has_pending_deployment.return_value = False\n        mock_lineage.sans.return_value = [san.DNSName('isnot.org')]\n        mock_lineage.private_key_type = 'ecdsa'\n        mock_lineage.elliptic_curve = 'secp256r1'\n        mock_lineage.reuse_key = reuse_key\n        mock_certr = mock.MagicMock()\n        mock_key = mock.MagicMock(pem='pem_key')\n        mock_client = mock.MagicMock()\n        stdout = io.StringIO()\n        mock_client.obtain_certificate.return_value = (mock_certr, 'chain',\n                                                       mock_key, 'csr')\n\n        def write_msg(message, *args, **kwargs):  # pylint: disable=unused-argument\n            \"\"\"Write message to stdout.\"\"\"\n            stdout.write(message)\n\n        try:\n            with mock.patch('certbot._internal.cert_manager.find_duplicative_certs') as mock_fdc:\n                mock_fdc.return_value = (mock_lineage, None)\n                with mock.patch('certbot._internal.main._init_le_client') as mock_init:\n                    mock_init.return_value = mock_client\n                    with mock.patch('certbot._internal.display.obj.get_display') as mock_display:\n                        if not quiet_mode:\n                            mock_display().notification.side_effect = write_msg\n                        with mock.patch('certbot._internal.main.renewal.crypto_util') \\\n                            as mock_crypto_util:\n                            mock_crypto_util.notAfter.return_value = expiry_date\n                            with mock.patch('certbot._internal.eff.handle_subscription'):\n                                with mock.patch('certbot._internal.renewal.should_autorenew') as should_autorenew:\n                                    should_autorenew.return_value = due_for_renewal\n                                    if not args:\n                                        args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly']\n                                    if extra_args:\n                                        args += extra_args\n                                    try:\n                                        ret, stdout, _, _ = self._call(args, stdout)\n                                        if ret:\n                                            print(\"Returned\", ret)\n                                            raise AssertionError(ret)\n                                        assert not error_expected, \"renewal should have errored\"\n                                    except: # pylint: disable=bare-except\n                                        if not error_expected:\n                                            raise AssertionError(\n                                                \"Unexpected renewal error:\\n\" +\n                                                traceback.format_exc())\n\n            if should_renew:\n                if reuse_key and not new_key:\n                    # The location of the previous live privkey.pem is passed\n                    # to obtain_certificate\n                    mock_client.obtain_certificate.assert_called_once_with([mock.ANY],\n                        os.path.normpath(os.path.join(\n                            self.config.config_dir, \"live/sample-renewal/privkey.pem\")))\n                else:\n                    mock_client.obtain_certificate.assert_called_once_with([mock.ANY], None)\n            else:\n                assert mock_client.obtain_certificate.call_count == 0\n        except:\n            self._dump_log()\n            raise\n        finally:\n            if log_out:\n                with open(os.path.join(self.config.logs_dir, \"letsencrypt.log\")) as lf:\n                    assert log_out in lf.read()\n\n        return mock_lineage, mock_display, stdout\n\n    @mock.patch('certbot._internal.main._report_new_cert')\n    @mock.patch('certbot._internal.main.util.atexit_register')\n    @mock.patch('certbot.crypto_util.notAfter')\n    def test_certonly_renewal(self, _, mock_register, mock_report):\n        lineage, _, _ = self._test_renewal_common(True, [])\n        assert lineage.save_successor.call_count == 1\n        lineage.update_all_links_to.assert_called_once_with(\n            lineage.latest_common_version())\n        assert mock_report.call_count == 1\n        assert 'fullchain.pem' in mock_report.call_args[0][2]\n        assert 'donate' in mock_register.call_args[0][1]\n\n    @mock.patch('certbot._internal.main.display_util.notify')\n    @mock.patch('certbot._internal.log.logging.handlers.RotatingFileHandler.doRollover')\n    @mock.patch('certbot.crypto_util.notAfter')\n    def test_certonly_renewal_triggers(self, _, __, mock_notify):\n        # --dry-run should force renewal\n        _, _, _ = self._test_renewal_common(False, ['--dry-run', '--keep'],\n                                                      log_out=\"simulating renewal\")\n        mock_notify.assert_any_call('The dry run was successful.')\n\n        self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],\n                                  log_out=\"Auto-renewal forced\")\n\n        _, mock_displayer, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],\n                                  should_renew=False)\n        assert 'not yet due' in mock_displayer().notification.call_args[0][0]\n\n    def _dump_log(self):\n        print(\"Logs:\")\n        log_path = os.path.join(self.config.logs_dir, \"letsencrypt.log\")\n        if os.path.exists(log_path):\n            with open(log_path) as lf:\n                print(lf.read())\n\n    def test_renew_verb(self):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\", \"-tvv\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True)\n\n    def test_reuse_key(self):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False)\n        args = [\"renew\", \"--dry-run\", \"--reuse-key\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True)\n\n    @mock.patch('certbot._internal.storage.RenewableCert.save_successor')\n    def test_reuse_key_no_dry_run(self, unused_save_successor):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False)\n        args = [\"renew\", \"--reuse-key\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True)\n\n    @mock.patch('certbot._internal.storage.RenewableCert.save_successor')\n    def test_new_key(self, unused_save_successor):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--reuse-key\", \"--new-key\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True, reuse_key=True,\n                                  new_key=True)\n\n    @mock.patch('sys.stdin')\n    def test_noninteractive_renewal_delay(self, stdin):\n        stdin.isatty.return_value = False\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\", \"-tvv\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True)\n        assert self.mock_sleep.call_count == 1\n        # in main.py:\n        #     sleep_time = random.randint(1, 60*8)\n        sleep_call_arg = self.mock_sleep.call_args[0][0]\n        assert 1 <= sleep_call_arg <= 60*8\n\n    @mock.patch('sys.stdin')\n    def test_interactive_no_renewal_delay(self, stdin):\n        stdin.isatty.return_value = True\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\", \"-tvv\"]\n        self._test_renewal_common(True, [], args=args, should_renew=True)\n        assert self.mock_sleep.call_count == 0\n\n    @mock.patch('certbot._internal.renewal.should_renew')\n    def test_renew_skips_recent_certs(self, should_renew):\n        should_renew.return_value = False\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        expiry = datetime.datetime.now() + datetime.timedelta(days=90)\n        _, _, stdout = self._test_renewal_common(False, extra_args=None, should_renew=False,\n                                                 args=['renew'], expiry_date=expiry)\n        assert 'No renewals were attempted.' in stdout.getvalue()\n        assert 'The following certificates are not due for renewal yet:' in stdout.getvalue()\n\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    def test_quiet_renew(self, _):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\"]\n        _, _, stdout = self._test_renewal_common(True, [], args=args, should_renew=True)\n        out = stdout.getvalue()\n        assert \"renew\" in out\n\n        args = [\"renew\", \"--dry-run\", \"-q\"]\n        _, _, stdout = self._test_renewal_common(True, [], args=args,\n                                                 should_renew=True, quiet_mode=True)\n        out = stdout.getvalue()\n        assert \"\" == out\n\n    def test_renew_hook_validation(self):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\", \"--post-hook=no-such-command\"]\n        self._test_renewal_common(True, [], args=args, should_renew=False,\n                                  error_expected=True)\n\n    def test_renew_no_hook_validation(self):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        args = [\"renew\", \"--dry-run\", \"--post-hook=no-such-command\",\n                \"--disable-hook-validation\"]\n        with mock.patch(\"certbot._internal.hooks.post_hook\"):\n            self._test_renewal_common(True, [], args=args, should_renew=True,\n                                      error_expected=False)\n\n    def test_renew_verb_empty_config(self):\n        rd = os.path.join(self.config.config_dir, 'renewal')\n        if not os.path.exists(rd):\n            filesystem.makedirs(rd)\n        with open(os.path.join(rd, 'empty.conf'), 'w'):\n            pass  # leave the file empty\n        args = [\"renew\", \"--dry-run\", \"-tvv\"]\n        self._test_renewal_common(False, [], args=args, should_renew=False, error_expected=True)\n\n    def test_renew_with_certname(self):\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        self._test_renewal_common(True, [], should_renew=True,\n            args=['renew', '--dry-run', '--cert-name', 'sample-renewal'])\n\n    def test_renew_with_bad_certname(self):\n        self._test_renewal_common(True, [], should_renew=False,\n            args=['renew', '--dry-run', '--cert-name', 'sample-renewal'],\n            error_expected=True)\n\n    def _make_dummy_renewal_config(self):\n        renewer_configs_dir = os.path.join(self.config.config_dir, 'renewal')\n        filesystem.makedirs(renewer_configs_dir)\n        with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f:\n            f.write(\"My contents don't matter\")\n\n    def _test_renew_common(self, renewalparams=None, names=None,\n                           assert_oc_called=None, **kwargs):\n        self._make_dummy_renewal_config()\n        with mock.patch('certbot._internal.storage.RenewableCert') as mock_rc:\n            mock_lineage = mock.MagicMock()\n            mock_lineage.fullchain = \"somepath/fullchain.pem\"\n            if renewalparams is not None:\n                mock_lineage.configuration = {'renewalparams': renewalparams}\n            if names is not None:\n                mock_lineage.sans.return_value = [san.DNSName(n) for n in names]\n            mock_rc.return_value = mock_lineage\n            with mock.patch('certbot._internal.main.renew_cert') as mock_renew_cert:\n                kwargs.setdefault('args', ['renew'])\n                self._test_renewal_common(True, None, should_renew=False, **kwargs)\n\n            if assert_oc_called is not None:\n                if assert_oc_called:\n                    assert mock_renew_cert.called\n                else:\n                    assert mock_renew_cert.called is False\n\n    def test_renew_no_renewalparams(self):\n        self._test_renew_common(assert_oc_called=False, error_expected=True)\n\n    def test_renew_no_authenticator(self):\n        self._test_renew_common(renewalparams={}, assert_oc_called=False,\n            error_expected=True)\n\n    def test_renew_with_bad_int(self):\n        renewalparams = {'authenticator': 'webroot',\n                         'rsa_key_size': 'over 9000'}\n        self._test_renew_common(renewalparams=renewalparams, error_expected=True,\n                                assert_oc_called=False)\n\n    def test_renew_with_nonetype_http01(self):\n        renewalparams = {'authenticator': 'webroot',\n                         'http01_port': 'None'}\n        self._test_renew_common(renewalparams=renewalparams,\n                                assert_oc_called=True)\n\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    def test_renew_with_configurator(self, mock_sel):\n        mock_sel.return_value = (mock.MagicMock(), mock.MagicMock())\n        renewalparams = {'authenticator': 'webroot'}\n        self._test_renew_common(\n            renewalparams=renewalparams, assert_oc_called=True,\n            args='renew --configurator apache'.split())\n\n    def test_renew_plugin_config_restoration(self):\n        renewalparams = {'authenticator': 'webroot',\n                         'webroot_path': 'None',\n                         'webroot_imaginary_flag': '42'}\n        self._test_renew_common(renewalparams=renewalparams,\n                                assert_oc_called=True)\n\n    def test_renew_with_webroot_map(self):\n        renewalparams = {'authenticator': 'webroot'}\n        self._test_renew_common(\n            renewalparams=renewalparams, assert_oc_called=True,\n            args=['renew', '--webroot-map', json.dumps({'example.com': tempfile.gettempdir()})])\n\n    def test_renew_reconstitute_error(self):\n        # pylint: disable=protected-access\n        with mock.patch('certbot._internal.main.renewal.reconstitute') as mock_reconstitute:\n            mock_reconstitute.side_effect = Exception\n            self._test_renew_common(assert_oc_called=False, error_expected=True)\n\n    def test_renew_obtain_cert_error(self):\n        self._make_dummy_renewal_config()\n        with mock.patch('certbot._internal.storage.RenewableCert') as mock_rc:\n            mock_lineage = mock.MagicMock()\n            mock_lineage.fullchain = \"somewhere/fullchain.pem\"\n            mock_rc.return_value = mock_lineage\n            mock_lineage.configuration = {\n                'renewalparams': {'authenticator': 'webroot'}}\n            with mock.patch('certbot._internal.main.renew_cert') as mock_renew_cert:\n                mock_renew_cert.side_effect = Exception\n                self._test_renewal_common(True, None, error_expected=True,\n                                          args=['renew'], should_renew=False)\n\n    def test_renew_with_bad_cli_args(self):\n        self._test_renewal_common(True, None, args='renew -d example.com'.split(),\n                                  should_renew=False, error_expected=True)\n        self._test_renewal_common(True, None, args='renew --csr {0}'.format(CSR).split(),\n                                  should_renew=False, error_expected=True)\n\n    def test_no_renewal_with_hooks(self):\n        _, _, stdout = self._test_renewal_common(\n            due_for_renewal=False, extra_args=None, should_renew=False,\n            args=['renew', '--post-hook',\n                  '{0} -c \"print(\\'hello world\\');\"'\n                  .format(sys.executable)])\n        assert 'No hooks were run.' in stdout.getvalue()\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot._internal.main._find_lineage_for_sans_and_certname')\n    @mock.patch('certbot._internal.main._init_le_client')\n    @mock.patch('certbot._internal.main._report_new_cert')\n    def test_certonly_reinstall(self, mock_report_new_cert, mock_init,\n                                mock_renewal, mock_get_utility):\n        mock_renewal.return_value = ('reinstall', mock.MagicMock())\n        mock_init.return_value = mock_client = mock.MagicMock()\n        self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])\n        assert mock_client.obtain_certificate.called is False\n        assert mock_client.obtain_and_enroll_certificate.called is False\n        assert mock_get_utility().add_message.call_count == 0\n        mock_report_new_cert.assert_not_called()\n        #self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0])\n\n    def _test_certonly_csr_common(self, extra_args=None):\n        certr = 'certr'\n        chain = 'chain'\n        mock_client = mock.MagicMock()\n        mock_client.obtain_certificate_from_csr.return_value = (certr, chain)\n        cert_path = os.path.normpath(os.path.join(\n            self.config.config_dir,\n            'live/example.com/cert_512.pem'))\n        full_path = os.path.normpath(os.path.join(\n            self.config.config_dir,\n            'live/example.com/fullchain.pem'))\n        mock_client.save_certificate.return_value = cert_path, None, full_path\n        with mock.patch('certbot._internal.main._init_le_client') as mock_init:\n            mock_init.return_value = mock_client\n            chain_path = os.path.normpath(os.path.join(\n                self.config.config_dir,\n                'live/example.com/chain.pem'))\n            args = ('-a standalone certonly --csr {0} --cert-path {1} '\n                    '--chain-path {2} --fullchain-path {3}').format(\n                        CSR, cert_path, chain_path, full_path).split()\n            if extra_args:\n                args += extra_args\n            with mock.patch('certbot._internal.main.crypto_util'):\n                self._call(args)\n\n        if '--dry-run' in args:\n            assert mock_client.save_certificate.called is False\n        else:\n            mock_client.save_certificate.assert_called_once_with(\n                certr, chain, cert_path, chain_path, full_path)\n\n    @mock.patch('certbot._internal.main._csr_report_new_cert')\n    @mock.patch('certbot._internal.main.util.atexit_register')\n    @mock.patch('certbot._internal.eff.handle_subscription')\n    def test_certonly_csr(self, mock_subscription, mock_register, mock_csr_report):\n        self._test_certonly_csr_common()\n        assert mock_csr_report.call_count == 1\n        assert 'cert_512.pem' in mock_csr_report.call_args[0][1]\n        assert mock_csr_report.call_args[0][2] is None\n        assert 'fullchain.pem' in mock_csr_report.call_args[0][3]\n        assert 'donate' in mock_register.call_args[0][1]\n        assert mock_subscription.called is True\n\n    @mock.patch('certbot._internal.main._csr_report_new_cert')\n    def test_certonly_csr_dry_run(self, mock_csr_report):\n        self._test_certonly_csr_common(['--dry-run'])\n        assert mock_csr_report.call_count == 1\n        assert mock_csr_report.call_args[0][0].dry_run is True\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.main.client.acme_client')\n    def test_revoke_with_key(self, mock_acme_client,\n            mock_delete_if_appropriate):\n        mock_delete_if_appropriate.return_value = False\n        server = 'foo.bar'\n        self._call_no_clientmock(['--cert-path', SS_CERT_PATH, '--key-path', RSA2048_KEY_PATH,\n                                 '--server', server, 'revoke'])\n        with open(RSA2048_KEY_PATH, 'rb') as f:\n            assert mock_acme_client.ClientV2.call_count == 1\n            assert mock_acme_client.ClientNetwork.call_args[0][0] == \\\n                             jose.JWK.load(f.read())\n        with open(SS_CERT_PATH, 'rb') as f:\n            cert = x509.load_pem_x509_certificate(f.read())\n            mock_revoke = mock_acme_client.ClientV2().revoke\n            mock_revoke.assert_called_once_with(cert, mock.ANY)\n\n    def test_revoke_with_key_mismatch(self):\n        server = 'foo.bar'\n        with pytest.raises(errors.Error):\n            self._call_no_clientmock(['--cert-path', CERT, '--key-path', P256_KEY_PATH,\n                                 '--server', server, 'revoke'])\n\n    @mock.patch('certbot._internal.main._delete_if_appropriate')\n    @mock.patch('certbot._internal.main._determine_account')\n    def test_revoke_without_key(self, mock_determine_account,\n            mock_delete_if_appropriate):\n        mock_delete_if_appropriate.return_value = False\n        mock_determine_account.return_value = (mock.MagicMock(), None)\n        _, _, _, client = self._call(['--cert-path', CERT, 'revoke'])\n        with open(CERT, 'rb') as f:\n            cert = x509.load_pem_x509_certificate(f.read())\n            mock_revoke = client.create_acme_client().revoke\n            mock_revoke.assert_called_once_with(cert, mock.ANY)\n\n    @mock.patch('certbot._internal.log.post_arg_parse_setup')\n    def test_register(self, _):\n        with mock.patch('certbot._internal.main.client') as mocked_client:\n            acc = mock.MagicMock()\n            acc.id = \"imaginary_account\"\n            mocked_client.register.return_value = (acc, \"worked\")\n            self._call_no_clientmock([\"register\", \"--email\", \"user@example.org\"])\n            # TODO: It would be more correct to explicitly check that\n            #       _determine_account() gets called in the above case,\n            #       but coverage statistics should also show that it did.\n            with mock.patch('certbot._internal.main.account') as mocked_account:\n                mocked_storage = mock.MagicMock()\n                mocked_account.AccountFileStorage.return_value = mocked_storage\n                mocked_storage.find_all.return_value = [\"an account\"]\n                x = self._call_no_clientmock([\"register\", \"--email\", \"user@example.org\"])\n                assert \"There is an existing account\" in x[0]\n\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    @mock.patch('certbot._internal.updater._run_updaters')\n    def test_plugin_selection_error(self, mock_run, mock_choose):\n        mock_choose.side_effect = errors.PluginSelectionError\n        with pytest.raises(errors.PluginSelectionError):\n            main.renew_cert(None, None, None)\n\n        self.config.dry_run = False\n        updater.run_generic_updaters(self.config, None, None)\n        # Make sure we're returning None, and hence not trying to run the\n        # without installer\n        assert mock_run.called is False\n\n    @mock.patch('certbot._internal.main.updater.run_renewal_deployer')\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    @mock.patch('certbot._internal.main._init_le_client')\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    def test_renew_doesnt_restart_on_dryrun(self, mock_get_cert, mock_init, mock_choose,\n                                            mock_run_renewal_deployer):\n        \"\"\"A dry-run renewal shouldn't try to restart the installer\"\"\"\n        self.config.dry_run = True\n        installer = mock.MagicMock()\n        mock_choose.return_value = (installer, mock.MagicMock())\n\n        main.renew_cert(self.config, None, None)\n\n        assert mock_init.call_count == 1\n        assert mock_get_cert.call_count == 1\n        installer.restart.assert_not_called()\n        mock_run_renewal_deployer.assert_not_called()\n\n\nclass UnregisterTest(unittest.TestCase):\n    def setUp(self):\n        self.patchers = {\n            '_determine_account': mock.patch('certbot._internal.main._determine_account'),\n            'account': mock.patch('certbot._internal.main.account'),\n            'client': mock.patch('certbot._internal.main.client'),\n            'get_utility': test_util.patch_display_util()}\n        self.mocks = {k: v.start() for k, v in self.patchers.items()}\n\n    def tearDown(self):\n        for patch in self.patchers.values():\n            patch.stop()\n\n    def test_abort_unregister(self):\n        self.mocks['account'].AccountFileStorage.return_value = mock.Mock()\n\n        util_mock = self.mocks['get_utility']()\n        util_mock.yesno.return_value = False\n\n        config = mock.Mock()\n        unused_plugins = mock.Mock()\n\n        res = main.unregister(config, unused_plugins)\n        assert res == \"Deactivation aborted.\"\n\n    @mock.patch(\"certbot._internal.main.display_util.notify\")\n    def test_unregister(self, mock_notify):\n        mocked_storage = mock.MagicMock()\n        mocked_storage.find_all.return_value = [\"an account\"]\n\n        self.mocks['account'].AccountFileStorage.return_value = mocked_storage\n        self.mocks['_determine_account'].return_value = (mock.MagicMock(), \"foo\")\n\n        cb_client = mock.MagicMock()\n        self.mocks['client'].Client.return_value = cb_client\n\n        config = mock.MagicMock()\n        unused_plugins = mock.MagicMock()\n\n        res = main.unregister(config, unused_plugins)\n\n        assert res is None\n        mock_notify.assert_called_once_with(\"Account deactivated.\")\n\n    def test_unregister_no_account(self):\n        mocked_storage = mock.MagicMock()\n        mocked_storage.find_all.return_value = []\n        self.mocks['account'].AccountFileStorage.return_value = mocked_storage\n\n        cb_client = mock.MagicMock()\n        self.mocks['client'].Client.return_value = cb_client\n\n        config = mock.MagicMock()\n        config.server = \"https://acme.example.com/directory\"\n        unused_plugins = mock.MagicMock()\n\n        res = main.unregister(config, unused_plugins)\n        m = \"Could not find existing account for server https://acme.example.com/directory.\"\n        assert res == m\n        assert cb_client.acme.deactivate_registration.called is False\n\n\nclass MakeOrVerifyNeededDirs(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.make_or_verify_needed_dirs.\"\"\"\n\n    @mock.patch(\"certbot._internal.main.util\")\n    def test_it(self, mock_util):\n        main.make_or_verify_needed_dirs(self.config)\n        for core_dir in (self.config.config_dir, self.config.work_dir,):\n            mock_util.set_up_core_dir.assert_any_call(\n                core_dir, constants.CONFIG_DIRS_MODE,\n                self.config.strict_permissions\n            )\n\n        hook_dirs = (self.config.renewal_pre_hooks_dir,\n                     self.config.renewal_deploy_hooks_dir,\n                     self.config.renewal_post_hooks_dir,)\n        for hook_dir in hook_dirs:\n            # default mode of 755 is used\n            mock_util.make_or_verify_dir.assert_any_call(\n                hook_dir, strict=self.config.strict_permissions)\n\n\nclass EnhanceTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.enhance.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.get_utility_patch = test_util.patch_display_util()\n        self.mock_get_utility = self.get_utility_patch.start()\n        self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)\n\n    def tearDown(self):\n        self.get_utility_patch.stop()\n\n    def _call(self, args):\n        plugins = disco.PluginsRegistry.find_all()\n        config = cli.prepare_and_parse_args(plugins, args)\n\n        with mock.patch('certbot._internal.cert_manager.get_certnames') as mock_certs:\n            mock_certs.return_value = ['example.com']\n            with mock.patch('certbot._internal.cert_manager.sans_for_certname') as mock_dom:\n                mock_dom.return_value = [san.DNSName('example.com')]\n                with mock.patch('certbot._internal.main._init_le_client') as mock_init:\n                    mock_client = mock.MagicMock()\n                    mock_client.config = config\n                    mock_init.return_value = mock_client\n                    main.enhance(config, plugins)\n                    return mock_client # returns the client\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main._find_sans_or_certname')\n    def test_selection_question(self, mock_find, mock_choose, mock_lineage, _rec):\n        mock_lineage.return_value = mock.MagicMock(chain_path=\"/tmp/nonexistent\")\n        mock_choose.return_value = ['example.com']\n        mock_find.return_value = (None, None)\n        with mock.patch('certbot._internal.main.plug_sel.pick_installer') as mock_pick:\n            self._call(['enhance', '--redirect'])\n            assert mock_pick.called\n            # Check that the message includes \"enhancements\"\n            assert \"enhancements\" in mock_pick.call_args[0][3]\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main._find_sans_or_certname')\n    def test_selection_auth_warning(self, mock_find, mock_choose, mock_lineage, _rec):\n        mock_lineage.return_value = mock.MagicMock(chain_path=\"/tmp/nonexistent\")\n        mock_choose.return_value = [\"example.com\"]\n        mock_find.return_value = (None, None)\n        with mock.patch('certbot._internal.main.plug_sel.pick_installer'):\n            with mock.patch('certbot._internal.main.plug_sel.logger.warning') as mock_log:\n                mock_client = self._call(['enhance', '-a', 'webroot', '--redirect'])\n                assert mock_log.called\n                assert \"make sense\" in mock_log.call_args[0][0]\n                assert mock_client.enhance_config.called\n\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    def test_enhance_config_call(self, _rec, mock_choose, mock_lineage):\n        mock_lineage.return_value = mock.MagicMock(chain_path=\"/tmp/nonexistent\")\n        mock_choose.return_value = [\"example.com\"]\n        with mock.patch('certbot._internal.main.plug_sel.pick_installer'):\n            mock_client = self._call(['enhance', '--redirect', '--hsts'])\n            req_enh = [\"redirect\", \"hsts\"]\n            not_req_enh = [\"uir\"]\n            assert mock_client.enhance_config.called\n            assert all(getattr(mock_client.config, e) for e in req_enh)\n            assert not any(getattr(mock_client.config, e) for e in not_req_enh)\n            assert san.DNSName(\"example.com\") in mock_client.enhance_config.call_args[0][0]\n\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    def test_enhance_noninteractive(self, _rec, mock_choose, mock_lineage):\n        mock_lineage.return_value = mock.MagicMock(\n            chain_path=\"/tmp/nonexistent\")\n        mock_choose.return_value = [\"example.com\"]\n        with mock.patch('certbot._internal.main.plug_sel.pick_installer'):\n            mock_client = self._call(['enhance', '--redirect',\n                                      '--hsts', '--non-interactive'])\n            assert mock_client.enhance_config.called\n            assert mock_choose.called is False\n\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    def test_user_abort_domains(self, _rec, mock_choose):\n        mock_choose.return_value = []\n        with mock.patch('certbot._internal.main.plug_sel.pick_installer'):\n            with pytest.raises(errors.Error):\n                self._call(['enhance', '--redirect', '--hsts'])\n\n    def test_no_enhancements_defined(self):\n        with pytest.raises(errors.MisconfigurationError):\n            self._call(['enhance', '-a', 'null'])\n\n    @mock.patch('certbot._internal.main.plug_sel.choose_configurator_plugins')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    def test_plugin_selection_error(self, _rec, mock_choose, mock_pick):\n        mock_choose.return_value = [\"example.com\"]\n        mock_pick.return_value = (None, None)\n        mock_pick.side_effect = errors.PluginSelectionError()\n        mock_client = self._call(['enhance', '--hsts'])\n        assert mock_client.enhance_config.called is False\n\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @test_util.patch_display_util()\n    def test_enhancement_enable(self, _, _rec, mock_inst, mock_choose, mock_lineage):\n        mock_inst.return_value = self.mockinstaller\n        mock_choose.return_value = [\"example.com\", \"another.tld\"]\n        mock_lineage.return_value = mock.MagicMock(chain_path=\"/tmp/nonexistent\")\n        self._call(['enhance', '--auto-hsts'])\n        assert self.mockinstaller.enable_autohsts.called\n        assert self.mockinstaller.enable_autohsts.call_args[0][1] == \\\n                          [\"example.com\", \"another.tld\"]\n\n    @mock.patch('certbot._internal.cert_manager.lineage_for_certname')\n    @mock.patch('certbot._internal.main.display_ops.choose_values')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @test_util.patch_display_util()\n    def test_enhancement_enable_not_supported(self, _, _rec, mock_inst, mock_choose, mock_lineage):\n        mock_inst.return_value = null.Installer(self.config, \"null\")\n        mock_choose.return_value = [\"example.com\", \"another.tld\"]\n        mock_lineage.return_value = mock.MagicMock(chain_path=\"/tmp/nonexistent\")\n        with pytest.raises(errors.NotSupportedError):\n            self._call(['enhance', '--auto-hsts'])\n\n    def test_enhancement_enable_conflict(self):\n        with pytest.raises(errors.Error):\n            self._call(['enhance', '--auto-hsts', '--hsts'])\n\n\nclass InstallTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.install.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_install_enhancement_not_supported(self, mock_inst, _rec):\n        mock_inst.return_value = null.Installer(self.config, \"null\")\n        plugins = disco.PluginsRegistry.find_all()\n        self.config.auto_hsts = True\n        self.config.certname = \"nonexistent\"\n        with pytest.raises(errors.NotSupportedError):\n            main.install(self.config, plugins)\n\n    @mock.patch('certbot._internal.main.plug_sel.record_chosen_plugins')\n    @mock.patch('certbot._internal.main.plug_sel.pick_installer')\n    def test_install_enhancement_no_certname(self, mock_inst, _rec):\n        mock_inst.return_value = self.mockinstaller\n        plugins = disco.PluginsRegistry.find_all()\n        self.config.auto_hsts = True\n        self.config.certname = None\n        self.config.key_path = \"/tmp/nonexistent\"\n        self.config.cert_path = \"/tmp/nonexistent\"\n        with pytest.raises(errors.ConfigurationError):\n            main.install(self.config, plugins)\n\n\nclass ReportNewCertTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.main._report_new_cert and\n       certbot._internal.main._csr_report_new_cert.\n    \"\"\"\n\n    def setUp(self):\n        self.notify_patch = mock.patch('certbot._internal.main.display_util.notify')\n        self.mock_notify = self.notify_patch.start()\n\n        self.notafter_patch = mock.patch('certbot._internal.main.crypto_util.notAfter')\n        self.mock_notafter = self.notafter_patch.start()\n        self.mock_notafter.return_value = datetime.datetime(1970, 1, 1, 0, 0)\n\n    def tearDown(self):\n        self.notify_patch.stop()\n        self.notafter_patch.stop()\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.main import _report_new_cert\n        return _report_new_cert(*args, **kwargs)\n\n    @classmethod\n    def _call_csr(cls, *args, **kwargs):\n        from certbot._internal.main import _csr_report_new_cert\n        return _csr_report_new_cert(*args, **kwargs)\n\n    def test_report_dry_run(self):\n        self._call(mock.Mock(dry_run=True), None, None, None)\n        self.mock_notify.assert_called_with(\"The dry run was successful.\")\n\n    def test_csr_report_dry_run(self):\n        self._call_csr(mock.Mock(dry_run=True), None, None, None)\n        self.mock_notify.assert_called_with(\"The dry run was successful.\")\n\n    def test_report_no_paths(self):\n        with pytest.raises(AssertionError):\n            self._call(mock.Mock(dry_run=False), None, None, None)\n\n        with pytest.raises(AssertionError):\n            self._call_csr(mock.Mock(dry_run=False), None, None, None)\n\n    def test_report(self):\n        self._call(mock.Mock(dry_run=False),\n                  '/path/to/cert.pem', '/path/to/fullchain.pem',\n                  '/path/to/privkey.pem')\n\n        self.mock_notify.assert_called_with(\n            '\\nSuccessfully received certificate.\\n'\n            'Certificate is saved at: /path/to/fullchain.pem\\n'\n            'Key is saved at:         /path/to/privkey.pem\\n'\n            'This certificate expires on 1970-01-01.\\n'\n            'These files will be updated when the certificate renews.\\n'\n            'Certbot has set up a scheduled task to automatically renew this '\n            'certificate in the background.'\n        )\n\n    def test_report_no_key(self):\n        self._call(mock.Mock(dry_run=False),\n                  '/path/to/cert.pem', '/path/to/fullchain.pem',\n                  None)\n\n        self.mock_notify.assert_called_with(\n            '\\nSuccessfully received certificate.\\n'\n            'Certificate is saved at: /path/to/fullchain.pem\\n'\n            'This certificate expires on 1970-01-01.\\n'\n            'These files will be updated when the certificate renews.\\n'\n            'Certbot has set up a scheduled task to automatically renew this '\n            'certificate in the background.'\n        )\n\n    def test_report_no_preconfigured_renewal(self):\n        self._call(mock.Mock(dry_run=False, preconfigured_renewal=False),\n                  '/path/to/cert.pem', '/path/to/fullchain.pem',\n                  '/path/to/privkey.pem')\n\n        self.mock_notify.assert_called_with(\n            '\\nSuccessfully received certificate.\\n'\n            'Certificate is saved at: /path/to/fullchain.pem\\n'\n            'Key is saved at:         /path/to/privkey.pem\\n'\n            'This certificate expires on 1970-01-01.\\n'\n            'These files will be updated when the certificate renews.'\n        )\n\n    def test_csr_report(self):\n        self._call_csr(mock.Mock(dry_run=False), '/path/to/cert.pem',\n                      '/path/to/chain.pem', '/path/to/fullchain.pem')\n\n        self.mock_notify.assert_called_with(\n            '\\nSuccessfully received certificate.\\n'\n            'Certificate is saved at:            /path/to/cert.pem\\n'\n            'Intermediate CA chain is saved at:  /path/to/chain.pem\\n'\n            'Full certificate chain is saved at: /path/to/fullchain.pem\\n'\n            'This certificate expires on 1970-01-01.'\n        )\n\n    def test_manual_no_hooks_report(self):\n        \"\"\"Shouldn't get a message about autorenewal if no --manual-auth-hook\"\"\"\n        self._call(mock.Mock(dry_run=False, authenticator='manual', manual_auth_hook=None),\n                  '/path/to/cert.pem', '/path/to/fullchain.pem',\n                  '/path/to/privkey.pem')\n\n        self.mock_notify.assert_called_with(\n            '\\nSuccessfully received certificate.\\n'\n            'Certificate is saved at: /path/to/fullchain.pem\\n'\n            'Key is saved at:         /path/to/privkey.pem\\n'\n            'This certificate expires on 1970-01-01.\\n'\n            'These files will be updated when the certificate renews.'\n        )\n\n\nclass ReportNextStepsTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.main._report_next_steps\"\"\"\n\n    def setUp(self):\n        self.config = mock.MagicMock(\n            cert_name=\"example.com\", preconfigured_renewal=True,\n            csr=None, authenticator=\"nginx\", manual_auth_hook=None)\n        notify_patch = mock.patch('certbot._internal.main.display_util.notify')\n        self.mock_notify = notify_patch.start()\n        self.addCleanup(notify_patch.stop)\n        self.old_stdout = sys.stdout\n        sys.stdout = io.StringIO()\n\n    def tearDown(self):\n        sys.stdout = self.old_stdout\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.main import _report_next_steps\n        _report_next_steps(*args, **kwargs)\n\n    def _output(self) -> str:\n        assert self.mock_notify.call_count == 2\n        assert self.mock_notify.call_args_list[0][0][0] == 'NEXT STEPS:'\n        return self.mock_notify.call_args_list[1][0][0]\n\n    def test_report(self):\n        \"\"\"No steps for a normal renewal\"\"\"\n        self.config.authenticator = \"manual\"\n        self.config.manual_auth_hook = \"/bin/true\"\n        self._call(self.config, None, None)\n        self.mock_notify.assert_not_called()\n\n    def test_csr_report(self):\n        \"\"\"--csr requires manual renewal\"\"\"\n        self.config.csr = \"foo.csr\"\n        self._call(self.config, None, None)\n        assert \"--csr will not be renewed\" in self._output()\n\n    def test_manual_no_hook_renewal(self):\n        \"\"\"--manual without a hook requires manual renewal\"\"\"\n        self.config.authenticator = \"manual\"\n        self._call(self.config, None, None)\n        assert \"--manual certificates requires\" in self._output()\n\n    def test_no_preconfigured_renewal(self):\n        \"\"\"No --preconfigured-renewal needs manual cron setup\"\"\"\n        self.config.preconfigured_renewal = False\n        self._call(self.config, None, None)\n        assert \"https://certbot.org/renewal-setup\" in self._output()\n\n\nclass UpdateAccountTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.update_account\"\"\"\n\n    def setUp(self):\n        patches = {\n            'account': mock.patch('certbot._internal.main.account'),\n            'atexit': mock.patch('certbot.util.atexit'),\n            'client': mock.patch('certbot._internal.main.client'),\n            'determine_account': mock.patch('certbot._internal.main._determine_account'),\n            'notify': mock.patch('certbot._internal.main.display_util.notify'),\n            'prepare_sub': mock.patch('certbot._internal.eff.prepare_subscription'),\n            'util': test_util.patch_display_util()\n        }\n        self.mocks = { k: patches[k].start() for k in patches }\n        for patch in patches.values():\n            self.addCleanup(patch.stop)\n\n        return super().setUp()\n\n    def _call(self, args):\n        with mock.patch('certbot._internal.main.sys.stdout'), \\\n             mock.patch('certbot._internal.main.sys.stderr'):\n            args = ['--config-dir', self.config.config_dir,\n                    '--work-dir', self.config.work_dir,\n                    '--logs-dir', self.config.logs_dir, '--text'] + args\n            return main.main(args[:]) # NOTE: parser can alter its args!\n\n    def _prepare_mock_account(self):\n        mock_storage = mock.MagicMock()\n        mock_account = mock.MagicMock()\n        mock_regr = mock.MagicMock()\n        mock_storage.find_all.return_value = [mock_account]\n        self.mocks['account'].AccountFileStorage.return_value = mock_storage\n        mock_account.regr.body = mock_regr.body\n        self.mocks['determine_account'].return_value = (mock_account, mock.MagicMock())\n        return (mock_account, mock_storage, mock_regr)\n\n    def _test_update_no_contact(self, args):\n        \"\"\"Utility to assert that email removal is handled correctly\"\"\"\n        (_, mock_storage, mock_regr) = self._prepare_mock_account()\n        result = self._call(args)\n        # When update succeeds, the return value of update_account() is None\n        assert result is None\n        # We submitted a registration to the server\n        assert self.mocks['client'].Client().acme.update_registration.call_count == 1\n        mock_regr.body.update.assert_called_with(contact=())\n        # We got an update from the server and persisted it\n        assert mock_storage.update_regr.call_count == 1\n        # We should have notified the user\n        self.mocks['notify'].assert_called_with(\n            'Any contact information associated with this account has been removed.'\n        )\n        # We should not have called subscription because there's no email\n        self.mocks['prepare_sub'].assert_not_called()\n\n    def test_no_existing_accounts(self):\n        \"\"\"Test that no existing account is handled correctly\"\"\"\n        mock_storage = mock.MagicMock()\n        mock_storage.find_all.return_value = []\n        self.mocks['account'].AccountFileStorage.return_value = mock_storage\n        assert self._call(['update_account', '--email', 'user@example.org']) == \\\n                         'Could not find an existing account for server' \\\n                         ' https://acme-v02.api.letsencrypt.org/directory.'\n\n    def test_update_account_remove_email(self):\n        \"\"\"Test that --register-unsafely-without-email is handled as no email\"\"\"\n        self._test_update_no_contact(['update_account', '--register-unsafely-without-email'])\n\n    def test_update_account_empty_email(self):\n        \"\"\"Test that providing an empty email is handled as no email\"\"\"\n        self._test_update_no_contact(['update_account', '-m', ''])\n\n    @mock.patch('certbot._internal.main.display_ops.get_email')\n    def test_update_account_with_email(self, mock_email):\n        \"\"\"Test that updating with a singular email is handled correctly\"\"\"\n        mock_email.return_value = 'user@example.com'\n        (_, mock_storage, _) = self._prepare_mock_account()\n        mock_client = mock.MagicMock()\n        self.mocks['client'].Client.return_value = mock_client\n\n        result = self._call(['update_account'])\n        # None if registration succeeds\n        assert result is None\n        # We should have updated the server\n        assert mock_client.acme.update_registration.call_count == 1\n        # We should have updated the account on disk\n        assert mock_storage.update_regr.call_count == 1\n        # Subscription should have been prompted\n        assert self.mocks['prepare_sub'].call_count == 1\n        # Should have printed the email\n        self.mocks['notify'].assert_called_with(\n            'Your e-mail address was updated to user@example.com.')\n\n    def test_update_account_with_multiple_emails(self):\n        \"\"\"Test that multiple email addresses are handled correctly\"\"\"\n        (_, mock_storage, mock_regr) = self._prepare_mock_account()\n        assert self._call(['update_account', '-m', 'user@example.com,user@example.org']) is None\n        mock_regr.body.update.assert_called_with(\n            contact=['mailto:user@example.com', 'mailto:user@example.org']\n        )\n        assert mock_storage.update_regr.call_count == 1\n        self.mocks['notify'].assert_called_with(\n            'Your e-mail address was updated to user@example.com,user@example.org.')\n\n\nclass ShowAccountTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.main.show_account\"\"\"\n\n    def setUp(self):\n        patches = {\n            'account': mock.patch('certbot._internal.main.account'),\n            'atexit': mock.patch('certbot.util.atexit'),\n            'client': mock.patch('certbot._internal.main.client'),\n            'determine_account': mock.patch('certbot._internal.main._determine_account'),\n            'notify': mock.patch('certbot._internal.main.display_util.notify'),\n            'util': test_util.patch_display_util()\n        }\n        self.mocks = { k: patches[k].start() for k in patches }\n        for patch in patches.values():\n            self.addCleanup(patch.stop)\n\n        return super().setUp()\n\n    def _call(self, args):\n        with mock.patch('certbot._internal.main.sys.stdout'), \\\n             mock.patch('certbot._internal.main.sys.stderr'):\n            args = ['--config-dir', self.config.config_dir,\n                    '--work-dir', self.config.work_dir,\n                    '--logs-dir', self.config.logs_dir, '--text'] + args\n            return main.main(args[:]) # NOTE: parser can alter its args!\n\n    def _prepare_mock_account(self):\n        mock_storage = mock.MagicMock()\n        mock_account = mock.MagicMock()\n        mock_regr = mock.MagicMock()\n        mock_storage.find_all.return_value = [mock_account]\n        self.mocks['account'].AccountFileStorage.return_value = mock_storage\n        mock_account.regr.body = mock_regr.body\n        mock_account.key.thumbprint.return_value = b'foobarbaz'\n        self.mocks['determine_account'].return_value = (mock_account, mock.MagicMock())\n\n    def _test_show_account(self, contact):\n        self._prepare_mock_account()\n        mock_client = mock.MagicMock()\n        mock_regr = mock.MagicMock()\n        mock_regr.body.contact = contact\n        mock_regr.uri = 'https://www.letsencrypt-demo.org/acme/reg/1'\n        mock_client.acme.query_registration.return_value = mock_regr\n        self.mocks['client'].Client.return_value = mock_client\n\n        args = ['show_account']\n\n        self._call(args)\n\n        assert mock_client.acme.query_registration.call_count == 1\n\n    def test_no_existing_accounts(self):\n        \"\"\"Test that no existing account is handled correctly\"\"\"\n        mock_storage = mock.MagicMock()\n        mock_storage.find_all.return_value = []\n        self.mocks['account'].AccountFileStorage.return_value = mock_storage\n        assert self._call(['show_account']) == \\\n                         'Could not find an existing account for server' \\\n                         ' https://acme-v02.api.letsencrypt.org/directory.'\n\n    def test_no_existing_client(self):\n        \"\"\"Test that issues with the ACME client are handled correctly\"\"\"\n        self._prepare_mock_account()\n        mock_client = mock.MagicMock()\n        mock_client.acme = None\n        self.mocks['client'].Client.return_value = mock_client\n        try:\n            self._call(['show_account'])\n        except errors.Error as e:\n            assert 'ACME client is not set.' == str(e)\n\n    def test_no_contacts(self):\n        self._test_show_account(())\n\n        assert self.mocks['notify'].call_count == 1\n        self.mocks['notify'].assert_has_calls([\n            mock.call('Account details for server https://acme-v02.api.letsencr'\n                      'ypt.org/directory:\\n  Account URL: https://www.letsencry'\n                      'pt-demo.org/acme/reg/1\\n  Account Thumbprint: Zm9vYmFyYmF6\\n'\n                      '  Email contact: none')])\n\n    def test_single_email(self):\n        contact = ('mailto:foo@example.com',)\n        self._test_show_account(contact)\n\n        assert self.mocks['notify'].call_count == 1\n        self.mocks['notify'].assert_has_calls([\n            mock.call('Account details for server https://acme-v02.api.letsencr'\n                      'ypt.org/directory:\\n  Account URL: https://www.letsencry'\n                      'pt-demo.org/acme/reg/1\\n  Account Thumbprint: Zm9vYmFyYmF6'\n                      '\\n  Email contact: foo@example.com')])\n\n    def test_double_email(self):\n        contact = ('mailto:foo@example.com', 'mailto:bar@example.com')\n        self._test_show_account(contact)\n\n        assert self.mocks['notify'].call_count == 1\n        self.mocks['notify'].assert_has_calls([\n            mock.call('Account details for server https://acme-v02.api.letsencr'\n                      'ypt.org/directory:\\n  Account URL: https://www.letsencry'\n                      'pt-demo.org/acme/reg/1\\n  Account Thumbprint: Zm9vYmFyYmF6\\n'\n                      '  Email contacts: foo@example.com, bar@example.com')])\n\n\nclass TestLockOrder:\n    \"\"\"Tests that Certbot's directory locks were acquired in the right order.\"\"\"\n    EXPECTED_ERROR_TYPE = errors.Error\n    EXPECTED_ERROR_STR = 'Expected TestLockOrder error'\n    # This regex is needed because certbot renew captures raised errors and\n    # raises its own.\n    EXPECTED_ERROR_STR_REGEX = f'{EXPECTED_ERROR_STR}|1 renew failure'\n\n    @pytest.fixture\n    def mock_lock_dir(self):\n        with mock.patch('certbot._internal.lock.lock_dir') as mock_lock_dir:\n            yield mock_lock_dir\n\n    @contextlib.contextmanager\n    def mock_plugin_prepare(self, authenticator_dir, installer_dir, mock_lock_dir, subcommand):\n        \"\"\"Patches plugin prepare to call mock_lock_dir and raise the expected error.\"\"\"\n        def authenticator_lock(unused_self):\n            mock_lock_dir(authenticator_dir)\n            raise self.EXPECTED_ERROR_TYPE(self.EXPECTED_ERROR_STR)\n\n        def installer_lock(unused_self):\n            mock_lock_dir(installer_dir)\n            # Unless an installer isn't needed (e.g. certbot install), we\n            # expect the authenticator to raise the expected error because it\n            # is prepared last. See\n            # https://github.com/certbot/certbot/blob/7a6752a68ed77e73c2b29ab20d3ca8927f4fa7b0/certbot/certbot/_internal/plugins/selection.py#L246-L249\n            if subcommand == 'install':\n                raise self.EXPECTED_ERROR_TYPE(self.EXPECTED_ERROR_STR)\n\n        with mock.patch.object(standalone.Authenticator, 'prepare', authenticator_lock):\n            with mock.patch.object(null.Installer, 'prepare', installer_lock):\n                yield\n\n    @pytest.fixture(params='certonly install renew run'.split())\n    def args_and_lock_order(self, mock_lock_dir, request, tmp_path):\n        \"\"\"Sets up Certbot with args and mocks to error after acquiring the last lock.\n\n        This fixture yields the CLI arguments that should be given to Certbot\n        and the expected order of directories to be locked. An error is raised\n        after acquiring the last lock just as a means of stopping Certbot's\n        execution.\n\n        \"\"\"\n        # select directories\n        authenticator_dir = str(tmp_path / 'authenticator')\n        config_dir = str(tmp_path / 'config')\n        installer_dir = str(tmp_path / 'installer')\n        logs_dir = str(tmp_path / 'logs')\n        work_dir = str(tmp_path / 'work')\n\n        # prepare args and lineage\n        subcommand = request.param\n        args = [subcommand, '-a', 'standalone', '-i', 'null', '--no-random-sleep-on-renew',\n                '--config-dir', config_dir, '--logs-dir', logs_dir, '--work-dir',\n                work_dir]\n        test_util.make_lineage(config_dir, 'sample-renewal.conf')\n\n        with self.mock_plugin_prepare(authenticator_dir, installer_dir, mock_lock_dir, subcommand):\n            lock_order = [logs_dir, config_dir, work_dir, installer_dir]\n            if subcommand == 'install':\n                yield args, lock_order\n            else:\n                # We expect the installer to be prepared even for certonly\n                # because an installer was requested on the command line.\n                yield args, lock_order + [authenticator_dir]\n\n    def test_lock_order(self, args_and_lock_order, mock_lock_dir):\n        args, lock_order = args_and_lock_order\n        with pytest.raises(self.EXPECTED_ERROR_TYPE, match=self.EXPECTED_ERROR_STR_REGEX):\n            main.main(args)\n        assert mock_lock_dir.call_count == len(lock_order)\n        for call, locked_dir in zip(mock_lock_dir.call_args_list, lock_order):\n            assert call[0][0] == locked_dir\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/ocsp_test.py",
    "content": "\"\"\"Tests for ocsp.py\"\"\"\n# pylint: disable=protected-access\nimport contextlib\nfrom datetime import datetime\nfrom datetime import timedelta\nfrom datetime import timezone\nimport sys\nimport unittest\nfrom unittest import mock\n\nfrom cryptography import x509\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.exceptions import UnsupportedAlgorithm\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.x509 import ocsp as ocsp_lib\nimport pytest\n\nfrom certbot import crypto_util\nfrom certbot.tests import util as test_util\n\n\nclass OSCPTestCryptography(unittest.TestCase):\n    \"\"\"\n    OCSP revokation tests using Cryptography >= 2.4.0\n    \"\"\"\n\n    def setUp(self):\n        from certbot._internal import ocsp\n        self.checker = ocsp.RevocationChecker()\n        self.cert_path = test_util.vector_path('ocsp_certificate.pem')\n        self.chain_path = test_util.vector_path('ocsp_issuer_certificate.pem')\n        self.cert_obj = mock.MagicMock()\n        self.cert_obj.cert_path = self.cert_path\n        self.cert_obj.chain_path = self.chain_path\n        now = datetime.now(timezone.utc)\n        self.mock_notAfter = mock.patch('certbot._internal.ocsp.crypto_util.notAfter',\n                                        return_value=now + timedelta(hours=2))\n        self.mock_notAfter.start()\n        # Ensure the mock.patch is stopped even if test raises an exception\n        self.addCleanup(self.mock_notAfter.stop)\n\n    @mock.patch('certbot._internal.ocsp._determine_ocsp_server')\n    @mock.patch('certbot._internal.ocsp._check_ocsp_cryptography')\n    def test_ensure_cryptography_toggled(self, mock_check, mock_determine):\n        mock_determine.return_value = ('http://example.com', 'example.com')\n        self.checker.ocsp_revoked(self.cert_obj)\n\n        mock_check.assert_called_once_with(self.cert_path, self.chain_path, 'http://example.com', 10)\n\n    def test_revoke(self):\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked\n\n    def test_responder_is_issuer(self):\n        issuer = x509.load_pem_x509_certificate(\n            test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())\n\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,\n                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:\n            # OCSP response with ResponseID as Name\n            mocks['mock_response'].return_value.responder_name = issuer.subject\n            mocks['mock_response'].return_value.responder_key_hash = None\n            self.checker.ocsp_revoked(self.cert_obj)\n            # OCSP response with ResponseID as KeyHash\n            key_hash = x509.SubjectKeyIdentifier.from_public_key(issuer.public_key()).digest\n            mocks['mock_response'].return_value.responder_name = None\n            mocks['mock_response'].return_value.responder_key_hash = key_hash\n            self.checker.ocsp_revoked(self.cert_obj)\n\n        # Here responder and issuer are the same. So only the signature of the OCSP\n        # response is checked (using the issuer/responder public key).\n        assert mocks['mock_check'].call_count == 2\n        assert mocks['mock_check'].call_args_list[0][0][0].public_numbers() == \\\n            issuer.public_key().public_numbers()\n        assert mocks['mock_check'].call_args_list[1][0][0].public_numbers() == \\\n            issuer.public_key().public_numbers()\n\n    def test_responder_is_authorized_delegate(self):\n        issuer = x509.load_pem_x509_certificate(\n            test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())\n        responder = x509.load_pem_x509_certificate(\n            test_util.load_vector('ocsp_responder_certificate.pem'), default_backend())\n\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,\n                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:\n            # OCSP response with ResponseID as Name\n            mocks['mock_response'].return_value.responder_name = responder.subject\n            mocks['mock_response'].return_value.responder_key_hash = None\n            self.checker.ocsp_revoked(self.cert_obj)\n            # OCSP response with ResponseID as KeyHash\n            key_hash = x509.SubjectKeyIdentifier.from_public_key(responder.public_key()).digest\n            mocks['mock_response'].return_value.responder_name = None\n            mocks['mock_response'].return_value.responder_key_hash = key_hash\n            self.checker.ocsp_revoked(self.cert_obj)\n\n        # Here responder and issuer are not the same. Two signatures will be checked then,\n        # first to verify the responder cert (using the issuer public key), second to\n        # to verify the OCSP response itself (using the responder public key).\n        assert mocks['mock_check'].call_count == 4\n        assert mocks['mock_check'].call_args_list[0][0][0].public_numbers() == \\\n                         issuer.public_key().public_numbers()\n        assert mocks['mock_check'].call_args_list[1][0][0].public_numbers() == \\\n                         responder.public_key().public_numbers()\n        assert mocks['mock_check'].call_args_list[2][0][0].public_numbers() == \\\n                         issuer.public_key().public_numbers()\n        assert mocks['mock_check'].call_args_list[3][0][0].public_numbers() == \\\n                         responder.public_key().public_numbers()\n\n    def test_revoke_resiliency(self):\n        # Server return an invalid HTTP response\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,\n                        http_status_code=400):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # OCSP response in invalid\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.UNAUTHORIZED):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # OCSP response is valid, but certificate status is unknown\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.UNKNOWN, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # The OCSP response says that the certificate is revoked, but certificate\n        # does not contain the OCSP extension.\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):\n            with mock.patch('cryptography.x509.Extensions.get_extension_for_class',\n                            side_effect=x509.ExtensionNotFound(\n                                'Not found', x509.AuthorityInformationAccessOID.OCSP)):\n                revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # OCSP response uses an unsupported signature.\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,\n                        check_signature_side_effect=UnsupportedAlgorithm('foo')):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # OSCP signature response is invalid.\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,\n                        check_signature_side_effect=InvalidSignature('foo')):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # Assertion error on OCSP response validity\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL,\n                        check_signature_side_effect=AssertionError('foo')):\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # No responder cert in OCSP response\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,\n                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:\n            mocks['mock_response'].return_value.certificates = []\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        # Responder cert is not signed by certificate issuer\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED,\n                        ocsp_lib.OCSPResponseStatus.SUCCESSFUL) as mocks:\n            cert = mocks['mock_response'].return_value.certificates[0]\n            mocks['mock_response'].return_value.certificates[0] = mock.Mock(\n                issuer='fake', subject=cert.subject)\n            revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n        with _ocsp_mock(ocsp_lib.OCSPCertStatus.REVOKED, ocsp_lib.OCSPResponseStatus.SUCCESSFUL):\n            # This mock is necessary to avoid the first call contained in _determine_ocsp_server\n            # of the method cryptography.x509.Extensions.get_extension_for_class.\n            with mock.patch('certbot._internal.ocsp._determine_ocsp_server') as mock_server:\n                mock_server.return_value = ('https://example.com', 'example.com')\n                with mock.patch('cryptography.x509.Extensions.get_extension_for_class',\n                                side_effect=x509.ExtensionNotFound(\n                                    'Not found', x509.AuthorityInformationAccessOID.OCSP)):\n                    revoked = self.checker.ocsp_revoked(self.cert_obj)\n        assert revoked is False\n\n\nclass TestDeprecation:\n    \"\"\"Tests related to the deprecation of certbot.ocsp.\n\n    These tests can be deleted after this module is removed from Certbot.\n\n    \"\"\"\n    def test_deprecation_warning(self):\n        with pytest.warns(DeprecationWarning, match='certbot.ocsp is deprecated'):\n            import certbot.ocsp  # noqa: F401\n\n    def test_no_changes(self):\n        from certbot._internal import ocsp\n        expected_hash = '4f595b3c6e63749af1f71b5b4890b94e04734bb75f8bff95cf7d7a7e4752d5c1'\n        failure_message = ('Despite being prefixed by _internal, certbot._internal.ocsp is still '\n            'part of our public API while certbot.ocsp exists. You are free to make changes to '\n            'this file and update the hash in this test however, please be sure your changes do '\n            'not affect the API of the certbot.ocsp module.')\n        assert crypto_util.sha256sum(ocsp.__file__) == expected_hash, failure_message\n\n\n@contextlib.contextmanager\ndef _ocsp_mock(certificate_status, response_status,\n               http_status_code=200, check_signature_side_effect=None):\n    with mock.patch('certbot._internal.ocsp.ocsp.load_der_ocsp_response') as mock_response:\n        mock_response.return_value = _construct_mock_ocsp_response(\n            certificate_status, response_status)\n        with mock.patch('certbot._internal.ocsp.requests.post') as mock_post:\n            mock_post.return_value = mock.Mock(status_code=http_status_code)\n            with mock.patch('certbot._internal.ocsp.crypto_util.verify_signed_payload') \\\n                as mock_check:\n                if check_signature_side_effect:\n                    mock_check.side_effect = check_signature_side_effect\n                yield {\n                    'mock_response': mock_response,\n                    'mock_post': mock_post,\n                    'mock_check': mock_check,\n                }\n\n\ndef _construct_mock_ocsp_response(certificate_status, response_status):\n    cert = x509.load_pem_x509_certificate(\n        test_util.load_vector('ocsp_certificate.pem'), default_backend())\n    issuer = x509.load_pem_x509_certificate(\n        test_util.load_vector('ocsp_issuer_certificate.pem'), default_backend())\n    responder = x509.load_pem_x509_certificate(\n        test_util.load_vector('ocsp_responder_certificate.pem'), default_backend())\n    builder = ocsp_lib.OCSPRequestBuilder()\n    builder = builder.add_certificate(cert, issuer, hashes.SHA1())\n    request = builder.build()\n\n    return mock.Mock(\n        response_status=response_status,\n        certificate_status=certificate_status,\n        serial_number=request.serial_number,\n        issuer_key_hash=request.issuer_key_hash,\n        issuer_name_hash=request.issuer_name_hash,\n        responder_name=responder.subject,\n        certificates=[responder],\n        hash_algorithm=hashes.SHA1(),\n        next_update_utc=datetime.now(timezone.utc) + timedelta(days=1),\n        this_update_utc=datetime.now(timezone.utc) - timedelta(days=1),\n        signature_algorithm_oid=x509.oid.SignatureAlgorithmOID.RSA_WITH_SHA1,\n    )\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/__init__.py",
    "content": "\"\"\"Certbot Plugins Tests\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/common_test.py",
    "content": "\"\"\"Tests for certbot.plugins.common.\"\"\"\nimport functools\nimport shutil\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\nAUTH_KEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\nACHALL = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(challenges.HTTP01(token=b'token1'),\n                                             messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"encryption-example.demo\"),\n            account_key=AUTH_KEY)\n\n\nclass NamespaceFunctionsTest(unittest.TestCase):\n    \"\"\"Tests for certbot.plugins.common.*_namespace functions.\"\"\"\n\n    def test_option_namespace(self):\n        from certbot.plugins.common import option_namespace\n        assert \"foo-\" == option_namespace(\"foo\")\n\n    def test_dest_namespace(self):\n        from certbot.plugins.common import dest_namespace\n        assert \"foo_\" == dest_namespace(\"foo\")\n\n    def test_dest_namespace_with_dashes(self):\n        from certbot.plugins.common import dest_namespace\n        assert \"foo_bar_\" == dest_namespace(\"foo-bar\")\n\n\nclass PluginTest(unittest.TestCase):\n    \"\"\"Test for certbot.plugins.common.Plugin.\"\"\"\n\n    def setUp(self):\n        from certbot.plugins.common import Plugin\n\n        class MockPlugin(Plugin):  # pylint: disable=missing-docstring\n            def prepare(self) -> None:\n                pass\n\n            def more_info(self) -> str:\n                return \"info\"\n\n            @classmethod\n            def add_parser_arguments(cls, add):\n                add(\"foo-bar\", dest=\"different_to_foo_bar\", x=1, y=None)\n\n        self.plugin_cls = MockPlugin\n        self.config = mock.MagicMock()\n        self.plugin = MockPlugin(config=self.config, name=\"mock\")\n\n    def test_init(self):\n        assert \"mock\" == self.plugin.name\n        assert self.config == self.plugin.config\n\n    def test_option_namespace(self):\n        assert \"mock-\" == self.plugin.option_namespace\n\n    def test_option_name(self):\n        assert \"mock-foo_bar\" == self.plugin.option_name(\"foo_bar\")\n\n    def test_dest_namespace(self):\n        assert \"mock_\" == self.plugin.dest_namespace\n\n    def test_dest(self):\n        assert \"mock_foo_bar\" == self.plugin.dest(\"foo-bar\")\n        assert \"mock_foo_bar\" == self.plugin.dest(\"foo_bar\")\n\n    def test_conf(self):\n        assert self.config.mock_foo_bar == self.plugin.conf(\"foo-bar\")\n\n    def test_inject_parser_options(self):\n        parser = mock.MagicMock()\n        self.plugin_cls.inject_parser_options(parser, \"mock\")\n        # note that inject_parser_options doesn't check if dest has\n        # correct prefix\n        parser.add_argument.assert_called_once_with(\n            \"--mock-foo-bar\", dest=\"different_to_foo_bar\", x=1, y=None)\n\n    def test_fallback_auth_hint(self):\n        assert \"the mock plugin completed the required dns-01 challenges\" in \\\n                      self.plugin.auth_hint([acme_util.DNS01_A, acme_util.DNS01_A])\n        assert \"the mock plugin completed the required dns-01 and http-01 challenges\" in \\\n                      self.plugin.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A,\n                                             acme_util.DNS01_A])\n\n\nclass InstallerTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot.plugins.common.Installer.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        filesystem.mkdir(self.config.config_dir)\n        from certbot.tests.util import DummyInstaller\n\n        self.installer = DummyInstaller(config=self.config,\n                                   name=\"Installer\")\n        self.reverter = self.installer.reverter\n\n    def test_add_to_real_checkpoint(self):\n        files = {\"foo.bar\", \"baz.qux\",}\n        save_notes = \"foo bar baz qux\"\n        self._test_wrapped_method(\"add_to_checkpoint\", files, save_notes)\n\n    def test_add_to_real_checkpoint2(self):\n        self._test_add_to_checkpoint_common(False)\n\n    def test_add_to_temporary_checkpoint(self):\n        self._test_add_to_checkpoint_common(True)\n\n    def _test_add_to_checkpoint_common(self, temporary):\n        files = {\"foo.bar\", \"baz.qux\",}\n        save_notes = \"foo bar baz qux\"\n\n        installer_func = functools.partial(self.installer.add_to_checkpoint,\n                                           temporary=temporary)\n\n        if temporary:\n            reverter_func_name = \"add_to_temp_checkpoint\"\n        else:\n            reverter_func_name = \"add_to_checkpoint\"\n\n        self._test_adapted_method(installer_func, reverter_func_name, files, save_notes)\n\n    def test_finalize_checkpoint(self):\n        self._test_wrapped_method(\"finalize_checkpoint\", \"foo\")\n\n    def test_recovery_routine(self):\n        self._test_wrapped_method(\"recovery_routine\")\n\n    def test_revert_temporary_config(self):\n        self._test_wrapped_method(\"revert_temporary_config\")\n\n    def test_rollback_checkpoints(self):\n        self._test_wrapped_method(\"rollback_checkpoints\", 42)\n\n    def _test_wrapped_method(self, name, *args, **kwargs):\n        \"\"\"Test a wrapped reverter method.\n\n        :param str name: name of the method to test\n        :param tuple args: position arguments to method\n        :param dict kwargs: keyword arguments to method\n\n        \"\"\"\n        installer_func = getattr(self.installer, name)\n        self._test_adapted_method(installer_func, name, *args, **kwargs)\n\n    def _test_adapted_method(self, installer_func,\n                             reverter_func_name, *passed_args, **passed_kwargs):\n        \"\"\"Test an adapted reverter method\n\n        :param callable installer_func: installer method to test\n        :param str reverter_func_name: name of the method on the\n            reverter that should be called\n        :param tuple passed_args: positional arguments passed from\n            installer method to the reverter method\n        :param dict passed_kargs: keyword arguments passed from\n            installer method to the reverter method\n\n        \"\"\"\n        with mock.patch.object(self.reverter, reverter_func_name) as reverter_func:\n            installer_func(*passed_args, **passed_kwargs)\n            reverter_func.assert_called_once_with(*passed_args, **passed_kwargs)\n            reverter_func.side_effect = errors.ReverterError\n            with pytest.raises(errors.PluginError):\n                installer_func(*passed_args, **passed_kwargs)\n\n    def test_install_ssl_dhparams(self):\n        self.installer.install_ssl_dhparams()\n        assert os.path.isfile(self.installer.ssl_dhparams)\n\n    def _current_ssl_dhparams_hash(self):\n        from certbot._internal.constants import SSL_DHPARAMS_SRC\n        return crypto_util.sha256sum(SSL_DHPARAMS_SRC)\n\n    def test_current_file_hash_in_all_hashes(self):\n        from certbot._internal.constants import ALL_SSL_DHPARAMS_HASHES\n        assert self._current_ssl_dhparams_hash() in ALL_SSL_DHPARAMS_HASHES, \\\n            \"Constants.ALL_SSL_DHPARAMS_HASHES must be appended\" \\\n            \" with the sha256 hash of self.config.ssl_dhparams when it is updated.\"\n\n\nclass AddrTest(unittest.TestCase):\n    \"\"\"Tests for certbot.plugins.common.Addr.\"\"\"\n\n    def setUp(self):\n        from certbot.plugins.common import Addr\n        self.addr1 = Addr.fromstring(\"192.168.1.1\")\n        self.addr2 = Addr.fromstring(\"192.168.1.1:*\")\n        self.addr3 = Addr.fromstring(\"192.168.1.1:80\")\n        self.addr4 = Addr.fromstring(\"[fe00::1]\")\n        self.addr5 = Addr.fromstring(\"[fe00::1]:*\")\n        self.addr6 = Addr.fromstring(\"[fe00::1]:80\")\n        self.addr7 = Addr.fromstring(\"[fe00::1]:5\")\n        self.addr8 = Addr.fromstring(\"[fe00:1:2:3:4:5:6:7:8:9]:8080\")\n\n    def test_fromstring(self):\n        assert self.addr1.get_addr() == \"192.168.1.1\"\n        assert self.addr1.get_port() == \"\"\n        assert self.addr2.get_addr() == \"192.168.1.1\"\n        assert self.addr2.get_port() == \"*\"\n        assert self.addr3.get_addr() == \"192.168.1.1\"\n        assert self.addr3.get_port() == \"80\"\n        assert self.addr4.get_addr() == \"[fe00::1]\"\n        assert self.addr4.get_port() == \"\"\n        assert self.addr5.get_addr() == \"[fe00::1]\"\n        assert self.addr5.get_port() == \"*\"\n        assert self.addr6.get_addr() == \"[fe00::1]\"\n        assert self.addr6.get_port() == \"80\"\n        assert self.addr6.get_ipv6_exploded() == \\\n                         \"fe00:0:0:0:0:0:0:1\"\n        assert self.addr1.get_ipv6_exploded() == \\\n                         \"\"\n        assert self.addr7.get_port() == \"5\"\n        assert self.addr8.get_ipv6_exploded() == \\\n                         \"fe00:1:2:3:4:5:6:7\"\n\n    def test_str(self):\n        assert str(self.addr1) == \"192.168.1.1\"\n        assert str(self.addr2) == \"192.168.1.1:*\"\n        assert str(self.addr3) == \"192.168.1.1:80\"\n        assert str(self.addr4) == \"[fe00::1]\"\n        assert str(self.addr5) == \"[fe00::1]:*\"\n        assert str(self.addr6) == \"[fe00::1]:80\"\n\n    def test_get_addr_obj(self):\n        assert str(self.addr1.get_addr_obj(\"443\")) == \"192.168.1.1:443\"\n        assert str(self.addr2.get_addr_obj(\"\")) == \"192.168.1.1\"\n        assert str(self.addr1.get_addr_obj(\"*\")) == \"192.168.1.1:*\"\n        assert str(self.addr4.get_addr_obj(\"443\")) == \"[fe00::1]:443\"\n        assert str(self.addr5.get_addr_obj(\"\")) == \"[fe00::1]\"\n        assert str(self.addr4.get_addr_obj(\"*\")) == \"[fe00::1]:*\"\n\n    def test_eq(self):\n        assert self.addr1 == self.addr2.get_addr_obj(\"\")\n        assert self.addr1 != self.addr2\n        assert self.addr1 != 3333\n\n        assert self.addr4 == self.addr4.get_addr_obj(\"\")\n        assert self.addr4 != self.addr5\n        assert self.addr4 != 3333\n        from certbot.plugins.common import Addr\n        assert self.addr4 == Addr.fromstring(\"[fe00:0:0::1]\")\n        assert self.addr4 == Addr.fromstring(\"[fe00:0::0:0:1]\")\n\n\n    def test_set_inclusion(self):\n        from certbot.plugins.common import Addr\n        set_a = {self.addr1, self.addr2}\n        addr1b = Addr.fromstring(\"192.168.1.1\")\n        addr2b = Addr.fromstring(\"192.168.1.1:*\")\n        set_b = {addr1b, addr2b}\n\n        assert set_a == set_b\n\n        set_c = {self.addr4, self.addr5}\n        addr4b = Addr.fromstring(\"[fe00::1]\")\n        addr5b = Addr.fromstring(\"[fe00::1]:*\")\n        set_d = {addr4b, addr5b}\n\n        assert set_c == set_d\n\n\nclass ChallengePerformerTest(unittest.TestCase):\n    \"\"\"Tests for certbot.plugins.common.ChallengePerformer.\"\"\"\n\n    def setUp(self):\n        configurator = mock.MagicMock()\n\n        from certbot.plugins.common import ChallengePerformer\n        self.performer = ChallengePerformer(configurator)\n\n    def test_add_chall(self):\n        self.performer.add_chall(ACHALL, 0)\n        assert 1 == len(self.performer.achalls)\n        assert [0] == self.performer.indices\n\n    def test_perform(self):\n        with pytest.raises(NotImplementedError):\n            self.performer.perform()\n\n\nclass InstallVersionControlledFileTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.plugins.common.install_version_controlled_file.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.hashes = [\"someotherhash\"]\n        self.dest_path = os.path.join(self.tempdir, \"options-ssl-dest.conf\")\n        self.hash_path = os.path.join(self.tempdir, \".options-ssl-conf.txt\")\n        self.old_path = os.path.join(self.tempdir, \"options-ssl-old.conf\")\n        self.source_path = os.path.join(self.tempdir, \"options-ssl-src.conf\")\n        for path in (self.source_path, self.old_path,):\n            with open(path, \"w\") as f:\n                f.write(path)\n            self.hashes.append(crypto_util.sha256sum(path))\n\n    def _call(self):\n        from certbot.plugins.common import install_version_controlled_file\n        install_version_controlled_file(self.dest_path,\n                                        self.hash_path,\n                                        self.source_path,\n                                        self.hashes)\n\n    def _current_file_hash(self):\n        return crypto_util.sha256sum(self.source_path)\n\n    def _assert_current_file(self):\n        assert os.path.isfile(self.dest_path)\n        assert crypto_util.sha256sum(self.dest_path) == \\\n            self._current_file_hash()\n\n    def test_no_file(self):\n        assert not os.path.isfile(self.dest_path)\n        self._call()\n        self._assert_current_file()\n\n    def test_current_file(self):\n        # 1st iteration installs the file, the 2nd checks if it needs updating\n        for _ in range(2):\n            self._call()\n            self._assert_current_file()\n\n    def test_prev_file_updates_to_current(self):\n        shutil.copyfile(self.old_path, self.dest_path)\n        self._call()\n        self._assert_current_file()\n\n    def test_manually_modified_current_file_does_not_update(self):\n        self._call()\n        with open(self.dest_path, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.called is False\n        assert os.path.isfile(self.dest_path)\n        assert crypto_util.sha256sum(self.source_path) == \\\n            self._current_file_hash()\n        assert crypto_util.sha256sum(self.dest_path) != \\\n            self._current_file_hash()\n\n    def test_manually_modified_past_file_warns(self):\n        with open(self.dest_path, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with open(self.hash_path, \"w\") as f:\n            f.write(\"hashofanoldversion\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.call_args[0][0] == \\\n                \"%s has been manually modified; updated file \" \\\n                \"saved to %s. We recommend updating %s for security purposes.\"\n        assert crypto_util.sha256sum(self.source_path) == \\\n            self._current_file_hash()\n        # only print warning once\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.called is False\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/disco_test.py",
    "content": "\"\"\"Tests for certbot._internal.plugins.disco.\"\"\"\nimport functools\nimport string\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal.plugins import null\nfrom certbot._internal.plugins import standalone\nfrom certbot._internal.plugins import webroot\n\nif sys.version_info >= (3, 10):  # pragma: no cover\n    import importlib.metadata as importlib_metadata\nelse:\n    import importlib_metadata\n\n\nclass _EntryPointLoadFail(importlib_metadata.EntryPoint):\n    def load(self):\n        raise RuntimeError(\"Loading failure\")\n\n\nEP_SA = importlib_metadata.EntryPoint(\n    name=\"sa\",\n    value=\"certbot._internal.plugins.standalone:Authenticator\",\n    group=\"certbot.plugins\")\n\nEP_WR = importlib_metadata.EntryPoint(\n    name=\"wr\",\n    value=\"certbot._internal.plugins.webroot:Authenticator\",\n    group=\"certbot.plugins\")\n\nEP_SA_LOADFAIL = _EntryPointLoadFail(\n    name=\"sa\",\n    value=\"certbot._internal.plugins.standalone:Authenticator\",\n    group=\"certbot.plugins\")\n\n\nclass PluginEntryPointTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.disco.PluginEntryPoint.\"\"\"\n\n    def setUp(self):\n        self.ep1 = importlib_metadata.EntryPoint(\n            name=\"ep1\",\n            value=\"p1.ep1:Authenticator\",\n            group=\"certbot.plugins\")\n        self.ep1prim = importlib_metadata.EntryPoint(\n            name=\"ep1\",\n            value=\"p2.pe2:Authenticator\",\n            group=\"certbot.plugins\")\n        # nested\n        self.ep2 = importlib_metadata.EntryPoint(\n            name=\"ep2\",\n            value=\"p2.foo.ep2:Authenticator\",\n            group=\"certbot.plugins\")\n        # project name != top-level package name\n        self.ep3 = importlib_metadata.EntryPoint(\n            name=\"ep3\",\n            value=\"a.ep3:Authenticator\",\n            group=\"certbot.plugins\")\n\n        from certbot._internal.plugins.disco import PluginEntryPoint\n        self.plugin_ep = PluginEntryPoint(EP_SA)\n\n    def test_entry_point_to_plugin_name_not_prefixed(self):\n        from certbot._internal.plugins.disco import PluginEntryPoint\n\n        names = {\n            self.ep1: \"ep1\",\n            self.ep1prim: \"ep1\",\n            self.ep2: \"ep2\",\n            self.ep3: \"ep3\",\n            EP_SA: \"sa\",\n        }\n\n        for entry_point, name in names.items():\n            assert name == PluginEntryPoint.entry_point_to_plugin_name(entry_point)\n\n    def test_description(self):\n        assert \"server locally\" in self.plugin_ep.description\n\n    def test_description_with_name(self):\n        self.plugin_ep.plugin_cls = mock.MagicMock(description=\"Desc\")\n        assert \"Desc (sa)\" == self.plugin_ep.description_with_name\n\n    def test_long_description(self):\n        self.plugin_ep.plugin_cls = mock.MagicMock(\n            long_description=\"Long desc\")\n        assert \"Long desc\" == self.plugin_ep.long_description\n\n    def test_long_description_nonexistent(self):\n        self.plugin_ep.plugin_cls = mock.MagicMock(\n            description=\"Long desc not found\", spec=[\"description\"])\n        assert \"Long desc not found\" == self.plugin_ep.long_description\n\n    def test_ifaces(self):\n        assert self.plugin_ep.ifaces((interfaces.Authenticator,))\n        assert not self.plugin_ep.ifaces((interfaces.Installer,))\n        assert not self.plugin_ep.ifaces((\n            interfaces.Installer, interfaces.Authenticator))\n\n    def test__init__(self):\n        assert self.plugin_ep.initialized is False\n        assert self.plugin_ep.prepared is False\n        assert self.plugin_ep.misconfigured is False\n        assert self.plugin_ep.available is False\n        assert self.plugin_ep.problem is None\n        assert self.plugin_ep.entry_point is EP_SA\n        assert \"sa\" == self.plugin_ep.name\n\n        assert self.plugin_ep.plugin_cls is standalone.Authenticator\n\n    def test_init(self):\n        config = mock.MagicMock()\n        plugin = self.plugin_ep.init(config=config)\n        assert self.plugin_ep.initialized is True\n        assert plugin.config is config\n        # memoize!\n        assert self.plugin_ep.init() is plugin\n        assert plugin.config is config\n        # try to give different config\n        assert self.plugin_ep.init(123) is plugin\n        assert plugin.config is config\n\n        assert self.plugin_ep.prepared is False\n        assert self.plugin_ep.misconfigured is False\n        assert self.plugin_ep.available is False\n\n    def test_prepare(self):\n        config = mock.MagicMock()\n        self.plugin_ep.init(config=config)\n        self.plugin_ep.prepare()\n        assert self.plugin_ep.prepared\n        assert self.plugin_ep.misconfigured is False\n\n        # output doesn't matter that much, just test if it runs\n        str(self.plugin_ep)\n\n    def test_prepare_misconfigured(self):\n        plugin = mock.MagicMock()\n        plugin.prepare.side_effect = errors.MisconfigurationError\n        # pylint: disable=protected-access\n        self.plugin_ep._initialized = plugin\n        assert isinstance(self.plugin_ep.prepare(), errors.MisconfigurationError)\n        assert self.plugin_ep.prepared\n        assert self.plugin_ep.misconfigured\n        assert isinstance(self.plugin_ep.problem, errors.MisconfigurationError)\n        assert self.plugin_ep.available\n\n    def test_prepare_no_installation(self):\n        plugin = mock.MagicMock()\n        plugin.prepare.side_effect = errors.NoInstallationError\n        # pylint: disable=protected-access\n        self.plugin_ep._initialized = plugin\n        assert isinstance(self.plugin_ep.prepare(), errors.NoInstallationError)\n        assert self.plugin_ep.prepared is True\n        assert self.plugin_ep.misconfigured is False\n        assert self.plugin_ep.available is False\n\n    def test_prepare_generic_plugin_error(self):\n        plugin = mock.MagicMock()\n        plugin.prepare.side_effect = errors.PluginError\n        # pylint: disable=protected-access\n        self.plugin_ep._initialized = plugin\n        assert isinstance(self.plugin_ep.prepare(), errors.PluginError)\n        assert self.plugin_ep.prepared\n        assert self.plugin_ep.misconfigured is False\n        assert self.plugin_ep.available is False\n\n    def test_str(self):\n        output = str(self.plugin_ep)\n        assert \"Authenticator\" in output\n        assert \"Installer\" not in output\n        assert \"Plugin\" in output\n\n    def test_repr(self):\n        assert \"PluginEntryPoint#sa\" == repr(self.plugin_ep)\n\n\nclass PluginsRegistryTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.disco.PluginsRegistry.\"\"\"\n\n    @classmethod\n    def _create_new_registry(cls, plugins):\n        from certbot._internal.plugins.disco import PluginsRegistry\n        return PluginsRegistry(plugins)\n\n    def setUp(self):\n        self.plugin_ep = mock.MagicMock()\n        self.plugin_ep.name = \"mock\"\n        self.plugin_ep.__hash__.side_effect = TypeError\n        self.plugins = {self.plugin_ep.name: self.plugin_ep}\n        self.reg = self._create_new_registry(self.plugins)\n        self.ep1 = importlib_metadata.EntryPoint(\n            name=\"ep1\",\n            value=\"p1.ep1\",\n            group=\"certbot.plugins\")\n\n    def test_find_all(self):\n        from certbot._internal.plugins.disco import PluginsRegistry\n        with mock.patch(\"certbot._internal.plugins.disco.importlib_metadata\") as mock_meta:\n            mock_meta.entry_points.side_effect = [\n                [EP_SA], [EP_WR, self.ep1],\n            ]\n            with mock.patch.object(importlib_metadata.EntryPoint, 'load') as mock_load:\n                mock_load.side_effect = [\n                    standalone.Authenticator, webroot.Authenticator,\n                    null.Installer, null.Installer]\n                plugins = PluginsRegistry.find_all()\n        assert plugins[\"sa\"].plugin_cls is standalone.Authenticator\n        assert plugins[\"sa\"].entry_point is EP_SA\n        assert plugins[\"wr\"].plugin_cls is webroot.Authenticator\n        assert plugins[\"wr\"].entry_point is EP_WR\n        assert plugins[\"ep1\"].plugin_cls is null.Installer\n        assert plugins[\"ep1\"].entry_point is self.ep1\n        assert \"p1:ep1\" not in plugins\n\n    def test_find_all_error_message(self):\n        from certbot._internal.plugins.disco import PluginsRegistry\n        with mock.patch(\"certbot._internal.plugins.disco.importlib_metadata\") as mock_meta:\n            #EP_SA.load = None  # This triggers a TypeError when the entrypoint loads\n            mock_meta.entry_points.side_effect = [\n                [EP_SA_LOADFAIL], [EP_WR, self.ep1],\n            ]\n            with self.assertRaises(errors.PluginError) as cm:\n                PluginsRegistry.find_all()\n            assert \"standalone' plugin errored\" in str(cm.exception)\n\n    def test_getitem(self):\n        assert self.plugin_ep == self.reg[\"mock\"]\n\n    def test_iter(self):\n        assert [\"mock\"] == list(self.reg)\n\n    def test_len(self):\n        assert 0 == len(self._create_new_registry({}))\n        assert 1 == len(self.reg)\n\n    def test_init(self):\n        self.plugin_ep.init.return_value = \"baz\"\n        assert [\"baz\"] == self.reg.init(\"bar\")\n        self.plugin_ep.init.assert_called_once_with(\"bar\")\n\n    def test_filter(self):\n        assert self.plugins == \\\n            self.reg.filter(lambda p_ep: p_ep.name.startswith(\"m\"))\n        assert {} == self.reg.filter(lambda p_ep: p_ep.name.startswith(\"b\"))\n\n    def test_ifaces(self):\n        self.plugin_ep.ifaces.return_value = True\n        # pylint: disable=protected-access\n        assert self.plugins == self.reg.ifaces()._plugins\n        self.plugin_ep.ifaces.return_value = False\n        assert {} == self.reg.ifaces()._plugins\n\n    def test_prepare(self):\n        self.plugin_ep.prepare.return_value = \"baz\"\n        assert [\"baz\"] == self.reg.prepare()\n        self.plugin_ep.prepare.assert_called_once_with()\n\n    def test_prepare_order(self):\n        order: list[str] = []\n        plugins = {\n            c: mock.MagicMock(prepare=functools.partial(order.append, c))\n            for c in string.ascii_letters\n        }\n        reg = self._create_new_registry(plugins)\n        reg.prepare()\n        # order of prepare calls must be sorted to prevent deadlock\n        # caused by plugins acquiring locks during prepare\n        assert order == sorted(string.ascii_letters)\n\n    def test_available(self):\n        self.plugin_ep.available = True\n        # pylint: disable=protected-access\n        assert self.plugins == self.reg.available()._plugins\n        self.plugin_ep.available = False\n        assert {} == self.reg.available()._plugins\n\n    def test_find_init(self):\n        assert self.reg.find_init(mock.Mock()) is None\n        self.plugin_ep.initialized = True\n        assert self.reg.find_init(self.plugin_ep.init()) is self.plugin_ep\n\n    def test_repr(self):\n        self.plugin_ep.__repr__ = lambda _: \"PluginEntryPoint#mock\"\n        assert \"PluginsRegistry(PluginEntryPoint#mock)\" == \\\n                         repr(self.reg)\n\n    def test_str(self):\n        assert \"No plugins\" == str(self._create_new_registry({}))\n        self.plugin_ep.__str__ = lambda _: \"Mock\"\n        assert \"Mock\" == str(self.reg)\n        plugins = {self.plugin_ep.name: self.plugin_ep, \"foo\": \"Bar\"}\n        reg = self._create_new_registry(plugins)\n        assert \"Bar\\n\\nMock\" == str(reg)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/dns_common_test.py",
    "content": "\"\"\"Tests for certbot.plugins.dns_common.\"\"\"\n\nimport collections\nimport logging\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot.plugins import dns_common\nfrom certbot.plugins import dns_test_common\nfrom certbot.tests import util as test_util\n\n\nclass DNSAuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):\n    # pylint: disable=protected-access\n\n    class _FakeDNSAuthenticator(dns_common.DNSAuthenticator):\n        _setup_credentials = mock.MagicMock()\n        _perform = mock.MagicMock()\n        _cleanup = mock.MagicMock()\n\n        def more_info(self):  # pylint: disable=missing-docstring,no-self-use\n            return 'A fake authenticator for testing.'\n\n    class _FakeConfig:\n        fake_propagation_seconds = 0\n        fake_config_key = 1\n        fake_other_key = None\n        fake_file_path = None\n\n    def setUp(self):\n        super().setUp()\n\n        self.config = DNSAuthenticatorTest._FakeConfig()\n\n        self.auth = DNSAuthenticatorTest._FakeDNSAuthenticator(self.config, \"fake\")\n\n    @test_util.patch_display_util()\n    def test_perform(self, unused_mock_get_utility):\n        self.auth.perform([self.achall])\n\n        self.auth._perform.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY)\n\n    def test_cleanup(self):\n        self.auth._attempt_cleanup = True\n\n        self.auth.cleanup([self.achall])\n\n        self.auth._cleanup.assert_called_once_with(dns_test_common.DOMAIN, mock.ANY, mock.ANY)\n\n    @test_util.patch_display_util()\n    def test_prompt(self, mock_get_utility):\n        mock_display = mock_get_utility()\n        mock_display.input.side_effect = ((display_util.OK, \"\",),\n                                          (display_util.OK, \"value\",))\n\n        self.auth._configure(\"other_key\", \"\")\n        assert self.auth.config.fake_other_key == \"value\"\n\n    @test_util.patch_display_util()\n    def test_prompt_canceled(self, mock_get_utility):\n        mock_display = mock_get_utility()\n        mock_display.input.side_effect = ((display_util.CANCEL, \"c\",),)\n\n        with pytest.raises(errors.PluginError):\n            self.auth._configure(\"other_key\", \"\")\n\n    @test_util.patch_display_util()\n    def test_prompt_file(self, mock_get_utility):\n        path = os.path.join(self.tempdir, 'file.ini')\n        open(path, \"wb\").close()\n\n        mock_display = mock_get_utility()\n        mock_display.directory_select.side_effect = ((display_util.OK, \"\",),\n                                                     (display_util.OK, \"not-a-file.ini\",),\n                                                     (display_util.OK, self.tempdir),\n                                                     (display_util.OK, path,))\n\n        self.auth._configure_file(\"file_path\", \"\")\n        assert self.auth.config.fake_file_path == path\n\n    @test_util.patch_display_util()\n    def test_prompt_file_canceled(self, mock_get_utility):\n        mock_display = mock_get_utility()\n        mock_display.directory_select.side_effect = ((display_util.CANCEL, \"c\",),)\n\n        with pytest.raises(errors.PluginError):\n            self.auth._configure_file(\"file_path\", \"\")\n\n    def test_configure_credentials(self):\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"fake_test\": \"value\"}, path)\n        setattr(self.config, \"fake_credentials\", path)\n\n        credentials = self.auth._configure_credentials(\"credentials\", \"\", {\"test\": \"\"})\n\n        assert credentials.conf(\"test\") == \"value\"\n\n    @test_util.patch_display_util()\n    def test_prompt_credentials(self, mock_get_utility):\n        bad_path = os.path.join(self.tempdir, 'bad-file.ini')\n        dns_test_common.write({\"fake_other\": \"other_value\"}, bad_path)\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"fake_test\": \"value\"}, path)\n        setattr(self.config, \"fake_credentials\", \"\")\n\n        mock_display = mock_get_utility()\n        mock_display.directory_select.side_effect = ((display_util.OK, \"\",),\n                                                     (display_util.OK, \"not-a-file.ini\",),\n                                                     (display_util.OK, self.tempdir),\n                                                     (display_util.OK, bad_path),\n                                                     (display_util.OK, path,))\n\n        credentials = self.auth._configure_credentials(\"credentials\", \"\", {\"test\": \"\"})\n        assert credentials.conf(\"test\") == \"value\"\n\n    def test_auth_hint(self):\n        assert 'try increasing --fake-propagation-seconds (currently 0 seconds).' in \\\n            self.auth.auth_hint([mock.MagicMock()])\n\n\nclass CredentialsConfigurationTest(test_util.TempDirTestCase):\n    class _MockLoggingHandler(logging.Handler):\n        messages = None\n\n        def __init__(self, *args, **kwargs):\n            self.reset()\n            super().__init__(*args, **kwargs)\n\n        def emit(self, record):\n            self.messages[record.levelname.lower()].append(record.getMessage())\n\n        def reset(self):\n            \"\"\"Allows the handler to be reset between tests.\"\"\"\n            self.messages = collections.defaultdict(list)\n\n    def test_valid_file(self):\n        path = os.path.join(self.tempdir, 'too-permissive-file.ini')\n\n        dns_test_common.write({\"test\": \"value\", \"other\": 1}, path)\n\n        credentials_configuration = dns_common.CredentialsConfiguration(path)\n        assert \"value\" == credentials_configuration.conf(\"test\")\n        assert \"1\" == credentials_configuration.conf(\"other\")\n\n    def test_nonexistent_file(self):\n        path = os.path.join(self.tempdir, 'not-a-file.ini')\n\n        with pytest.raises(errors.PluginError):\n            dns_common.CredentialsConfiguration(path)\n\n    def test_valid_file_with_unsafe_permissions(self):\n        log = self._MockLoggingHandler()\n        dns_common.logger.addHandler(log)\n\n        path = os.path.join(self.tempdir, 'too-permissive-file.ini')\n        util.safe_open(path, \"wb\", 0o744).close()\n\n        dns_common.CredentialsConfiguration(path)\n\n        assert 1 == len([_ for _ in log.messages['warning'] if _.startswith(\"Unsafe\")])\n\n\nclass CredentialsConfigurationRequireTest(test_util.TempDirTestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.path = os.path.join(self.tempdir, 'file.ini')\n\n    def _write(self, values):\n        dns_test_common.write(values, self.path)\n\n    def test_valid(self):\n        self._write({\"test\": \"value\", \"other\": 1})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        credentials_configuration.require({\"test\": \"\", \"other\": \"\"})\n\n    def test_valid_but_extra(self):\n        self._write({\"test\": \"value\", \"other\": 1})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        credentials_configuration.require({\"test\": \"\"})\n\n    def test_valid_empty(self):\n        self._write({})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        credentials_configuration.require({})\n\n    def test_missing(self):\n        self._write({})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        with pytest.raises(errors.PluginError):\n            credentials_configuration.require({\"test\": \"\"})\n\n    def test_blank(self):\n        self._write({\"test\": \"\"})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        with pytest.raises(errors.PluginError):\n            credentials_configuration.require({\"test\": \"\"})\n\n    def test_typo(self):\n        self._write({\"tets\": \"typo!\"})\n\n        credentials_configuration = dns_common.CredentialsConfiguration(self.path)\n        with pytest.raises(errors.PluginError):\n            credentials_configuration.require({\"test\": \"\"})\n\n\nclass DomainNameGuessTest(unittest.TestCase):\n\n    def test_simple_case(self):\n        assert 'example.com' in \\\n            dns_common.base_domain_name_guesses(\"example.com\")\n\n    def test_sub_domain(self):\n        assert 'example.com' in \\\n            dns_common.base_domain_name_guesses(\"foo.bar.baz.example.com\")\n\n    def test_second_level_domain(self):\n        assert 'example.co.uk' in \\\n            dns_common.base_domain_name_guesses(\"foo.bar.baz.example.co.uk\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/enhancements_test.py",
    "content": "\"\"\"Tests for new style enhancements\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot._internal.plugins import null\nfrom certbot.plugins import enhancements\nimport certbot.tests.util as test_util\n\n\nclass EnhancementTest(test_util.ConfigTestCase):\n    \"\"\"Tests for new style enhancements in certbot.plugins.enhancements\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)\n\n    @test_util.patch_display_util()\n    def test_enhancement_enabled_enhancements(self, _):\n        FAKEINDEX = [\n            {\n                \"name\": \"autohsts\",\n                \"cli_dest\": \"auto_hsts\",\n            },\n            {\n                \"name\": \"somethingelse\",\n                \"cli_dest\": \"something\",\n            }\n        ]\n        with mock.patch(\"certbot.plugins.enhancements._INDEX\", FAKEINDEX):\n            self.config.auto_hsts = True\n            self.config.something = True\n            enabled = list(enhancements.enabled_enhancements(self.config))\n        assert len(enabled) == 2\n        assert [i for i in enabled if i[\"name\"] == \"autohsts\"]\n        assert [i for i in enabled if i[\"name\"] == \"somethingelse\"]\n\n    def test_are_requested(self):\n        assert len(list(enhancements.enabled_enhancements(self.config))) == 0\n        assert not enhancements.are_requested(self.config)\n        self.config.auto_hsts = True\n        assert len(list(enhancements.enabled_enhancements(self.config))) == 1\n        assert enhancements.are_requested(self.config)\n\n    def test_are_supported(self):\n        self.config.auto_hsts = True\n        unsupported = null.Installer(self.config, \"null\")\n        assert enhancements.are_supported(self.config, self.mockinstaller)\n        assert not enhancements.are_supported(self.config, unsupported)\n\n    def test_enable(self):\n        self.config.auto_hsts = True\n        domains = [\"example.com\", \"www.example.com\"]\n        lineage = \"lineage\"\n        enhancements.enable(lineage, domains, self.mockinstaller, self.config)\n        assert self.mockinstaller.enable_autohsts.called\n        assert self.mockinstaller.enable_autohsts.call_args[0] == (lineage, domains)\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/manual_test.py",
    "content": "\"\"\"Tests for certbot._internal.plugins.manual\"\"\"\nimport sys\nimport textwrap\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import challenges\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot._internal.plugins.manual.Authenticator.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        get_display_patch = test_util.patch_display_util()\n        self.mock_get_display = get_display_patch.start()\n        self.addCleanup(get_display_patch.stop)\n\n        self.http_achall = acme_util.HTTP01_A\n        self.dns_achall = acme_util.DNS01_A\n        self.dns_achall_2 = acme_util.DNS01_A_2\n        self.achalls = [self.http_achall, self.dns_achall, self.dns_achall_2]\n        for d in [\"config_dir\", \"work_dir\", \"in_progress\"]:\n            filesystem.mkdir(os.path.join(self.tempdir, d))\n            # \"backup_dir\" and \"temp_checkpoint_dir\" get created in\n            # certbot.util.make_or_verify_dir() during the Reverter\n            # initialization.\n        self.config = mock.MagicMock(\n            http01_port=0, manual_auth_hook=None, manual_cleanup_hook=None,\n            noninteractive_mode=False, validate_hooks=False,\n            config_dir=os.path.join(self.tempdir, \"config_dir\"),\n            work_dir=os.path.join(self.tempdir, \"work_dir\"),\n            backup_dir=os.path.join(self.tempdir, \"backup_dir\"),\n            temp_checkpoint_dir=os.path.join(\n                                        self.tempdir, \"temp_checkpoint_dir\"),\n            in_progress_dir=os.path.join(self.tempdir, \"in_progess\"))\n\n        from certbot._internal.plugins.manual import Authenticator\n        self.auth = Authenticator(self.config, name='manual')\n\n    def test_prepare_no_hook_noninteractive(self):\n        self.config.noninteractive_mode = True\n        with pytest.raises(errors.PluginError):\n            self.auth.prepare()\n\n    def test_prepare_bad_hook(self):\n        self.config.manual_auth_hook = os.path.abspath(os.sep)  # is / on UNIX\n        self.config.validate_hooks = True\n        with pytest.raises(errors.HookCommandNotFound):\n            self.auth.prepare()\n\n    def test_more_info(self):\n        assert isinstance(self.auth.more_info(), str)\n\n    def test_get_chall_pref(self):\n        assert self.auth.get_chall_pref('example.org') == \\\n                         [challenges.HTTP01, challenges.DNS01]\n\n    def test_script_perform(self):\n        self.config.manual_auth_hook = (\n            '{0} -c \"'\n            'from certbot.compat import os;'\n            'print(os.environ.get(\\'CERTBOT_DOMAIN\\'));'\n            'print(os.environ.get(\\'CERTBOT_IDENTIFIER\\'));'\n            'print(os.environ.get(\\'CERTBOT_TOKEN\\', \\'notoken\\'));'\n            'print(os.environ.get(\\'CERTBOT_VALIDATION\\', \\'novalidation\\'));'\n            'print(os.environ.get(\\'CERTBOT_ALL_DOMAINS\\'));'\n            'print(os.environ.get(\\'CERTBOT_ALL_IDENTIFIERS\\'));'\n            'print(os.environ.get(\\'CERTBOT_REMAINING_CHALLENGES\\'));\"'\n            .format(sys.executable))\n        dns_expected = '{0}\\n{1}\\n{2}\\n{3}\\n{4}\\n{5}\\n{6}'.format(\n            self.dns_achall.identifier.value,\n            self.dns_achall.identifier.value,\n            'notoken',\n            self.dns_achall.validation(self.dns_achall.account_key),\n            ','.join(achall.identifier.value for achall in self.achalls),\n            ','.join(achall.identifier.value for achall in self.achalls),\n            len(self.achalls) - self.achalls.index(self.dns_achall) - 1)\n        http_expected = '{0}\\n{1}\\n{2}\\n{3}\\n{4}\\n{5}\\n{6}'.format(\n            self.http_achall.identifier.value,\n            self.http_achall.identifier.value,\n            self.http_achall.chall.encode('token'),\n            self.http_achall.validation(self.http_achall.account_key),\n            ','.join(achall.identifier.value for achall in self.achalls),\n            ','.join(achall.identifier.value for achall in self.achalls),\n            len(self.achalls) - self.achalls.index(self.http_achall) - 1)\n\n        assert self.auth.perform(self.achalls) == \\\n            [achall.response(achall.account_key) for achall in self.achalls]\n        assert self.auth.env[self.dns_achall]['CERTBOT_AUTH_OUTPUT'] == \\\n            dns_expected\n        assert self.auth.env[self.http_achall]['CERTBOT_AUTH_OUTPUT'] == \\\n            http_expected\n\n        # Successful hook output should be sent to notify\n        assert self.mock_get_display().notification.call_count == len(self.achalls)\n        for i, (args, _) in enumerate(self.mock_get_display().notification.call_args_list):\n            needle = textwrap.indent(self.auth.env[self.achalls[i]]['CERTBOT_AUTH_OUTPUT'], ' ')\n            assert needle in args[0]\n\n    def test_manual_perform(self):\n        assert self.auth.perform(self.achalls) == \\\n            [achall.response(achall.account_key) for achall in self.achalls]\n\n        assert self.mock_get_display().notification.call_count == len(self.achalls)\n        for i, (args, kwargs) in enumerate(self.mock_get_display().notification.call_args_list):\n            achall = self.achalls[i]\n            assert achall.validation(achall.account_key) in args[0]\n            assert kwargs['wrap'] is False\n\n    def test_cleanup(self):\n        self.config.manual_auth_hook = ('{0} -c \"import sys; sys.stdout.write(\\'foo\\')\"'\n                                        .format(sys.executable))\n        self.config.manual_cleanup_hook = '# cleanup'\n        self.auth.perform(self.achalls)\n\n        for achall in self.achalls:\n            self.auth.cleanup([achall])\n            assert os.environ['CERTBOT_AUTH_OUTPUT'] == 'foo'\n            assert os.environ['CERTBOT_DOMAIN'] == achall.identifier.value\n            assert os.environ['CERTBOT_IDENTIFIER'] == achall.identifier.value\n            if isinstance(achall.chall, (challenges.HTTP01, challenges.DNS01)):\n                assert os.environ['CERTBOT_VALIDATION'] == \\\n                    achall.validation(achall.account_key)\n            if isinstance(achall.chall, challenges.HTTP01):\n                assert os.environ['CERTBOT_TOKEN'] == \\\n                    achall.chall.encode('token')\n            else:\n                assert 'CERTBOT_TOKEN' not in os.environ\n\n    def test_auth_hint_hook(self):\n        self.config.manual_auth_hook = '/bin/true'\n        assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \\\n            'The Certificate Authority failed to verify the DNS TXT records and challenge ' \\\n            'files created by the --manual-auth-hook. Ensure that this hook is functioning ' \\\n            'correctly and that it waits a sufficient duration of time for DNS propagation. ' \\\n            'Refer to \"certbot --help manual\" and the Certbot User Guide.'\n        assert self.auth.auth_hint([acme_util.HTTP01_A]) == \\\n            'The Certificate Authority failed to verify the challenge files created by the ' \\\n            '--manual-auth-hook. Ensure that this hook is functioning correctly. Refer to ' \\\n            '\"certbot --help manual\" and the Certbot User Guide.'\n\n    def test_auth_hint_no_hook(self):\n        assert self.auth.auth_hint([acme_util.DNS01_A, acme_util.HTTP01_A]) == \\\n            'The Certificate Authority failed to verify the manually created DNS TXT records ' \\\n            'and challenge files. Ensure that you created these in the correct location, or ' \\\n            'try waiting longer for DNS propagation on the next attempt.'\n        assert self.auth.auth_hint([acme_util.HTTP01_A, acme_util.HTTP01_A, acme_util.HTTP01_A]) == \\\n            'The Certificate Authority failed to verify the manually created challenge files. ' \\\n            'Ensure that you created these in the correct location.'\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/null_test.py",
    "content": "\"\"\"Tests for certbot._internal.plugins.null.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\n\nclass InstallerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.null.Installer.\"\"\"\n\n    def setUp(self):\n        from certbot._internal.plugins.null import Installer\n        self.installer = Installer(config=mock.MagicMock(), name=\"null\")\n\n    def test_it(self):\n        assert isinstance(self.installer.more_info(), str)\n        assert [] == self.installer.get_all_names()\n        assert [] == self.installer.supported_enhancements()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/selection_test.py",
    "content": "\"\"\"Tests for letsencrypt.plugins.selection\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal.display import obj as display_obj\nfrom certbot._internal.plugins.disco import PluginsRegistry\nfrom certbot.display import util as display_util\nfrom certbot.tests import util as test_util\n\n\nclass ConveniencePickPluginTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.selection.pick_*.\"\"\"\n\n    def _test(self, fun, ifaces):\n        config = mock.Mock()\n        default = mock.Mock()\n        plugins = mock.Mock()\n\n        with mock.patch(\"certbot._internal.plugins.selection.pick_plugin\") as mock_p:\n            mock_p.return_value = \"foo\"\n            assert \"foo\" == fun(config, default, plugins, \"Question?\")\n        mock_p.assert_called_once_with(\n            config, default, plugins, \"Question?\", ifaces)\n\n    def test_authenticator(self):\n        from certbot._internal.plugins.selection import pick_authenticator\n        self._test(pick_authenticator, (interfaces.Authenticator,))\n\n    def test_installer(self):\n        from certbot._internal.plugins.selection import pick_installer\n        self._test(pick_installer, (interfaces.Installer,))\n\n    def test_configurator(self):\n        from certbot._internal.plugins.selection import pick_configurator\n        self._test(pick_configurator,\n            (interfaces.Authenticator, interfaces.Installer))\n\n\nclass PickPluginTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.selection.pick_plugin.\"\"\"\n\n    def setUp(self):\n        self.config = mock.Mock(noninteractive_mode=False)\n        self.default = None\n        self.reg = mock.MagicMock()\n        self.question = \"Question?\"\n        self.ifaces: list[interfaces.Plugin] = []\n\n    def _call(self):\n        from certbot._internal.plugins.selection import pick_plugin\n        return pick_plugin(self.config, self.default, self.reg,\n                           self.question, self.ifaces)\n\n    def test_default_provided(self):\n        self.default = \"foo\"\n        self._call()\n        assert 1 == self.reg.filter.call_count\n\n    def test_no_default(self):\n        self._call()\n        assert 1 == self.reg.visible().ifaces.call_count\n\n    def test_no_candidate(self):\n        assert self._call() is None\n\n    def test_single(self):\n        plugin_ep = mock.MagicMock()\n        plugin_ep.init.return_value = \"foo\"\n        plugin_ep.misconfigured = False\n\n        self.reg.visible().ifaces().available.return_value = {\n            \"bar\": plugin_ep}\n        assert \"foo\" == self._call()\n\n    def test_single_misconfigured(self):\n        plugin_ep = mock.MagicMock()\n        plugin_ep.init.return_value = \"foo\"\n        plugin_ep.misconfigured = True\n\n        self.reg.visible().ifaces().available.return_value = {\n            \"bar\": plugin_ep}\n        assert self._call() is None\n\n    def test_multiple(self):\n        plugin_ep = mock.MagicMock()\n        plugin_ep.init.return_value = \"foo\"\n        self.reg.visible().ifaces().available.return_value = {\n            \"bar\": plugin_ep,\n            \"baz\": plugin_ep,\n        }\n        with mock.patch(\"certbot._internal.plugins.selection.choose_plugin\") as mock_choose:\n            mock_choose.return_value = plugin_ep\n            assert \"foo\" == self._call()\n        mock_choose.assert_called_once_with(\n            [plugin_ep, plugin_ep], self.question)\n\n    def test_choose_plugin_none(self):\n        self.reg.visible().ifaces().available.return_value = {\n            \"bar\": None,\n            \"baz\": None,\n        }\n\n        with mock.patch(\"certbot._internal.plugins.selection.choose_plugin\") as mock_choose:\n            mock_choose.return_value = None\n            assert self._call() is None\n\n    def test_default_must_be_filtered(self):\n        # https://github.com/certbot/certbot/issues/9664\n        self.default = \"foo\"\n        filtered = mock.MagicMock()\n        self.reg.filter.return_value = filtered\n        self._call()\n        assert filtered.ifaces.call_count == 1\n\n\nclass ChoosePluginTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.selection.choose_plugin.\"\"\"\n\n    def setUp(self):\n        display_obj.set_display(display_obj.FileDisplay(sys.stdout, False))\n\n        self.mock_apache = mock.Mock(\n            description_with_name=\"a\", misconfigured=True)\n        self.mock_apache.name = \"apache\"\n        self.mock_stand = mock.Mock(\n            description_with_name=\"s\", misconfigured=False)\n        self.mock_stand.init().more_info.return_value = \"standalone\"\n        self.plugins = [\n            self.mock_apache,\n            self.mock_stand,\n        ]\n\n    def _call(self):\n        from certbot._internal.plugins.selection import choose_plugin\n        return choose_plugin(self.plugins, \"Question?\")\n\n    @test_util.patch_display_util()\n    def test_selection(self, mock_util):\n        mock_util().menu.side_effect = [(display_util.OK, 0),\n                                        (display_util.OK, 1)]\n        assert self.mock_stand == self._call()\n        assert mock_util().notification.call_count == 1\n\n    @test_util.patch_display_util()\n    def test_more_info(self, mock_util):\n        mock_util().menu.side_effect = [\n            (display_util.OK, 1),\n        ]\n\n        assert self.mock_stand == self._call()\n\n    @test_util.patch_display_util()\n    def test_no_choice(self, mock_util):\n        mock_util().menu.return_value = (display_util.CANCEL, 0)\n        assert self._call() is None\n\n\nclass GetUnpreparedInstallerTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.plugins.selection.get_unprepared_installer.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.mock_apache_fail_ep = mock.Mock(\n            description_with_name=\"afail\")\n        self.mock_apache_fail_ep.check_name = lambda name: name == \"afail\"\n        self.mock_apache_ep = mock.Mock(\n            description_with_name=\"apache\")\n        self.mock_apache_ep.check_name = lambda name: name == \"apache\"\n        self.mock_apache_plugin = mock.MagicMock()\n        self.mock_apache_ep.init.return_value = self.mock_apache_plugin\n        self.plugins = PluginsRegistry({\n            \"afail\": self.mock_apache_fail_ep,\n            \"apache\": self.mock_apache_ep,\n        })\n\n    def _call(self):\n        from certbot._internal.plugins.selection import get_unprepared_installer\n        return get_unprepared_installer(self.config, self.plugins)\n\n    def test_no_installer_defined(self):\n        self.config.configurator = None\n        assert self._call() is None\n\n    def test_no_available_installers(self):\n        self.config.configurator = \"apache\"\n        self.plugins = PluginsRegistry({})\n        with pytest.raises(errors.PluginSelectionError):\n            self._call()\n\n    def test_get_plugin(self):\n        self.config.configurator = \"apache\"\n        installer = self._call()\n        assert installer is self.mock_apache_plugin\n\n    def test_multiple_installers_returned(self):\n        self.config.configurator = \"apache\"\n        # Two plugins with the same name\n        self.mock_apache_fail_ep.check_name = lambda name: name == \"apache\"\n        with pytest.raises(errors.PluginSelectionError):\n            self._call()\n\n\nclass TestChooseConfiguratorPlugins(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.selection.choose_configurator_plugins.\"\"\"\n\n    def _setupMockPlugin(self, name):\n        mock_ep = mock.Mock(\n            description_with_name=name)\n        mock_ep.check_name = lambda n: n == name\n        mock_plugin = mock.MagicMock()\n        mock_plugin.name = name\n        mock_ep.init.return_value = mock_plugin\n        mock_ep.misconfigured = False\n        return mock_ep\n\n    def _parseArgs(self, args):\n        from certbot._internal import cli\n        return cli.prepare_and_parse_args(self.plugins, args.split())\n\n    def setUp(self):\n        self.plugins = PluginsRegistry({\n            \"nginx\": self._setupMockPlugin(\"nginx\"),\n            \"apache\": self._setupMockPlugin(\"apache\"),\n            \"manual\": self._setupMockPlugin(\"manual\"),\n        })\n\n    def _runWithArgs(self, args):\n        from certbot._internal.plugins.selection import choose_configurator_plugins\n        return choose_configurator_plugins(self._parseArgs(args), self.plugins, \"certonly\")\n\n    def test_noninteractive_configurator(self):\n        # For certonly, setting either the nginx or apache configurators should\n        # return both an installer and authenticator\n        inst, auth = self._runWithArgs(\"certonly --nginx\")\n        assert inst.name == \"nginx\"\n        assert auth.name == \"nginx\"\n\n        inst, auth = self._runWithArgs(\"certonly --apache\")\n        assert inst.name == \"apache\"\n        assert auth.name == \"apache\"\n\n    def test_noninteractive_inst_arg(self):\n        # For certonly, if an installer arg is set, it should be returned as expected\n        inst, auth = self._runWithArgs(\"certonly -a nginx -i nginx\")\n        assert inst.name == \"nginx\"\n        assert auth.name == \"nginx\"\n\n        inst, auth = self._runWithArgs(\"certonly -a apache -i apache\")\n        assert inst.name == \"apache\"\n        assert auth.name == \"apache\"\n\n        # if no installer arg is set (or it's set to none), one shouldn't be returned\n        inst, auth = self._runWithArgs(\"certonly -a nginx\")\n        assert inst is None\n        assert auth.name == \"nginx\"\n        inst, auth = self._runWithArgs(\"certonly -a nginx -i none\")\n        assert inst is None\n        assert auth.name == \"nginx\"\n\n        inst, auth = self._runWithArgs(\"certonly -a apache\")\n        assert inst is None\n        assert auth.name == \"apache\"\n        inst, auth = self._runWithArgs(\"certonly -a apache -i none\")\n        assert inst is None\n        assert auth.name == \"apache\"\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/standalone_test.py",
    "content": "\"\"\"Tests for certbot._internal.plugins.standalone.\"\"\"\nimport errno\nimport socket\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges, messages\nfrom acme import standalone as acme_standalone\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\n\nclass ServerManagerTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.standalone.ServerManager.\"\"\"\n\n    def setUp(self):\n        from certbot._internal.plugins.standalone import ServerManager\n        self.http_01_resources: set[acme_standalone.HTTP01RequestHandler.HTTP01Resource] = {}\n        self.mgr = ServerManager(self.http_01_resources)\n\n    def test_init(self):\n        assert self.mgr.http_01_resources is self.http_01_resources\n\n    def _test_run_stop(self, challenge_type):\n        server = self.mgr.run(port=0, challenge_type=challenge_type)\n        port = server.getsocknames()[0][1]\n        assert self.mgr.running() == {port: server}\n        self.mgr.stop(port=port)\n        assert self.mgr.running() == {}\n\n    def test_run_stop_http_01(self):\n        self._test_run_stop(challenges.HTTP01)\n\n    def test_run_idempotent(self):\n        server = self.mgr.run(port=0, challenge_type=challenges.HTTP01)\n        port = server.getsocknames()[0][1]\n        server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01)\n        assert self.mgr.running() == {port: server}\n        assert server is server2\n        self.mgr.stop(port)\n        assert self.mgr.running() == {}\n\n    def test_run_bind_error(self):\n        some_server = socket.socket(socket.AF_INET6)\n        some_server.bind((\"\", 0))\n        port = some_server.getsockname()[1]\n        maybe_another_server = socket.socket()\n        try:\n            maybe_another_server.bind((\"\", port))\n        except OSError:\n            pass\n        with pytest.raises(errors.StandaloneBindError):\n            self.mgr.run(port,\n            challenge_type=challenges.HTTP01)\n        assert self.mgr.running() == {}\n        some_server.close()\n        maybe_another_server.close()\n\n\ndef get_open_port():\n    \"\"\"Gets an open port number from the OS.\"\"\"\n    open_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0)\n    open_socket.bind((\"\", 0))\n    port = open_socket.getsockname()[1]\n    open_socket.close()\n    return port\n\n\nclass AuthenticatorTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.standalone.Authenticator.\"\"\"\n\n    def setUp(self):\n        from certbot._internal.plugins.standalone import Authenticator\n\n        self.config = mock.MagicMock(http01_port=get_open_port())\n        self.auth = Authenticator(self.config, name=\"standalone\")\n        self.auth.servers = mock.MagicMock()\n\n    def test_more_info(self):\n        assert isinstance(self.auth.more_info(), str)\n\n    def test_get_chall_pref(self):\n        assert self.auth.get_chall_pref(identifier=None) == \\\n                         [challenges.HTTP01]\n\n    def test_perform(self):\n        achalls = self._get_achalls()\n        response = self.auth.perform(achalls)\n\n        expected = [achall.response(achall.account_key) for achall in achalls]\n        assert response == expected\n\n    @test_util.patch_display_util()\n    def test_perform_eaddrinuse_retry(self, mock_get_utility):\n        mock_utility = mock_get_utility()\n        encountered_errno = errno.EADDRINUSE\n        error = errors.StandaloneBindError(mock.MagicMock(errno=encountered_errno), -1)\n        self.auth.servers.run.side_effect = [error] + 2 * [mock.MagicMock()]\n        mock_yesno = mock_utility.yesno\n        mock_yesno.return_value = True\n\n        self.test_perform()\n        self._assert_correct_yesno_call(mock_yesno)\n\n    @test_util.patch_display_util()\n    def test_perform_eaddrinuse_no_retry(self, mock_get_utility):\n        mock_utility = mock_get_utility()\n        mock_yesno = mock_utility.yesno\n        mock_yesno.return_value = False\n\n        encountered_errno = errno.EADDRINUSE\n        with pytest.raises(errors.PluginError):\n            self._fail_perform(encountered_errno)\n        self._assert_correct_yesno_call(mock_yesno)\n\n    def _assert_correct_yesno_call(self, mock_yesno):\n        yesno_args, yesno_kwargs = mock_yesno.call_args\n        assert \"in use\" in yesno_args[0]\n        assert not yesno_kwargs.get(\"default\", True)\n\n    def test_perform_eacces(self):\n        encountered_errno = errno.EACCES\n        with pytest.raises(errors.PluginError):\n            self._fail_perform(encountered_errno)\n\n    def test_perform_unexpected_socket_error(self):\n        encountered_errno = errno.ENOTCONN\n        with pytest.raises(errors.StandaloneBindError):\n            self._fail_perform(encountered_errno)\n\n    def _fail_perform(self, encountered_errno):\n        error = errors.StandaloneBindError(mock.MagicMock(errno=encountered_errno), -1)\n        self.auth.servers.run.side_effect = error\n        self.auth.perform(self._get_achalls())\n\n    @classmethod\n    def _get_achalls(cls):\n        domain = b'localhost'\n        key = jose.JWK.load(test_util.load_vector('rsa512_key.pem'))\n        http_01 = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.HTTP01_P,\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=domain),\n            account_key=key)\n\n        return [http_01]\n\n    def test_cleanup(self):\n        self.auth.servers.running.return_value = {\n            1: \"server1\",\n            2: \"server2\",\n        }\n        self.auth.served[\"server1\"].add(\"chall1\")\n        self.auth.served[\"server2\"].update([\"chall2\", \"chall3\"])\n\n        self.auth.cleanup([\"chall1\"])\n        assert self.auth.served == {\n            \"server1\": set(), \"server2\": {\"chall2\", \"chall3\"}}\n        self.auth.servers.stop.assert_called_once_with(1)\n\n        self.auth.servers.running.return_value = {\n            2: \"server2\",\n        }\n        self.auth.cleanup([\"chall2\"])\n        assert self.auth.served == {\n            \"server1\": set(), \"server2\": {\"chall3\"}}\n        assert 1 == self.auth.servers.stop.call_count\n\n        self.auth.cleanup([\"chall3\"])\n        assert self.auth.served == {\n            \"server1\": set(), \"server2\": set()}\n        self.auth.servers.stop.assert_called_with(2)\n\n    def test_auth_hint(self):\n        self.config.http01_port = \"80\"\n        self.config.http01_address = None\n        assert \"on port 80\" in self.auth.auth_hint([])\n        self.config.http01_address = \"127.0.0.1\"\n        assert \"on 127.0.0.1:80\" in self.auth.auth_hint([])\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/storage_test.py",
    "content": "\"\"\"Tests for certbot.plugins.storage.PluginStorage\"\"\"\nimport json\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\n\nclass PluginStorageTest(test_util.ConfigTestCase):\n    \"\"\"Test for certbot.plugins.storage.PluginStorage\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.plugin_cls = test_util.DummyInstaller\n        filesystem.mkdir(self.config.config_dir)\n        with mock.patch(\"certbot.reverter.util\"):\n            self.plugin = self.plugin_cls(config=self.config, name=\"mockplugin\")\n\n    def test_load_errors_cant_read(self):\n        with open(os.path.join(self.config.config_dir,\n                               \".pluginstorage.json\"), \"w\") as fh:\n            fh.write(\"dummy\")\n        # When unable to read file that exists\n        mock_open = mock.mock_open()\n        mock_open.side_effect = IOError\n        self.plugin.storage._storagepath = os.path.join(self.config.config_dir,\n                                                       \".pluginstorage.json\")\n        with mock.patch(\"builtins.open\", mock_open):\n            with mock.patch('certbot.compat.os.path.isfile', return_value=True):\n                with mock.patch(\"certbot.reverter.util\"):\n                    with pytest.raises(errors.PluginStorageError):\n                        self.plugin.storage._load()  # pylint: disable=protected-access\n\n    def test_load_errors_empty(self):\n        with open(os.path.join(self.config.config_dir, \".pluginstorage.json\"), \"w\") as fh:\n            fh.write('')\n        with mock.patch(\"certbot.plugins.storage.logger.debug\") as mock_log:\n            # Should not error out but write a debug log line instead\n            with mock.patch(\"certbot.reverter.util\"):\n                nocontent = self.plugin_cls(self.config, \"mockplugin\")\n            with pytest.raises(KeyError):\n                nocontent.storage.fetch(\"value\")\n            assert mock_log.called\n            assert \"no values loaded\" in mock_log.call_args[0][0]\n\n    def test_load_errors_corrupted(self):\n        with open(os.path.join(self.config.config_dir,\n                               \".pluginstorage.json\"), \"w\") as fh:\n            fh.write('invalid json')\n        with mock.patch(\"certbot.plugins.storage.logger.error\") as mock_log:\n            with mock.patch(\"certbot.reverter.util\"):\n                corrupted = self.plugin_cls(self.config, \"mockplugin\")\n            with pytest.raises(errors.PluginError):\n                corrupted.storage.fetch(\"value\")\n            assert \"is corrupted\" in mock_log.call_args[0][0]\n\n    def test_save_errors_cant_serialize(self):\n        with mock.patch(\"certbot.plugins.storage.logger.error\") as mock_log:\n            # Set data as something that can't be serialized\n            self.plugin.storage._initialized = True  # pylint: disable=protected-access\n            self.plugin.storage._storagepath = \"/tmp/whatever\"\n            self.plugin.storage._data = self.plugin_cls  # pylint: disable=protected-access\n            with pytest.raises(errors.PluginStorageError):\n                self.plugin.storage.save()\n            assert \"Could not serialize\" in mock_log.call_args[0][0]\n\n    def test_save_errors_unable_to_write_file(self):\n        mock_open = mock.mock_open()\n        mock_open.side_effect = IOError\n        with mock.patch(\"certbot.compat.filesystem.open\", mock_open):\n            with mock.patch(\"certbot.plugins.storage.logger.error\") as mock_log:\n                self.plugin.storage._data = {\"valid\": \"data\"}  # pylint: disable=protected-access\n                self.plugin.storage._initialized = True  # pylint: disable=protected-access\n                self.plugin.storage._storagepath = \"/tmp/whatever\"\n                with pytest.raises(errors.PluginStorageError):\n                    self.plugin.storage.save()\n                assert \"Could not write\" in mock_log.call_args[0][0]\n\n    def test_save_uninitialized(self):\n        with mock.patch(\"certbot.reverter.util\"):\n            with pytest.raises(errors.PluginStorageError):\n                self.plugin_cls(self.config, \"x\").storage.save()\n\n    def test_namespace_isolation(self):\n        with mock.patch(\"certbot.reverter.util\"):\n            plugin1 = self.plugin_cls(self.config, \"first\")\n            plugin2 = self.plugin_cls(self.config, \"second\")\n        plugin1.storage.put(\"first_key\", \"first_value\")\n        with pytest.raises(KeyError):\n            plugin2.storage.fetch(\"first_key\")\n        with pytest.raises(KeyError):\n            plugin2.storage.fetch(\"first\")\n        assert plugin1.storage.fetch(\"first_key\") == \"first_value\"\n\n    def test_saved_state(self):\n        self.plugin.storage.put(\"testkey\", \"testvalue\")\n        # Write to disk\n        self.plugin.storage.save()\n        with mock.patch(\"certbot.reverter.util\"):\n            another = self.plugin_cls(self.config, \"mockplugin\")\n        assert another.storage.fetch(\"testkey\") == \"testvalue\"\n\n        with open(os.path.join(self.config.config_dir,\n                               \".pluginstorage.json\"), 'r') as fh:\n            psdata = fh.read()\n        psjson = json.loads(psdata)\n        assert \"mockplugin\" in psjson.keys()\n        assert len(psjson) == 1\n        assert psjson[\"mockplugin\"][\"testkey\"] == \"testvalue\"\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/util_test.py",
    "content": "\"\"\"Tests for certbot.plugins.util.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot.compat import os\n\n\ndef test_get_prefix():\n    from certbot.plugins.util import get_prefixes\n    assert get_prefixes('/a/b/c') == \\\n        [os.path.normpath(path) for path in ['/a/b/c', '/a/b', '/a', '/']]\n    assert get_prefixes('/') == [os.path.normpath('/')]\n    assert get_prefixes('a') == ['a']\n\n\n@mock.patch(\"certbot.plugins.util.logger.debug\")\ndef test_path_surgery(mock_debug):\n    from certbot.plugins.util import path_surgery\n    all_path = {\"PATH\": \"/usr/local/bin:/bin/:/usr/sbin/:/usr/local/sbin/\"}\n    with mock.patch.dict('os.environ', all_path):\n        with mock.patch('certbot.util.exe_exists') as mock_exists:\n            mock_exists.return_value = True\n            assert path_surgery(\"eg\") is True\n            assert mock_debug.call_count == 0\n            assert os.environ[\"PATH\"] == all_path[\"PATH\"]\n    if os.name != 'nt':\n        # This part is specific to Linux since on Windows no PATH surgery is ever done.\n        no_path = {\"PATH\": \"/tmp/\"}\n        with mock.patch.dict('os.environ', no_path):\n            path_surgery(\"thingy\")\n            assert mock_debug.call_count == (2 if os.name != 'nt' else 1)\n            assert \"Failed to find\" in mock_debug.call_args[0][0]\n            assert \"/usr/local/bin\" in os.environ[\"PATH\"]\n            assert \"/tmp\" in os.environ[\"PATH\"]\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/plugins/webroot_test.py",
    "content": "\"\"\"Tests for certbot._internal.plugins.webroot.\"\"\"\n\nfrom __future__ import print_function\n\nimport argparse\nimport errno\nimport json\nimport shutil\nimport sys\nimport tempfile\nimport unittest\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot._internal.cli import cli_utils\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\nKEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass AuthenticatorTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.plugins.webroot.Authenticator.\"\"\"\n\n    achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n        challb=acme_util.HTTP01_P,\n        identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"thing.com\"),\n        account_key=KEY)\n\n    def setUp(self):\n        from certbot._internal.plugins.webroot import Authenticator\n\n        # On Linux directories created by tempfile.mkdtemp inherit their permissions from their\n        # parent directory. So the actual permissions are inconsistent over various tests env.\n        # To circumvent this, a dedicated sub-workspace is created under the workspace, using\n        # filesystem.mkdir to get consistent permissions.\n        self.workspace = tempfile.mkdtemp()\n        self.path = os.path.join(self.workspace, 'webroot')\n        filesystem.mkdir(self.path)\n        self.partial_root_challenge_path = os.path.join(\n            self.path, \".well-known\")\n        self.root_challenge_path = os.path.join(\n            self.path, \".well-known\", \"acme-challenge\")\n        self.validation_path = os.path.join(\n            self.root_challenge_path,\n            \"ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ\")\n        self.config = mock.MagicMock(webroot_path=self.path,\n                                     webroot_map={\"thing.com\": self.path})\n        self.auth = Authenticator(self.config, \"webroot\")\n\n    def tearDown(self):\n        shutil.rmtree(self.path)\n\n    def test_more_info(self):\n        more_info = self.auth.more_info()\n        assert isinstance(more_info, str)\n        assert self.path in more_info\n\n    def test_add_parser_arguments(self):\n        add = mock.MagicMock()\n        self.auth.add_parser_arguments(add)\n        assert 2 == add.call_count\n\n    def test_prepare(self):\n        self.auth.prepare()  # shouldn't raise any exceptions\n\n    @test_util.patch_display_util()\n    def test_webroot_from_list(self, mock_get_utility):\n        self.config.webroot_path = []\n        self.config.webroot_map = {\"otherthing.com\": self.path}\n        mock_display = mock_get_utility()\n        mock_display.menu.return_value = (display_util.OK, 1,)\n\n        self.auth.perform([self.achall])\n        assert mock_display.menu.called\n        for call in mock_display.menu.call_args_list:\n            assert self.achall.identifier.value in call[0][0]\n            assert all(\n                webroot in call[0][1]\n                for webroot in self.config.webroot_map.values())\n        assert self.config.webroot_map[self.achall.identifier.value] == \\\n                         self.path\n\n    @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows')\n    @test_util.patch_display_util()\n    def test_webconfig_file_generate_and_cleanup(self, mock_get_utility):\n        mock_display = mock_get_utility()\n        mock_display.menu.return_value = (display_util.OK, 1,)\n\n        self.auth.perform([self.achall])\n        assert os.path.exists(os.path.join(self.root_challenge_path, \"web.config\"))\n        self.auth.cleanup([self.achall])\n        assert not os.path.exists(os.path.join(self.root_challenge_path, \"web.config\"))\n\n    @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows')\n    @test_util.patch_display_util()\n    def test_foreign_webconfig_file_handling(self, mock_get_utility):\n        mock_display = mock_get_utility()\n        mock_display.menu.return_value = (display_util.OK, 1,)\n\n        challenge_path = os.path.join(self.path, \".well-known\", \"acme-challenge\")\n        filesystem.makedirs(challenge_path)\n\n        webconfig_path = os.path.join(challenge_path, \"web.config\")\n        with open(webconfig_path, \"w\") as file:\n            file.write(\"something\")\n        self.auth.perform([self.achall])\n        from certbot import crypto_util\n        webconfig_hash = crypto_util.sha256sum(webconfig_path)\n        from certbot._internal.plugins.webroot import _WEB_CONFIG_SHA256SUMS\n        assert webconfig_hash not in _WEB_CONFIG_SHA256SUMS\n\n    @unittest.skipIf(filesystem.POSIX_MODE, reason='Test specific to Windows')\n    def test_foreign_webconfig_multiple_domains(self):\n        # Covers bug https://github.com/certbot/certbot/issues/9091\n        achall_2 = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(challenges.HTTP01(token=b\"bingo\"), \"pending\"),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"second-thing.com\"),\n            account_key=KEY)\n        self.config.webroot_map[\"second-thing.com\"] = self.path\n\n        challenge_path = os.path.join(self.path, \".well-known\", \"acme-challenge\")\n        filesystem.makedirs(challenge_path)\n\n        webconfig_path = os.path.join(challenge_path, \"web.config\")\n        with open(webconfig_path, \"w\") as file:\n            file.write(\"something\")\n        self.auth.perform([self.achall, achall_2])\n\n    @test_util.patch_display_util()\n    def test_webroot_from_list_help_and_cancel(self, mock_get_utility):\n        self.config.webroot_path = []\n        self.config.webroot_map = {\"otherthing.com\": self.path}\n\n        mock_display = mock_get_utility()\n        mock_display.menu.side_effect = ((display_util.CANCEL, -1),)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n        assert mock_display.menu.called\n        for call in mock_display.menu.call_args_list:\n            assert self.achall.identifier.value in call[0][0]\n            assert all(\n                webroot in call[0][1]\n                for webroot in self.config.webroot_map.values())\n\n    @test_util.patch_display_util()\n    def test_new_webroot(self, mock_get_utility):\n        self.config.webroot_path = []\n        self.config.webroot_map = {\"something.com\": self.path}\n\n        mock_display = mock_get_utility()\n        mock_display.menu.return_value = (display_util.OK, 0,)\n        with mock.patch('certbot.display.ops.validated_directory') as m:\n            m.side_effect = ((display_util.CANCEL, -1),\n                             (display_util.OK, self.path,))\n\n            self.auth.perform([self.achall])\n\n        assert self.config.webroot_map[self.achall.identifier.value] == self.path\n\n    @test_util.patch_display_util()\n    def test_new_webroot_empty_map_cancel(self, mock_get_utility):\n        self.config.webroot_path = []\n        self.config.webroot_map = {}\n\n        mock_display = mock_get_utility()\n        mock_display.menu.return_value = (display_util.OK, 0,)\n        with mock.patch('certbot.display.ops.validated_directory') as m:\n            m.return_value = (display_util.CANCEL, -1)\n            with pytest.raises(errors.PluginError):\n                self.auth.perform([self.achall])\n\n    def test_perform_missing_root(self):\n        self.config.webroot_path = None\n        self.config.webroot_map = {}\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([])\n\n    def test_perform_reraises_other_errors(self):\n        self.auth.full_path = os.path.join(self.path, \"null\")\n        permission_canary = os.path.join(self.path, \"rnd\")\n        with open(permission_canary, \"w\") as f:\n            f.write(\"thingimy\")\n        filesystem.chmod(self.path, 0o000)\n        try:\n            with open(permission_canary, \"r\"):\n                pass\n            print(\"Warning, running tests as root skips permissions tests...\")\n        except OSError:\n            # ok, permissions work, test away...\n            with pytest.raises(errors.PluginError):\n                self.auth.perform([])\n        filesystem.chmod(self.path, 0o700)\n\n    @mock.patch(\"certbot._internal.plugins.webroot.filesystem.copy_ownership_and_apply_mode\")\n    def test_failed_chown(self, mock_ownership):\n        mock_ownership.side_effect = OSError(errno.EACCES, \"msg\")\n        self.auth.perform([self.achall])  # exception caught and logged\n\n    @test_util.patch_display_util()\n    def test_perform_new_webroot_not_in_map(self, mock_get_utility):\n        new_webroot = tempfile.mkdtemp()\n        self.config.webroot_path = []\n        self.config.webroot_map = {\"whatever.com\": self.path}\n        mock_display = mock_get_utility()\n        mock_display.menu.side_effect = ((display_util.OK, 0),\n                                         (display_util.OK, new_webroot))\n        achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.HTTP01_P,\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"something.com\"),\n            account_key=KEY)\n        with mock.patch('certbot.display.ops.validated_directory') as m:\n            m.return_value = (display_util.OK, new_webroot,)\n            self.auth.perform([achall])\n        assert self.config.webroot_map[achall.identifier.value] == new_webroot\n\n    def test_perform_permissions(self):\n        self.auth.prepare()\n\n        # Remove exec bit from permission check, so that it\n        # matches the file\n        self.auth.perform([self.achall])\n        assert filesystem.check_mode(self.validation_path, 0o644)\n\n        # Check permissions of the directories\n        for dirpath, dirnames, _ in os.walk(self.path):\n            for directory in dirnames:\n                full_path = os.path.join(dirpath, directory)\n                assert filesystem.check_mode(full_path, 0o755)\n\n        assert filesystem.has_same_ownership(self.validation_path, self.path)\n\n    def test_perform_cleanup(self):\n        self.auth.prepare()\n        responses = self.auth.perform([self.achall])\n        assert 1 == len(responses)\n        assert os.path.exists(self.validation_path)\n        with open(self.validation_path) as validation_f:\n            validation = validation_f.read()\n        assert challenges.KeyAuthorizationChallengeResponse(\n                key_authorization=validation).verify(\n                    self.achall.chall, KEY.public_key())\n\n        self.auth.cleanup([self.achall])\n        assert not os.path.exists(self.validation_path)\n        assert not os.path.exists(self.root_challenge_path)\n        assert not os.path.exists(self.partial_root_challenge_path)\n\n    def test_perform_cleanup_existing_dirs(self):\n        filesystem.mkdir(self.partial_root_challenge_path)\n        self.auth.prepare()\n        self.auth.perform([self.achall])\n        self.auth.cleanup([self.achall])\n\n        # Ensure we don't \"clean up\" directories that previously existed\n        assert not os.path.exists(self.validation_path)\n        assert not os.path.exists(self.root_challenge_path)\n\n    def test_perform_cleanup_multiple_challenges(self):\n        bingo_achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=b\"bingo\"), \"pending\"),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"thing.com\"),\n            account_key=KEY)\n\n        bingo_validation_path = \"YmluZ28\"\n        filesystem.mkdir(self.partial_root_challenge_path)\n        self.auth.prepare()\n        self.auth.perform([bingo_achall, self.achall])\n\n        self.auth.cleanup([self.achall])\n        assert not os.path.exists(bingo_validation_path)\n        assert os.path.exists(self.root_challenge_path)\n        self.auth.cleanup([bingo_achall])\n        assert not os.path.exists(self.validation_path)\n        assert not os.path.exists(self.root_challenge_path)\n\n    def test_cleanup_leftovers(self):\n        self.auth.prepare()\n        self.auth.perform([self.achall])\n\n        leftover_path = os.path.join(self.root_challenge_path, 'leftover')\n        filesystem.mkdir(leftover_path)\n\n        self.auth.cleanup([self.achall])\n        assert not os.path.exists(self.validation_path)\n        assert os.path.exists(self.root_challenge_path)\n\n        os.rmdir(leftover_path)\n\n    @mock.patch('certbot.compat.os.rmdir')\n    def test_cleanup_failure(self, mock_rmdir):\n        self.auth.prepare()\n        self.auth.perform([self.achall])\n\n        os_error = OSError()\n        os_error.errno = errno.EACCES\n        mock_rmdir.side_effect = os_error\n\n        self.auth.cleanup([self.achall])\n        assert not os.path.exists(self.validation_path)\n        assert os.path.exists(self.root_challenge_path)\n\nclass WebrootActionTest(unittest.TestCase):\n    \"\"\"Tests for webroot argparse actions.\"\"\"\n\n    achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n        challb=acme_util.HTTP01_P,\n        identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"thing.com\"),\n        account_key=KEY)\n\n    ipchall = achallenges.KeyAuthorizationAnnotatedChallenge(\n        challb=acme_util.chall_to_challb(\n            challenges.HTTP01(token=((b'a' * 16))),\n            messages.STATUS_PENDING),\n        identifier=messages.Identifier(typ=messages.IDENTIFIER_IP, value=\"1.2.3.4\"),\n        account_key=KEY)\n\n    def setUp(self):\n        from certbot._internal.plugins.webroot import Authenticator\n        self.path = tempfile.mkdtemp()\n        self.parser = argparse.ArgumentParser()\n        self.parser.ip_addresses = []\n        self.parser.add_argument(\"-d\", \"--domains\",\n                                 action=cli_utils.DomainsAction, default=[])\n        self.parser.add_argument(\"--ip-address\",\n                                 action=cli_utils.IPAddressAction,\n                                 dest=\"ip_addresses\",\n                                 default=[])\n        Authenticator.inject_parser_options(self.parser, \"webroot\")\n\n    def test_webroot_map_action(self):\n        other_path = tempfile.mkdtemp()\n        args = self.parser.parse_args(\n            [\"--webroot-map\", json.dumps({'thing.com,thunk.com,9.8.7.6': self.path,'thunk.com': other_path})])\n        assert args.webroot_map[\"thing.com\"] == self.path\n        assert args.webroot_map[\"9.8.7.6\"] == self.path\n        assert args.webroot_map[\"thunk.com\"] == other_path\n\n    def test_domain_before_webroot(self):\n        args = self.parser.parse_args(\n            \"-d {0} -w {1}\".format(self.achall.identifier.value, self.path).split())\n        config = self._get_config_after_perform(args)\n        assert config.webroot_map[self.achall.identifier.value] == self.path\n\n    def test_multi_identifier(self):\n        args = self.parser.parse_args(\n            \"-w {0} -d {1} --ip-address {2}\".format(\n                self.path, self.achall.identifier.value, self.ipchall.identifier.value).split())\n\n        config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall])\n        assert config.webroot_map[self.achall.identifier.value] == self.path\n        assert config.webroot_map[self.ipchall.identifier.value] == self.path\n\n    def test_domain_before_webroot_error(self):\n        with pytest.raises(errors.PluginError):\n            self.parser.parse_args(\"-d foo -w bar -w baz\".split())\n        with pytest.raises(errors.PluginError):\n            self.parser.parse_args(\"-d foo -w bar -d baz -w qux\".split())\n\n    def test_multiwebroot(self):\n        ip = self.ipchall.identifier.value\n        dns_name = self.achall.identifier.value\n\n        ip_path = tempfile.mkdtemp()\n        dns_path = tempfile.mkdtemp()\n        args = self.parser.parse_args(f\"-w {dns_path} -d {dns_name} -w {ip_path} --ip-address {ip}\".split())\n        config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall])\n        assert config.webroot_map[dns_name] == dns_path\n        assert config.webroot_map[ip] == ip_path\n\n    def test_multiwebroot_ip_first(self):\n        ip = self.ipchall.identifier.value\n        dns_name = self.achall.identifier.value\n\n        ip_path = tempfile.mkdtemp()\n        dns_path = tempfile.mkdtemp()\n        args = self.parser.parse_args(f\"-w {ip_path} --ip-address {ip} -w {dns_path} -d {dns_name}\".split())\n        config = self._get_config_after_perform(args, challs=[self.achall, self.ipchall])\n        assert config.webroot_map[dns_name] == dns_path\n        assert config.webroot_map[ip] == ip_path\n\n    def test_webroot_map_partial_without_perform(self):\n        # This test acknowledges the fact that webroot_map content will be partial if webroot\n        # plugin perform method is not invoked (corner case when all auths are already valid).\n        # To not be a problem, the webroot_path must always been conserved during renew.\n        # This condition is challenged by:\n        # certbot.tests.renewal_tests::RenewalTest::test_webroot_params_conservation\n        # See https://github.com/certbot/certbot/pull/7095 for details.\n        other_webroot_path = tempfile.mkdtemp()\n        args = self.parser.parse_args(\"-w {0} -d {1} -w {2} -d bar\".format(\n            self.path, self.achall.identifier.value, other_webroot_path).split())\n        assert args.webroot_map == {self.achall.identifier.value: self.path}\n        assert args.webroot_path == [self.path, other_webroot_path]\n\n    def _get_config_after_perform(self, config, challs=None):\n        if not challs:\n            challs = [self.achall]\n        from certbot._internal.plugins.webroot import Authenticator\n        auth = Authenticator(config, \"webroot\")\n        auth.perform(challs)\n        return auth.config\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/renewal_test.py",
    "content": "\"\"\"Tests for certbot._internal.renewal\"\"\"\nimport copy\nimport datetime\nimport sys\nimport tempfile\nimport unittest\nfrom unittest import mock\n\nimport configobj\nimport pytest\n\nfrom acme import challenges, errors as acme_errors\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot._internal import storage\nimport certbot.tests.util as test_util\n\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives import serialization, hashes\nfrom cryptography import x509\n\ndef make_cert_with_lifetime(not_before: datetime.datetime, lifetime_days: int) -> bytes:\n    \"\"\"Return PEM of a self-signed certificate with the given notBefore and lifetime.\"\"\"\n    key = ec.generate_private_key(ec.SECP256R1())\n    not_after=not_before + datetime.timedelta(days=lifetime_days)\n    cert = x509.CertificateBuilder(\n        issuer_name=x509.Name([]),\n        subject_name=x509.Name([]),\n        public_key=key.public_key(),\n        serial_number=x509.random_serial_number(),\n        not_valid_before=not_before,\n        not_valid_after=not_after,\n    ).add_extension(\n        x509.SubjectAlternativeName([x509.DNSName(\"example.com\")]),\n        critical=False,\n    ).sign(\n        private_key=key,\n        algorithm=hashes.SHA256(),\n    )\n    return cert.public_bytes(serialization.Encoding.PEM)\n\nclass RenewalTest(test_util.ConfigTestCase):\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_ancient_webroot_renewal_conf(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        rc_path = test_util.make_lineage(\n            self.config.config_dir, 'sample-renewal-ancient.conf')\n        self.config.account = None\n        self.config.email = None\n        self.config.webroot_path = None\n        config = configuration.NamespaceConfig(self.config)\n        lineage = storage.RenewableCert(rc_path, config)\n        renewalparams = lineage.configuration['renewalparams']\n        # pylint: disable=protected-access\n        from certbot._internal import renewal\n        renewal._restore_webroot_config(config, renewalparams)\n        assert config.webroot_path == ['/var/www/']\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_webroot_params_conservation(self, mock_set_by_user):\n        # For more details about why this test is important, see:\n        # certbot._internal.plugins.webroot_test::\n        #   WebrootActionTest::test_webroot_map_partial_without_perform\n        from certbot._internal import renewal\n        mock_set_by_user.return_value = False\n\n        renewalparams = {\n            'webroot_map': {'test.example.com': '/var/www/test'},\n            'webroot_path': ['/var/www/test', '/var/www/other'],\n        }\n        renewal._restore_webroot_config(self.config, renewalparams)  # pylint: disable=protected-access\n        assert self.config.webroot_map == {'test.example.com': '/var/www/test'}\n        assert self.config.webroot_path == ['/var/www/test', '/var/www/other']\n\n        renewalparams = {\n            'webroot_map': {},\n            'webroot_path': '/var/www/test',\n        }\n        renewal._restore_webroot_config(self.config, renewalparams)  # pylint: disable=protected-access\n        assert self.config.webroot_map == {}\n        assert self.config.webroot_path == ['/var/www/test']\n\n    @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts')\n    def test_reuse_key_renewal_params(self, unused_mock_avoid_reuse_conflicts):\n        self.config.elliptic_curve = 'INVALID_VALUE'\n        self.config.reuse_key = True\n        self.config.dry_run = True\n        config = configuration.NamespaceConfig(self.config)\n\n        rc_path = test_util.make_lineage(\n            self.config.config_dir, 'sample-renewal.conf')\n        lineage = storage.RenewableCert(rc_path, config)\n\n        le_client = mock.MagicMock()\n        le_client.obtain_certificate.return_value = (None, None, None, None)\n\n        from certbot._internal import renewal\n\n        with mock.patch('certbot._internal.renewal.hooks.deploy_hook'):\n            renewal.renew_cert(self.config, None, le_client, lineage)\n\n        assert self.config.elliptic_curve == 'secp256r1'\n\n    @mock.patch('certbot._internal.renewal._avoid_reuse_key_conflicts')\n    def test_reuse_ec_key_renewal_params(self, unused_mock_avoid_reuse_conflicts):\n        self.config.elliptic_curve = 'INVALID_CURVE'\n        self.config.reuse_key = True\n        self.config.dry_run = True\n        self.config.key_type = 'ecdsa'\n        config = configuration.NamespaceConfig(self.config)\n\n        rc_path = test_util.make_lineage(\n            self.config.config_dir,\n            'sample-renewal-ec.conf',\n            ec=True,\n        )\n        lineage = storage.RenewableCert(rc_path, config)\n\n        le_client = mock.MagicMock()\n        le_client.obtain_certificate.return_value = (None, None, None, None)\n\n        from certbot._internal import renewal\n\n        with mock.patch('certbot._internal.renewal.hooks.deploy_hook'):\n            renewal.renew_cert(self.config, None, le_client, lineage)\n\n        assert self.config.elliptic_curve == 'secp256r1'\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_new_key(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        # When renewing with both reuse_key and new_key, the key should be regenerated,\n        # the key type, key parameters and reuse_key should be kept.\n        self.config.reuse_key = True\n        self.config.new_key = True\n        self.config.dry_run = True\n        config = configuration.NamespaceConfig(self.config)\n\n        rc_path = test_util.make_lineage(\n            self.config.config_dir, 'sample-renewal.conf')\n        lineage = storage.RenewableCert(rc_path, config)\n\n        le_client = mock.MagicMock()\n        le_client.obtain_certificate.return_value = (None, None, None, None)\n\n        from certbot._internal import renewal\n\n        with mock.patch('certbot._internal.renewal.hooks.deploy_hook'):\n            renewal.renew_cert(self.config, None, le_client, lineage)\n\n        assert self.config.elliptic_curve == 'secp256r1'\n        assert self.config.key_type == 'ecdsa'\n        assert self.config.reuse_key\n        # None is passed as the existing key, i.e. the key is not actually being reused.\n        le_client.obtain_certificate.assert_called_with(mock.ANY, None)\n\n    @mock.patch('certbot._internal.renewal.hooks.deploy_hook')\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_reuse_key_conflicts(self, mock_set_by_user, unused_mock_deploy_hook):\n        mock_set_by_user.return_value = False\n\n        # When renewing with reuse_key and a conflicting key parameter (size, curve)\n        # an error should be raised ...\n        self.config.reuse_key = True\n        self.config.key_type = \"rsa\"\n        self.config.rsa_key_size = 4096\n        self.config.dry_run = True\n\n        config = configuration.NamespaceConfig(self.config)\n\n        rc_path = test_util.make_lineage(\n            self.config.config_dir, 'sample-renewal.conf')\n        lineage = storage.RenewableCert(rc_path, config)\n        lineage.configuration[\"renewalparams\"][\"reuse_key\"] = True\n\n        le_client = mock.MagicMock()\n        le_client.obtain_certificate.return_value = (None, None, None, None)\n\n        from certbot._internal import renewal\n\n        with pytest.raises(errors.Error, match=\"Unable to change the --key-type\"):\n            renewal.renew_cert(self.config, None, le_client, lineage)\n\n        # ... unless --no-reuse-key is set\n        mock_set_by_user.side_effect = lambda var: var == \"reuse_key\"\n        self.config.reuse_key = False\n        renewal.renew_cert(self.config, None, le_client, lineage)\n\n    @test_util.patch_display_util()\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_remove_deprecated_config_elements(self, mock_set_by_user, unused_mock_get_utility):\n        mock_set_by_user.return_value = False\n        config = configuration.NamespaceConfig(self.config)\n        config.certname = \"sample-renewal-deprecated-option\"\n\n        rc_path = test_util.make_lineage(\n            self.config.config_dir, 'sample-renewal-deprecated-option.conf')\n\n        from certbot._internal import renewal\n        lineage_config = copy.deepcopy(self.config)\n        renewal.reconstitute(lineage_config, rc_path)\n        # This means that manual_public_ip_logging_ok was not modified in the config based on its\n        # value in the renewal conf file\n        assert isinstance(lineage_config.manual_public_ip_logging_ok, mock.MagicMock)\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_absent_key_type_restored(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False)\n\n        from certbot._internal import renewal\n        lineage_config = copy.deepcopy(self.config)\n        renewal.reconstitute(lineage_config, rc_path)\n        assert lineage_config.key_type == 'rsa'\n\n    @test_util.patch_display_util()\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    @mock.patch('certbot._internal.renewal.AriClientPool.get')\n    @mock.patch('certbot._internal.main.renew_cert')\n    @mock.patch(\"certbot._internal.renewal.datetime\")\n    def test_renewal_via_ari(self, mock_datetime, mock_renew_cert, mock_ari_client_get, mock_set_by_user, unused_mock_display):\n        mock_set_by_user.return_value = False\n        from certbot._internal import renewal\n        acme_client = mock.MagicMock()\n        mock_ari_client_get.return_value = acme_client\n        past = datetime.datetime(2025, 3, 19, 0, 0, 0, tzinfo=datetime.timezone.utc)\n        now = datetime.datetime(2025, 4, 19, 0, 0, 0, tzinfo=datetime.timezone.utc)\n        future = datetime.datetime(2025, 4, 19, 12, 0, 0, tzinfo=datetime.timezone.utc)\n        mock_datetime.datetime.now.return_value = now\n        acme_client.renewal_time.return_value = past, future\n\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False)\n        config = configuration.NamespaceConfig(self.config)\n\n        with mock.patch('time.sleep'):\n            renewal.handle_renewal_request(config)\n\n        mock_renew_cert.assert_called_once()\n        # This value comes from `sample-renewal.conf` and is different than\n        # the global default.\n        expected_server = \"https://acme-staging-v02.api.letsencrypt.org/directory\"\n        assert expected_server != config.server\n        mock_ari_client_get.assert_called_once()\n        assert mock_ari_client_get.call_args[0][0] == expected_server\n\n    @test_util.patch_display_util()\n    @mock.patch('acme.client.ClientNetwork.get')\n    @mock.patch('certbot._internal.storage.RenewableCert.autorenewal_is_enabled')\n    def test_no_network_if_no_autorenew(self, mock_autorenewal_enabled,\n            mock_client_network_get, unused_mock_display):\n        from certbot._internal import renewal\n        mock_autorenewal_enabled.return_value = False\n\n        test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf', ec=False)\n\n        with mock.patch('time.sleep'):\n            renewal.handle_renewal_request(self.config)\n\n        assert mock_client_network_get.call_count == 0\n\n    @mock.patch('acme.client.ClientV2')\n    def test_dry_run_no_ari_call(self, mock_acme):\n        from certbot._internal import renewal\n        self.config.dry_run = True\n        ari_client_pool = mock.MagicMock()\n        ari_client_pool.get.return_value = mock_acme\n        with mock.patch('time.sleep'):\n            renewal.should_renew(self.config, mock.Mock(), ari_client_pool)\n        assert mock_acme.renewal_time.call_count == 0\n\n    def test_default_renewal_time(self):\n        from certbot._internal import renewal\n        cert_pem = make_cert_with_lifetime(datetime.datetime(2025, 3, 12, 00, 00, 00), 8)\n        t = renewal._default_renewal_time(cert_pem)\n        assert t == datetime.datetime(2025, 3, 16, 00, 00, 00, tzinfo=datetime.timezone.utc)\n\n        cert_pem = make_cert_with_lifetime(datetime.datetime(2025, 3, 12, 00, 00, 00), 18)\n        t = renewal._default_renewal_time(cert_pem)\n        assert t == datetime.datetime(2025, 3, 24, 00, 00, 00, tzinfo=datetime.timezone.utc)\n\n    @mock.patch(\"certbot._internal.storage.atomic_rewrite\")\n    @mock.patch(\"certbot._internal.renewal.datetime\")\n    def test_renew_before_expiry(self, mock_datetime, unused_mock_atomic_rewrite):\n        \"\"\"When neither OCSP nor the ACME client indicate it's time to renew,\n           obey the renew_before_expiry config.\n        \"\"\"\n        from certbot._internal import renewal\n\n        # This certificate has a lifetime of 7 days, and the tests below\n        # that use a \"None\" interval (i.e. choose a default) rely on that fact.\n        #\n        # Not Before: Dec 11 22:34:45 2014 GMT\n        # Not After : Dec 18 22:34:45 2014 GMT\n        not_before = datetime.datetime(2014, 12, 11, 22, 34, 45)\n        short_cert = make_cert_with_lifetime(not_before, 7)\n\n        ari_server = \"http://ari\"\n        future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=100000)\n        ari_client_pool = MockAriClientPool(future, future)\n\n        mock_renewable_cert = mock.MagicMock()\n        mock_renewable_cert.server = ari_server\n        mock_renewable_cert.autorenewal_is_enabled.return_value = True\n        mock_renewable_cert.version.return_value = \"/tmp/abc\"\n        mock_renewable_cert.ocsp_revoked.return_value = False\n        mock_renewable_cert.configfile = configobj.ConfigObj()\n\n        mock_datetime.timedelta = datetime.timedelta\n\n        with tempfile.NamedTemporaryFile() as tmp_cert:\n            tmp_cert.close()  # close now because of compatibility issues on Windows\n            with open(tmp_cert.name, 'wb') as c:\n                c.write(short_cert)\n\n            mock_renewable_cert.version.return_value = tmp_cert.name\n\n            # First, test cases where ARI returns a renewal_time far in the future\n            for (current_time, interval, result) in [\n                    # 2014-12-13 12:00 (about 5 days prior to expiry)\n                    # Times that should result in autorenewal/autodeployment\n                    (1418472000, \"2 months\", True), (1418472000, \"1 week\", True),\n                    # With the \"default\" logic, this 7-day certificate should autorenew\n                    # at 3.5 days prior to expiry. We haven't reached that yet,\n                    # so don't renew.\n                    (1418472000, None, False),\n                    # Times that should not renew\n                    (1418472000, \"4 days\", False), (1418472000, \"2 days\", False),\n                    # 2014-12-16 20:00 (after the default renewal time but before expiry)\n                    # Times that should not renew\n                    (1418760000, None, False),\n                    (1418760000, \"1 day\", False),\n                    # 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry)\n                    # Times that should result in autorenewal/autodeployment\n                    (1241179200, \"7 years\", True),\n                    (1241179200, \"11 years 2 months\", True),\n                    # Times that should not renew\n                    (1241179200, \"8 hours\", False), (1241179200, \"2 days\", False),\n                    (1241179200, \"40 days\", False), (1241179200, \"9 months\", False),\n                    # 2015-01-01 (after expiry has already happened, so all\n                    #            intervals should cause autorenewal/autodeployment)\n                    (1420070400, \"0 seconds\", True),\n                    (1420070400, \"10 seconds\", True),\n                    (1420070400, \"10 minutes\", True),\n                    (1420070400, \"10 weeks\", True), (1420070400, \"10 months\", True),\n                    (1420070400, \"10 years\", True), (1420070400, \"99 months\", True),\n            ]:\n                sometime = datetime.datetime.fromtimestamp(current_time, datetime.timezone.utc)\n                mock_datetime.datetime.now.return_value = sometime\n                mock_renewable_cert.configuration = {\"renew_before_expiry\": interval}\n                assert renewal.should_autorenew(mock_renewable_cert, ari_client_pool) == result, f\"at {current_time}, with config '{interval}', ari response in future, expected {result}\"\n\n            # Now, test cases where ARI either fails (returns `(None, _)`) or\n            # the cert has no `server` value and ARI is skipped\n            ari_client_pool = MockAriClientPool(None, future)\n            for (current_time, interval, result) in [\n                    # 2014-12-13 12:00 (about 5 days prior to expiry)\n                    # Times that should result in autorenewal/autodeployment\n                    (1418472000, \"2 months\", True), (1418472000, \"1 week\", True),\n                    # With the \"default\" logic, this 7-day certificate should autorenew\n                    # at 3.5 days prior to expiry. We haven't reached that yet,\n                    # so don't renew.\n                    (1418472000, None, False),\n                    # Times that should not renew\n                    (1418472000, \"4 days\", False), (1418472000, \"2 days\", False),\n                    # 2014-12-16 20:00 (after the default renewal time but before expiry)\n                    # Times that should result in autorenewal/autodeployment\n                    (1418760000, None, True), # Note that this result is different from the above\n                    # Times that should not renew\n                    (1418760000, \"1 day\", False),\n                    # 2009-05-01 12:00:00+00:00 (about 5 years prior to expiry)\n                    # Times that should result in autorenewal/autodeployment\n                    (1241179200, \"7 years\", True),\n                    (1241179200, \"11 years 2 months\", True),\n                    # Times that should not renew\n                    (1241179200, \"8 hours\", False), (1241179200, \"2 days\", False),\n                    (1241179200, \"40 days\", False), (1241179200, \"9 months\", False),\n                    # 2015-01-01 (after expiry has already happened, so all\n                    #            intervals should cause autorenewal/autodeployment)\n                    (1420070400, \"0 seconds\", True),\n                    (1420070400, \"10 seconds\", True),\n                    (1420070400, \"10 minutes\", True),\n                    (1420070400, \"10 weeks\", True), (1420070400, \"10 months\", True),\n                    (1420070400, \"10 years\", True), (1420070400, \"99 months\", True),\n            ]:\n                sometime = datetime.datetime.fromtimestamp(current_time, datetime.timezone.utc)\n                mock_datetime.datetime.now.return_value = sometime\n                mock_renewable_cert.configuration = {\"renew_before_expiry\": interval}\n                mock_renewable_cert.server = ari_server\n                assert renewal.should_autorenew(mock_renewable_cert, ari_client_pool) == result, f\"at {current_time}, with config '{interval}', no ari response, expected {result}\"\n                mock_renewable_cert.server = None\n                assert renewal.should_autorenew(mock_renewable_cert, ari_client_pool) == result, f\"at {current_time}, with config '{interval}', skipped ari, expected {result}\"\n\n    @mock.patch(\"certbot._internal.storage.RenewableCert.ocsp_revoked\")\n    def test_should_autorenew(self, mock_ocsp):\n        from certbot._internal import renewal\n\n        mock_acme = mock.MagicMock()\n        ari_server = \"http://ari\"\n        future = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1000)\n        mock_acme.renewal_time.return_value = (future, future)\n        ari_client_pool = mock.MagicMock()\n        ari_client_pool.get.return_value = mock_acme\n        mock_rc = mock.MagicMock()\n\n        with mock.patch('certbot._internal.renewal.open', mock.mock_open(read_data=b'')):\n            # Autorenewal turned off\n            mock_rc.autorenewal_is_enabled.return_value = False\n            mock_rc.server = ari_server\n            assert not renewal.should_autorenew(mock_rc, ari_client_pool)\n            mock_rc.server = None\n            assert not renewal.should_autorenew(mock_rc, ari_client_pool)\n\n            # Autorenewal turned on, mandatory renewal on the basis of OCSP\n            # revocation\n            mock_rc.autorenewal_is_enabled.return_value = True\n            mock_ocsp.return_value = True\n            assert renewal.should_autorenew(mock_rc, ari_client_pool)\n            mock_rc.server = None\n            with mock.patch('certbot._internal.renewal.logger.warning') as mock_warning:\n                assert renewal.should_autorenew(mock_rc, ari_client_pool)\n            # Ensure we warned about skipping ARI checks when server is None\n            assert any(call.args[0].startswith('Skipping ARI check') for call in\n                       mock_warning.call_args_list)\n\n    @mock.patch(\"certbot._internal.storage.atomic_rewrite\")\n    @mock.patch('certbot._internal.storage.RenewableCert.ocsp_revoked')\n    @mock.patch('acme.client.ClientV2.renewal_time')\n    def test_resilient_ari_directory_fetches(self, mock_renewal_time, mock_ocsp,\n                                             unused_mock_atomic_rewrite):\n        from certbot._internal import renewal\n        from acme import messages\n\n        ari_server = 'http://ari'\n        ari_client_pool = mock.MagicMock()\n        ari_client_pool.get.side_effect = messages.Error()\n        mock_rc = mock.MagicMock()\n        mock_rc.server = ari_server\n        mock_rc.configfile = configobj.ConfigObj()\n        mock_rc.autorenewal_is_enabled.return_value = True\n        mock_ocsp.return_value = True\n\n        with mock.patch('certbot._internal.renewal.open', mock.mock_open(read_data=b'')):\n            with mock.patch('certbot._internal.renewal.logger') as mock_logger:\n                assert renewal.should_autorenew(mock_rc, ari_client_pool)\n\n        assert mock_renewal_time.call_count == 0\n        # Ensure we logged about skipping the ARI check and the underlying exception\n        assert any('ARI' in call.args[0] for call in mock_logger.warning.call_args_list)\n        assert any(call.kwargs.get('exc_info') for call in mock_logger.debug.call_args_list)\n\n    @mock.patch('certbot._internal.storage.RenewableCert.ocsp_revoked')\n    def test_resilient_ari_check(self, mock_ocsp):\n        from certbot._internal import renewal\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        renewable_cert = storage.RenewableCert(rc_path, self.config)\n\n        ari_error = acme_errors.ARIError('some error', datetime.datetime.now())\n        ari_client_pool = MockAriClientPool(None, None)\n        ari_client_pool.mock_acme.renewal_time.side_effect = ari_error\n\n        mock_ocsp.return_value = True\n\n        with mock.patch('certbot._internal.renewal.open', mock.mock_open(read_data=b'')):\n            with mock.patch('certbot._internal.renewal.logger') as mock_logger:\n                assert renewal.should_autorenew(renewable_cert, ari_client_pool)\n        # Ensure we logged about skipping the ARI check and the underlying exception\n        assert any('ARI' in call.args[0] for call in mock_logger.warning.call_args_list)\n        assert any(call.kwargs.get('exc_info') for call in mock_logger.debug.call_args_list)\n\n    def test_stores_ari_retry_after(self):\n        from certbot._internal import renewal\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        renewable_cert = storage.RenewableCert(rc_path, self.config)\n\n        renewal_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(\n            seconds=1000)\n        retry_after = datetime.datetime.now() + datetime.timedelta(seconds=1000)\n        mock_ari_client_pool = MockAriClientPool(renewal_time, retry_after)\n\n        # Check for renewal. As a side effect, this should cause the lineage config to be\n        # updated with 'ari_retry_after' in the renewalparams section.\n        renewal.should_autorenew(renewable_cert, mock_ari_client_pool)\n\n        with open(renewable_cert.configfile.filename, 'r') as c:\n            renewable_cert_config = configobj.ConfigObj(c)\n\n        assert renewable_cert_config['acme_renewal_info']['ari_retry_after'] == retry_after.isoformat(\n            timespec='seconds')\n\n    def test_skips_ari_when_retry_after_future(self):\n        from certbot._internal import renewal\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        renewable_cert = storage.RenewableCert(rc_path, self.config)\n\n        future = datetime.datetime.now() + datetime.timedelta(seconds=1000)\n        storage.atomic_rewrite(rc_path,\n           {\"acme_renewal_info\": {\"ari_retry_after\": future.isoformat(timespec=\"seconds\")}})\n\n        # ARI shouldn't be checked at all because retry after is in the future.\n        mock_ari_client_pool = MockAriClientPool(None, None)\n        mock_ari_client_pool.mock_acme.renewal_time.side_effect = errors.Error(\"Shouldn't be called\")\n\n        # Check for renewal. All we care about here is that renewal_time is not called; if it were,\n        # an exception would be raised.\n        renewal.should_autorenew(renewable_cert, mock_ari_client_pool)\n\n    def test_checks_ari_when_retry_after_absent(self):\n        from certbot._internal import renewal\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        renewable_cert = storage.RenewableCert(rc_path, self.config)\n\n        renewal_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1000)\n        retry_after = datetime.datetime.now() + datetime.timedelta(seconds=1000)\n        mock_ari_client_pool = MockAriClientPool(renewal_time, retry_after)\n\n        # The 'ari_retry_after' field is absent, so renewal_time _should_ be called.\n        # We don't care about the return value of should_autorenew.\n        renewal.should_autorenew(renewable_cert, mock_ari_client_pool)\n\n        mock_ari_client_pool.mock_acme.renewal_time.assert_called_once()\n\n    def test_checks_ari_when_retry_after_in_past(self):\n        from certbot._internal import renewal\n\n        rc_path = test_util.make_lineage(self.config.config_dir, 'sample-renewal.conf')\n        renewable_cert = storage.RenewableCert(rc_path, self.config)\n\n        past = datetime.datetime.now() - datetime.timedelta(seconds=1000)\n        storage.atomic_rewrite(rc_path,\n                               {\"acme_renewal_info\": {\"ari_retry_after\": past.isoformat(timespec=\"seconds\")}})\n\n        renewal_time = datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(seconds=1000)\n        retry_after = datetime.datetime.now() + datetime.timedelta(seconds=1000)\n        mock_ari_client_pool = MockAriClientPool(renewal_time, retry_after)\n\n        # The 'ari_retry_after' field is in the past, so renewal_time _should_ be called.\n        # We don't care about the return value of should_autorenew.\n        renewal.should_autorenew(renewable_cert, mock_ari_client_pool)\n        mock_ari_client_pool.mock_acme.renewal_time.assert_called_once()\n\nclass MockAriClientPool:\n    def __init__(self, renewal_time, retry_after):\n        self.mock_acme = mock.MagicMock()\n        self.mock_acme.renewal_time.return_value = (renewal_time, retry_after)\n\n    def get(self, server):\n        return self.mock_acme\n\n\nclass RestoreRequiredConfigElementsTest(test_util.ConfigTestCase):\n    \"\"\"Tests for certbot._internal.renewal.restore_required_config_elements.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.renewal import restore_required_config_elements\n        return restore_required_config_elements(*args, **kwargs)\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_allow_subset_of_names_success(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        self._call(self.config, {'allow_subset_of_names': 'True'})\n        assert self.config.allow_subset_of_names is True\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_allow_subset_of_names_failure(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        renewalparams = {'allow_subset_of_names': 'maybe'}\n        with pytest.raises(errors.Error):\n            self._call(self.config, renewalparams)\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_pref_challs_list(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        renewalparams = {'pref_challs': 'http-01, dns'.split(',')}\n        self._call(self.config, renewalparams)\n        expected = [challenges.HTTP01.typ, challenges.DNS01.typ]\n        assert self.config.pref_challs == expected\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_pref_challs_str(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        renewalparams = {'pref_challs': 'dns'}\n        self._call(self.config, renewalparams)\n        expected = [challenges.DNS01.typ]\n        assert self.config.pref_challs == expected\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_pref_challs_failure(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        renewalparams = {'pref_challs': 'finding-a-shrubbery'}\n        with pytest.raises(errors.Error):\n            self._call(self.config, renewalparams)\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_must_staple_success(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        self._call(self.config, {'must_staple': 'True'})\n        assert self.config.must_staple is True\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_must_staple_failure(self, mock_set_by_user):\n        mock_set_by_user.return_value = False\n        renewalparams = {'must_staple': 'maybe'}\n        with pytest.raises(errors.Error):\n            self._call(self.config, renewalparams)\n\n    @mock.patch.object(configuration.NamespaceConfig, 'set_by_user')\n    def test_ancient_server_renewal_conf(self, mock_set_by_user):\n        from certbot._internal import constants\n        self.config.server = None\n        mock_set_by_user.return_value = False\n        self._call(self.config, {'server': constants.V1_URI})\n        assert self.config.server == constants.CLI_DEFAULTS['server']\n\n    def test_related_values(self):\n        # certbot.configuration.NamespaceConfig.set_by_user considers some values as related to each\n        # other and considers both set by the user if either is. This test ensures all renewal\n        # parameters are restored regardless of their restoration order or relation between values.\n        # See https://github.com/certbot/certbot/issues/9805 for more info.\n        renewalparams = {\n            'server': 'https://example.org',\n            'account': 'somehash',\n        }\n        self._call(self.config, renewalparams)\n        self.assertEqual(self.config.account, renewalparams['account'])\n\n\nclass DescribeResultsTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.renewal._renew_describe_results.\"\"\"\n    def setUp(self):\n        self.patchers = {\n            'log_error': mock.patch('certbot._internal.renewal.logger.error'),\n            'notify': mock.patch('certbot._internal.renewal.display_util.notify')}\n        self.mock_notify = self.patchers['notify'].start()\n        self.mock_error = self.patchers['log_error'].start()\n\n    def tearDown(self):\n        for patch in self.patchers.values():\n            patch.stop()\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot._internal.renewal import _renew_describe_results\n        _renew_describe_results(*args, **kwargs)\n\n    def _assert_success_output(self, lines):\n        self.mock_notify.assert_has_calls([mock.call(l) for l in lines])\n\n    def test_no_renewal_attempts(self):\n        self._call(mock.MagicMock(dry_run=True), [], [], [], [])\n        self._assert_success_output(['No simulated renewals were attempted.'])\n\n    def test_successful_renewal(self):\n        self._call(mock.MagicMock(dry_run=False), ['good.pem'], None, None, None)\n        self._assert_success_output([\n            '\\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n            'Congratulations, all renewals succeeded: ',\n            '  good.pem (success)',\n            '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n        ])\n\n    def test_failed_renewal(self):\n        self._call(mock.MagicMock(dry_run=False), [], ['bad.pem'], [], [])\n        self._assert_success_output([\n            '\\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n            '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n        ])\n        self.mock_error.assert_has_calls([\n            mock.call('All %ss failed. The following certificates could not be renewed:', 'renewal'),\n            mock.call('  bad.pem (failure)'),\n        ])\n\n    def test_all_renewal(self):\n        self._call(mock.MagicMock(dry_run=True),\n                   ['good.pem', 'good2.pem'], ['bad.pem', 'bad2.pem'],\n                   ['foo.pem expires on 123'], ['errored.conf'])\n        self._assert_success_output([\n            '\\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n            'The following certificates are not due for renewal yet:',\n            '  foo.pem expires on 123 (skipped)',\n            'The following simulated renewals succeeded:',\n            '  good.pem (success)\\n  good2.pem (success)\\n',\n            '\\nAdditionally, the following renewal configurations were invalid: ',\n            '  errored.conf (parsefail)',\n            '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -',\n        ])\n        self.mock_error.assert_has_calls([\n            mock.call('The following %ss failed:', 'simulated renewal'),\n            mock.call('  bad.pem (failure)\\n  bad2.pem (failure)'),\n        ])\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/renewupdater_test.py",
    "content": "\"\"\"Tests for renewal updater interfaces\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import interfaces\nfrom certbot._internal import main\nfrom certbot._internal import updater\nfrom certbot.plugins import enhancements\nimport certbot.tests.util as test_util\n\n\nclass RenewUpdaterTest(test_util.ConfigTestCase):\n    \"\"\"Tests for interfaces.RenewDeployer and interfaces.GenericUpdater\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        self.generic_updater = mock.MagicMock(spec=interfaces.GenericUpdater)\n        self.generic_updater.restart = mock.MagicMock()\n        self.renew_deployer = mock.MagicMock(spec=interfaces.RenewDeployer)\n        self.mockinstaller = mock.MagicMock(spec=enhancements.AutoHSTSEnhancement)\n\n    @mock.patch('certbot._internal.main._get_and_save_cert')\n    @mock.patch('certbot._internal.plugins.selection.choose_configurator_plugins')\n    @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer')\n    @test_util.patch_display_util()\n    def test_server_updates(self, _, mock_geti, mock_select, mock_getsave):\n        mock_getsave.return_value = mock.MagicMock()\n        mock_generic_updater = self.generic_updater\n\n        # Generic Updater\n        mock_select.return_value = (mock_generic_updater, None)\n        mock_geti.return_value = mock_generic_updater\n        with mock.patch('certbot._internal.main._init_le_client'):\n            main.renew_cert(self.config, None, mock.MagicMock())\n        assert mock_generic_updater.restart.called\n\n        mock_generic_updater.restart.reset_mock()\n        mock_generic_updater.generic_updates.reset_mock()\n        updater.run_generic_updaters(self.config, mock.MagicMock(), None)\n        assert mock_generic_updater.generic_updates.call_count == 1\n        assert mock_generic_updater.restart.called is False\n\n    def test_renew_deployer(self):\n        lineage = mock.MagicMock()\n        mock_deployer = self.renew_deployer\n        updater.run_renewal_deployer(self.config, lineage, mock_deployer)\n        mock_deployer.renew_deploy.assert_called_with(lineage)\n\n    @mock.patch(\"certbot._internal.updater.logger.debug\")\n    def test_updater_skip_dry_run(self, mock_log):\n        self.config.dry_run = True\n        updater.run_generic_updaters(self.config, None, None)\n        assert mock_log.called\n        assert mock_log.call_args[0][0] == \\\n                          \"Skipping updaters in dry-run mode.\"\n\n    @mock.patch(\"certbot._internal.updater.logger.debug\")\n    def test_deployer_skip_dry_run(self, mock_log):\n        self.config.dry_run = True\n        updater.run_renewal_deployer(self.config, None, None)\n        assert mock_log.called\n        assert mock_log.call_args[0][0] == \\\n                          \"Skipping renewal deployer in dry-run mode.\"\n\n    @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer')\n    def test_enhancement_updates(self, mock_geti):\n        mock_geti.return_value = self.mockinstaller\n        updater.run_generic_updaters(self.config, mock.MagicMock(), None)\n        assert self.mockinstaller.update_autohsts.called\n        assert self.mockinstaller.update_autohsts.call_count == 1\n\n    def test_enhancement_deployer(self):\n        updater.run_renewal_deployer(self.config, mock.MagicMock(),\n                                     self.mockinstaller)\n        assert self.mockinstaller.deploy_autohsts.called\n\n    @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer')\n    def test_enhancement_updates_not_called(self, mock_geti):\n        self.config.disable_renew_updates = True\n        mock_geti.return_value = self.mockinstaller\n        updater.run_generic_updaters(self.config, mock.MagicMock(), None)\n        assert self.mockinstaller.update_autohsts.called is False\n\n    def test_enhancement_deployer_not_called(self):\n        self.config.disable_renew_updates = True\n        updater.run_renewal_deployer(self.config, mock.MagicMock(),\n                                     self.mockinstaller)\n        assert self.mockinstaller.deploy_autohsts.called is False\n\n    @mock.patch('certbot._internal.plugins.selection.get_unprepared_installer')\n    def test_enhancement_no_updater(self, mock_geti):\n        FAKEINDEX = [\n            {\n                \"name\": \"Test\",\n                \"class\": enhancements.AutoHSTSEnhancement,\n                \"updater_function\": None,\n                \"deployer_function\": \"deploy_autohsts\",\n                \"enable_function\": \"enable_autohsts\"\n            }\n        ]\n        mock_geti.return_value = self.mockinstaller\n        with mock.patch(\"certbot.plugins.enhancements._INDEX\", FAKEINDEX):\n            updater.run_generic_updaters(self.config, mock.MagicMock(), None)\n        assert self.mockinstaller.update_autohsts.called is False\n\n    def test_enhancement_no_deployer(self):\n        FAKEINDEX = [\n            {\n                \"name\": \"Test\",\n                \"class\": enhancements.AutoHSTSEnhancement,\n                \"updater_function\": \"deploy_autohsts\",\n                \"deployer_function\": None,\n                \"enable_function\": \"enable_autohsts\"\n            }\n        ]\n        with mock.patch(\"certbot.plugins.enhancements._INDEX\", FAKEINDEX):\n            updater.run_renewal_deployer(self.config, mock.MagicMock(),\n                                         self.mockinstaller)\n        assert self.mockinstaller.deploy_autohsts.called is False\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/reverter_test.py",
    "content": "\"\"\"Test certbot.reverter.\"\"\"\nimport csv\nimport logging\nimport shutil\nimport sys\nimport tempfile\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.tests import util as test_util\n\n\nclass ReverterCheckpointLocalTest(test_util.ConfigTestCase):\n    \"\"\"Test the Reverter Class.\"\"\"\n    def setUp(self):\n        super().setUp()\n        from certbot.reverter import Reverter\n\n        # Disable spurious errors... we are trying to test for them\n        logging.disable(logging.CRITICAL)\n\n        self.reverter = Reverter(self.config)\n\n        tup = setup_test_files()\n        self.config1, self.config2, self.dir1, self.dir2, self.sets = tup\n\n    def tearDown(self):\n        shutil.rmtree(self.config.work_dir)\n        shutil.rmtree(self.dir1)\n        shutil.rmtree(self.dir2)\n\n        logging.disable(logging.NOTSET)\n\n    @mock.patch(\"certbot.reverter.Reverter._read_and_append\")\n    def test_no_change(self, mock_read):\n        mock_read.side_effect = OSError(\"cannot even\")\n        try:\n            self.reverter.add_to_checkpoint(self.sets[0], \"save1\")\n        except OSError:\n            pass\n        self.reverter.finalize_checkpoint(\"blah\")\n        path = os.listdir(self.reverter.config.backup_dir)[0]\n        no_change = os.path.join(self.reverter.config.backup_dir, path, \"CHANGES_SINCE\")\n        with open(no_change, \"r\") as f:\n            x = f.read()\n        assert \"No changes\" in x\n\n    def test_basic_add_to_temp_checkpoint(self):\n        # These shouldn't conflict even though they are both named config.txt\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"save1\")\n        self.reverter.add_to_temp_checkpoint(self.sets[1], \"save2\")\n\n        assert os.path.isdir(self.config.temp_checkpoint_dir)\n        assert get_save_notes(\n            self.config.temp_checkpoint_dir) == \"save1save2\"\n        assert not os.path.isfile(\n            os.path.join(self.config.temp_checkpoint_dir, \"NEW_FILES\"))\n\n        assert get_filepaths(self.config.temp_checkpoint_dir) == \\\n            \"{0}\\n{1}\\n\".format(self.config1, self.config2)\n\n    def test_add_to_checkpoint_copy_failure(self):\n        with mock.patch(\"certbot.reverter.shutil.copy2\") as mock_copy2:\n            mock_copy2.side_effect = OSError(\"bad copy\")\n            with pytest.raises(errors.ReverterError):\n                self.reverter.add_to_checkpoint(self.sets[0], \"save1\")\n\n    def test_checkpoint_conflict(self):\n        \"\"\"Make sure that checkpoint errors are thrown appropriately.\"\"\"\n        config3 = os.path.join(self.dir1, \"config3.txt\")\n        self.reverter.register_file_creation(True, config3)\n        update_file(config3, \"This is a new file!\")\n\n        self.reverter.add_to_checkpoint(self.sets[2], \"save1\")\n        # This shouldn't throw an error\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"save2\")\n        # Raise error\n        with pytest.raises(errors.ReverterError):\n            self.reverter.add_to_checkpoint(self.sets[2], \"save3\")\n        # Should not cause an error\n        self.reverter.add_to_checkpoint(self.sets[1], \"save4\")\n\n        # Check to make sure new files are also checked...\n        with pytest.raises(errors.ReverterError):\n            self.reverter.add_to_checkpoint({config3}, \"invalid save\")\n\n    def test_multiple_saves_and_temp_revert(self):\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"save1\")\n        update_file(self.config1, \"updated-directive\")\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"save2-updated dir\")\n        update_file(self.config1, \"new directive change that we won't keep\")\n\n        self.reverter.revert_temporary_config()\n        assert read_in(self.config1) == \"directive-dir1\"\n\n    def test_multiple_registration_fail_and_revert(self):\n\n        config3 = os.path.join(self.dir1, \"config3.txt\")\n        update_file(config3, \"Config3\")\n        config4 = os.path.join(self.dir2, \"config4.txt\")\n        update_file(config4, \"Config4\")\n\n        # Test multiple registrations and two registrations at once\n        self.reverter.register_file_creation(True, self.config1)\n        self.reverter.register_file_creation(True, self.config2)\n        self.reverter.register_file_creation(True, config3, config4)\n\n        # Simulate Certbot crash... recovery routine is run\n        self.reverter.recovery_routine()\n\n        assert not os.path.isfile(self.config1)\n        assert not os.path.isfile(self.config2)\n        assert not os.path.isfile(config3)\n        assert not os.path.isfile(config4)\n\n    def test_multiple_registration_same_file(self):\n        self.reverter.register_file_creation(True, self.config1)\n        self.reverter.register_file_creation(True, self.config1)\n        self.reverter.register_file_creation(True, self.config1)\n        self.reverter.register_file_creation(True, self.config1)\n\n        files = get_new_files(self.config.temp_checkpoint_dir)\n\n        assert len(files) == 1\n\n    def test_register_file_creation_write_error(self):\n        m_open = mock.mock_open()\n        with mock.patch(\"certbot.reverter.open\", m_open, create=True):\n            m_open.side_effect = OSError(\"bad open\")\n            with pytest.raises(errors.ReverterError):\n                self.reverter.register_file_creation(True, self.config1)\n\n    def test_bad_registration(self):\n        # Made this mistake and want to make sure it doesn't happen again...\n        with pytest.raises(errors.ReverterError):\n            self.reverter.register_file_creation(\"filepath\")\n\n    def test_register_undo_command(self):\n        coms = [\n            [\"a2dismod\", \"ssl\"],\n            [\"a2dismod\", \"rewrite\"],\n            [\"cleanslate\"]\n        ]\n        for com in coms:\n            self.reverter.register_undo_command(True, com)\n\n        act_coms = get_undo_commands(self.config.temp_checkpoint_dir)\n\n        for a_com, com in zip(act_coms, coms):\n            assert a_com == com\n\n    def test_bad_register_undo_command(self):\n        m_open = mock.mock_open()\n        with mock.patch(\"certbot.reverter.open\", m_open, create=True):\n            m_open.side_effect = OSError(\"bad open\")\n            with pytest.raises(errors.ReverterError):\n                self.reverter.register_undo_command(True, [\"command\"])\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_run_undo_commands(self, mock_run):\n        mock_run.side_effect = [\"\", errors.SubprocessError]\n        coms = [\n            [\"invalid_command\"],\n            [\"a2dismod\", \"ssl\"],\n        ]\n        for com in coms:\n            self.reverter.register_undo_command(True, com)\n\n        self.reverter.revert_temporary_config()\n\n        assert mock_run.call_count == 2\n\n    def test_recovery_routine_in_progress_failure(self):\n        self.reverter.add_to_checkpoint(self.sets[0], \"perm save\")\n\n        # pylint: disable=protected-access\n        self.reverter._recover_checkpoint = mock.MagicMock(\n            side_effect=errors.ReverterError)\n        with pytest.raises(errors.ReverterError):\n            self.reverter.recovery_routine()\n\n    def test_recover_checkpoint_revert_temp_failures(self):\n\n        mock_recover = mock.MagicMock(\n            side_effect=errors.ReverterError(\"e\"))\n\n        # pylint: disable=protected-access\n        self.reverter._recover_checkpoint = mock_recover\n\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"config1 save\")\n\n        with pytest.raises(errors.ReverterError):\n            self.reverter.revert_temporary_config()\n\n    def test_recover_checkpoint_rollback_failure(self):\n        mock_recover = mock.MagicMock(\n            side_effect=errors.ReverterError(\"e\"))\n        # pylint: disable=protected-access\n        self.reverter._recover_checkpoint = mock_recover\n\n        self.reverter.add_to_checkpoint(self.sets[0], \"config1 save\")\n        self.reverter.finalize_checkpoint(\"Title\")\n\n        with pytest.raises(errors.ReverterError):\n            self.reverter.rollback_checkpoints(1)\n\n    def test_recover_checkpoint_copy_failure(self):\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"save1\")\n\n        with mock.patch(\"certbot.reverter.shutil.copy2\") as mock_copy2:\n            mock_copy2.side_effect = OSError(\"bad copy\")\n            with pytest.raises(errors.ReverterError):\n                self.reverter.revert_temporary_config()\n\n    def test_recover_checkpoint_rm_failure(self):\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"temp save\")\n\n        with mock.patch(\"certbot.reverter.shutil.rmtree\") as mock_rmtree:\n            mock_rmtree.side_effect = OSError(\"Cannot remove tree\")\n            with pytest.raises(errors.ReverterError):\n                self.reverter.revert_temporary_config()\n\n    @mock.patch(\"certbot.reverter.logger.warning\")\n    def test_recover_checkpoint_missing_new_files(self, mock_warn):\n        self.reverter.register_file_creation(\n            True, os.path.join(self.dir1, \"missing_file.txt\"))\n        self.reverter.revert_temporary_config()\n        assert mock_warn.call_count == 1\n\n    @mock.patch(\"certbot.reverter.os.remove\")\n    def test_recover_checkpoint_remove_failure(self, mock_remove):\n        self.reverter.register_file_creation(True, self.config1)\n        mock_remove.side_effect = OSError(\"Can't remove\")\n        with pytest.raises(errors.ReverterError):\n            self.reverter.revert_temporary_config()\n\n    def test_recovery_routine_temp_and_perm(self):\n        # Register a new perm checkpoint file\n        config3 = os.path.join(self.dir1, \"config3.txt\")\n        self.reverter.register_file_creation(False, config3)\n        update_file(config3, \"This is a new perm file!\")\n\n        # Add changes to perm checkpoint\n        self.reverter.add_to_checkpoint(self.sets[0], \"perm save1\")\n        update_file(self.config1, \"updated perm config1\")\n        self.reverter.add_to_checkpoint(self.sets[1], \"perm save2\")\n        update_file(self.config2, \"updated perm config2\")\n\n        # Add changes to a temporary checkpoint\n        self.reverter.add_to_temp_checkpoint(self.sets[0], \"temp save1\")\n        update_file(self.config1, \"second update now temp config1\")\n\n        # Register a new temp checkpoint file\n        config4 = os.path.join(self.dir2, \"config4.txt\")\n        self.reverter.register_file_creation(True, config4)\n        update_file(config4, \"New temporary file!\")\n\n        # Now erase everything\n        self.reverter.recovery_routine()\n\n        # Now Run tests\n        # These were new files.. they should be removed\n        assert not os.path.isfile(config3)\n        assert not os.path.isfile(config4)\n\n        # Check to make sure everything got rolled back appropriately\n        assert read_in(self.config1) == \"directive-dir1\"\n        assert read_in(self.config2) == \"directive-dir2\"\n\n\nclass TestFullCheckpointsReverter(test_util.ConfigTestCase):\n    \"\"\"Tests functions having to deal with full checkpoints.\"\"\"\n    def setUp(self):\n        super().setUp()\n        from certbot.reverter import Reverter\n\n        # Disable spurious errors...\n        logging.disable(logging.CRITICAL)\n\n        self.reverter = Reverter(self.config)\n\n        tup = setup_test_files()\n        self.config1, self.config2, self.dir1, self.dir2, self.sets = tup\n\n    def tearDown(self):\n        shutil.rmtree(self.config.work_dir)\n        shutil.rmtree(self.dir1)\n        shutil.rmtree(self.dir2)\n\n        logging.disable(logging.NOTSET)\n\n    def test_rollback_improper_inputs(self):\n        with pytest.raises(errors.ReverterError):\n            self.reverter.rollback_checkpoints(\"-1\")\n        with pytest.raises(errors.ReverterError):\n            self.reverter.rollback_checkpoints(-1000)\n        with pytest.raises(errors.ReverterError):\n            self.reverter.rollback_checkpoints(\"one\")\n\n    def test_rollback_finalize_checkpoint_valid_inputs(self):\n\n        config3 = self._setup_three_checkpoints()\n\n        # Check resulting backup directory\n        assert len(os.listdir(self.config.backup_dir)) == 3\n        # Check rollbacks\n        # First rollback\n        self.reverter.rollback_checkpoints(1)\n        assert read_in(self.config1) == \"update config1\"\n        assert read_in(self.config2) == \"update config2\"\n        # config3 was not included in checkpoint\n        assert read_in(config3) == \"Final form config3\"\n\n        # Second rollback\n        self.reverter.rollback_checkpoints(1)\n        assert read_in(self.config1) == \"update config1\"\n        assert read_in(self.config2) == \"directive-dir2\"\n        assert not os.path.isfile(config3)\n\n        # One dir left... check title\n        all_dirs = os.listdir(self.config.backup_dir)\n        assert len(all_dirs) == 1\n        assert \"First Checkpoint\" in get_save_notes(\n                os.path.join(self.config.backup_dir, all_dirs[0]))\n        # Final rollback\n        self.reverter.rollback_checkpoints(1)\n        assert read_in(self.config1) == \"directive-dir1\"\n\n    def test_finalize_checkpoint_no_in_progress(self):\n        # No need to warn for this... just make sure there are no errors.\n        self.reverter.finalize_checkpoint(\"No checkpoint...\")\n\n    @mock.patch(\"certbot.reverter.shutil.move\")\n    def test_finalize_checkpoint_cannot_title(self, mock_move):\n        self.reverter.add_to_checkpoint(self.sets[0], \"perm save\")\n        mock_move.side_effect = OSError(\"cannot move\")\n\n        with pytest.raises(errors.ReverterError):\n            self.reverter.finalize_checkpoint(\"Title\")\n\n    @mock.patch(\"certbot.reverter.filesystem.replace\")\n    def test_finalize_checkpoint_no_rename_directory(self, mock_replace):\n\n        self.reverter.add_to_checkpoint(self.sets[0], \"perm save\")\n        mock_replace.side_effect = OSError\n\n        with pytest.raises(errors.ReverterError):\n            self.reverter.finalize_checkpoint(\"Title\")\n\n    @mock.patch(\"certbot.reverter.logger\")\n    def test_rollback_too_many(self, mock_logger):\n        # Test no exist warning...\n        self.reverter.rollback_checkpoints(1)\n        assert mock_logger.warning.call_count == 1\n\n        # Test Generic warning\n        self._setup_three_checkpoints()\n        mock_logger.warning.reset_mock()\n        self.reverter.rollback_checkpoints(4)\n        assert mock_logger.warning.call_count == 1\n\n    def test_multi_rollback(self):\n        config3 = self._setup_three_checkpoints()\n        self.reverter.rollback_checkpoints(3)\n\n        assert read_in(self.config1) == \"directive-dir1\"\n        assert read_in(self.config2) == \"directive-dir2\"\n        assert not os.path.isfile(config3)\n\n    def _setup_three_checkpoints(self):\n        \"\"\"Generate some finalized checkpoints.\"\"\"\n        # Checkpoint1 - config1\n        self.reverter.add_to_checkpoint(self.sets[0], \"first save\")\n        self.reverter.finalize_checkpoint(\"First Checkpoint\")\n\n        update_file(self.config1, \"update config1\")\n\n        # Checkpoint2 - new file config3, update config2\n        config3 = os.path.join(self.dir1, \"config3.txt\")\n        self.reverter.register_file_creation(False, config3)\n        update_file(config3, \"directive-config3\")\n        self.reverter.add_to_checkpoint(self.sets[1], \"second save\")\n        self.reverter.finalize_checkpoint(\"Second Checkpoint\")\n\n        update_file(self.config2, \"update config2\")\n        update_file(config3, \"update config3\")\n\n        # Checkpoint3 - update config1, config2\n        self.reverter.add_to_checkpoint(self.sets[2], \"third save\")\n        self.reverter.finalize_checkpoint(\"Third Checkpoint - Save both\")\n\n        update_file(self.config1, \"Final form config1\")\n        update_file(self.config2, \"Final form config2\")\n        update_file(config3, \"Final form config3\")\n\n        return config3\n\n\ndef setup_test_files():\n    \"\"\"Setup sample configuration files.\"\"\"\n    dir1 = tempfile.mkdtemp(\"dir1\")\n    dir2 = tempfile.mkdtemp(\"dir2\")\n    config1 = os.path.join(dir1, \"config.txt\")\n    config2 = os.path.join(dir2, \"config.txt\")\n    with open(config1, \"w\") as file_fd:\n        file_fd.write(\"directive-dir1\")\n    with open(config2, \"w\") as file_fd:\n        file_fd.write(\"directive-dir2\")\n\n    sets = [{config1},\n            {config2},\n            {config1, config2}]\n\n    return config1, config2, dir1, dir2, sets\n\n\ndef get_save_notes(dire):\n    \"\"\"Read save notes\"\"\"\n    return read_in(os.path.join(dire, \"CHANGES_SINCE\"))\n\n\ndef get_filepaths(dire):\n    \"\"\"Get Filepaths\"\"\"\n    return read_in(os.path.join(dire, \"FILEPATHS\"))\n\n\ndef get_new_files(dire):\n    \"\"\"Get new files.\"\"\"\n    return read_in(os.path.join(dire, \"NEW_FILES\")).splitlines()\n\n\ndef get_undo_commands(dire):\n    \"\"\"Get new files.\"\"\"\n    with open(os.path.join(dire, \"COMMANDS\")) as csvfile:\n        return list(csv.reader(csvfile))\n\n\ndef read_in(path):\n    \"\"\"Read in a file, return the str\"\"\"\n    with open(path, \"r\") as file_fd:\n        return file_fd.read()\n\n\ndef update_file(filename, string):\n    \"\"\"Update a file with a new value.\"\"\"\n    with open(filename, \"w\") as file_fd:\n        file_fd.write(string)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/san_test.py",
    "content": "\"\"\"Tests for the san module\"\"\"\nimport ipaddress\nfrom datetime import datetime\n\nimport pytest\nimport unittest\n\nfrom cryptography import x509\nfrom cryptography.x509.oid import NameOID\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\n\nfrom certbot._internal import san\n\nclass SanTest(unittest.TestCase):\n    def test_str(self) -> None:\n        assert str(san.DNSName(\"example.com\")) == \"example.com\"\n        assert str(san.IPAddress(\"192.168.1.1\")) == \"192.168.1.1\"\n\n    def test_hash(self) -> None:\n        assert hash(san.DNSName(\"example.com\")) == hash(san.DNSName(\"example.com\"))\n        assert hash(san.DNSName(\"EXAMPLE.COM.\")) == hash(san.DNSName(\"example.com\"))\n        assert hash(san.IPAddress(\"192.168.1.1\")) == hash(san.IPAddress(\"192.168.1.1\"))\n\n    def test_repr(self) -> None:\n        assert repr(san.DNSName(\"example.com\")) == \"DNS(example.com)\"\n        assert repr(san.IPAddress(\"192.168.1.1\")) == \"IP(192.168.1.1)\"\n\n    def test_eq(self) -> None:\n        with pytest.raises(TypeError):\n            san.DNSName(\"example.com\") == \"example.com\" # pylint: disable=expression-not-assigned\n        with pytest.raises(TypeError):\n            \"example.com\" == san.DNSName(\"example.com\") # pylint: disable=expression-not-assigned\n        with pytest.raises(TypeError):\n            san.IPAddress(\"192.168.1.1\") == \"192.168.1.1\" # pylint: disable=expression-not-assigned\n        with pytest.raises(TypeError):\n            \"192.168.1.1\" == san.IPAddress(\"192.168.1.1\") # pylint: disable=expression-not-assigned\n        assert san.DNSName(\"example.com\") == san.DNSName(\"example.com\")\n        assert san.DNSName(\"Example.com\") == san.DNSName(\"example.com\")\n        assert san.DNSName(\"example.com\") == san.DNSName(\"Example.com\")\n        assert san.DNSName(\"EXAMPLE.COM\") == san.DNSName(\"Example.com\")\n        assert san.DNSName(\"EXAMPLE.COM.\") == san.DNSName(\"example.com\")\n\n    def test_is_wildcard(self) -> None:\n        assert not san.DNSName(\"example.com\").is_wildcard()\n        assert not san.DNSName(\"example.*.com\").is_wildcard()\n        assert san.DNSName(\"*.example.com\").is_wildcard()\n        assert not san.IPAddress(\"192.168.1.1\").is_wildcard()\n\n    def test_split(self) -> None:\n        assert san.split([]) == ([], [])\n        assert san.split([san.IPAddress(\"192.168.1.1\")]) == ([], [san.IPAddress(\"192.168.1.1\")])\n        assert san.split([san.DNSName(\"example.com\")]) == ([san.DNSName(\"example.com\")], [])\n        assert san.split([san.DNSName(\"example.com\"), san.IPAddress(\"192.168.1.1\"),\n                          san.DNSName(\"example.org\"), san.IPAddress(\"192.168.1.2\")]) == (\n                          [san.DNSName(\"example.com\"), san.DNSName(\"example.org\")],\n                          [san.IPAddress(\"192.168.1.1\"), san.IPAddress(\"192.168.1.2\")])\n\n    def test_display(self) -> None:\n        assert san.display([san.DNSName(\"example.com\")]) == \"example.com\"\n        assert san.display([san.DNSName(\"example.com\"), san.IPAddress(\"192.168.1.1\")]) \\\n            == \"example.com, 192.168.1.1\"\n\n    def test_ip_address(self) -> None:\n        with pytest.raises(ValueError):\n            san.IPAddress(\"example.com\")\n\nclass FromX509Test(unittest.TestCase):\n    def test_csr(self) -> None:\n        key = ec.generate_private_key(ec.SECP256R1())\n        csr = (\n            x509.CertificateSigningRequestBuilder()\n            .subject_name(x509.Name([]))\n            .add_extension(\n                x509.SubjectAlternativeName(\n                    [x509.DNSName(\"example.com\"),\n                     x509.IPAddress(ipaddress.ip_address(\"192.168.1.1\"))]\n                ),\n                critical=False,\n            )\n        ).sign(key, hashes.SHA256())\n        result = san.from_x509(csr.subject, csr.extensions)\n        assert result == (\n            [san.DNSName(\"example.com\")],\n            [san.IPAddress(\"192.168.1.1\")],\n        )\n\n    def test_cert(self) -> None:\n        key = ec.generate_private_key(ec.SECP256R1())\n        cert = (\n            x509.CertificateBuilder()\n            .serial_number(x509.random_serial_number())\n            .not_valid_before(datetime.now())\n            .not_valid_after(datetime.now())\n            .subject_name(x509.Name([]))\n            .issuer_name(x509.Name([]))\n            .public_key(key.public_key())\n            .add_extension(\n                x509.SubjectAlternativeName(\n                    [x509.DNSName(\"example.com\"),\n                     x509.IPAddress(ipaddress.ip_address(\"192.168.1.1\"))]\n                ),\n                critical=False,\n            )\n        ).sign(key, hashes.SHA256())\n        result = san.from_x509(cert.subject, cert.extensions)\n        assert result == (\n            [san.DNSName(\"example.com\")],\n            [san.IPAddress(\"192.168.1.1\")],\n        )\n\n    def test_cn(self) -> None:\n        key = ec.generate_private_key(ec.SECP256R1())\n        csr = (\n            x509.CertificateSigningRequestBuilder()\n            .subject_name(x509.Name([\n                x509.NameAttribute(NameOID.COMMON_NAME, \"common.example\"),\n            ]))\n            .add_extension(\n                x509.SubjectAlternativeName(\n                    [x509.DNSName(\"example.com\"),\n                     x509.IPAddress(ipaddress.ip_address(\"192.168.1.1\"))]\n                ),\n                critical=False,\n            )\n        ).sign(key, hashes.SHA256())\n        result = san.from_x509(csr.subject, csr.extensions)\n        assert result == (\n            [san.DNSName(\"common.example\"), san.DNSName(\"example.com\")],\n            [san.IPAddress(\"192.168.1.1\")],\n        )\n\n    def test_cn_duplicate(self) -> None:\n        key = ec.generate_private_key(ec.SECP256R1())\n        csr = (\n            x509.CertificateSigningRequestBuilder()\n            .subject_name(x509.Name([\n                x509.NameAttribute(NameOID.COMMON_NAME, \"example.com\"),\n            ]))\n            .add_extension(\n                x509.SubjectAlternativeName(\n                    [x509.DNSName(\"example.com\"),\n                     x509.IPAddress(ipaddress.ip_address(\"192.168.1.1\"))]\n                ),\n                critical=False,\n            )\n        ).sign(key, hashes.SHA256())\n        result = san.from_x509(csr.subject, csr.extensions)\n        assert result == (\n            [san.DNSName(\"example.com\")],\n            [san.IPAddress(\"192.168.1.1\")],\n        )\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/storage_test.py",
    "content": "\"\"\"Tests for certbot._internal.storage.\"\"\"\n# pylint disable=protected-access\nimport datetime\nimport shutil\nimport stat\nimport sys\nimport unittest\nfrom unittest import mock\nimport zoneinfo\n\nimport configobj\nimport pytest\n\nimport certbot\nfrom certbot import errors\nfrom certbot._internal.storage import ALL_FOUR\nfrom certbot._internal import san\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\n\ndef unlink_all(rc_object):\n    \"\"\"Unlink all four items associated with this RenewableCert.\"\"\"\n    for kind in ALL_FOUR:\n        os.unlink(getattr(rc_object, kind))\n\n\ndef fill_with_sample_data(rc_object):\n    \"\"\"Put dummy data into all four files of this RenewableCert.\"\"\"\n    for kind in ALL_FOUR:\n        with open(getattr(rc_object, kind), \"w\") as f:\n            f.write(kind)\n\n\nclass RelevantValuesTest(unittest.TestCase):\n    \"\"\"Tests for certbot._internal.storage.relevant_values.\"\"\"\n\n    def setUp(self):\n        self.values = {\"server\": \"example.org\", \"key_type\": \"rsa\"}\n        self.mock_config = mock.MagicMock()\n        self.mock_config.set_by_user = mock.MagicMock()\n\n    def _call(self, values):\n        from certbot._internal.storage import relevant_values\n        self.mock_config.to_dict.return_value = values\n        return relevant_values(self.mock_config)\n\n    @mock.patch(\"certbot._internal.plugins.disco.PluginsRegistry.find_all\")\n    def test_namespace(self, mock_find_all):\n        mock_find_all.return_value = [\"certbot-foo:bar\"]\n        self.mock_config.set_by_user.return_value = True\n\n        self.values[\"certbot_foo:bar_baz\"] = 42\n        assert self._call(self.values.copy()) == self.values\n\n    def test_option_set(self):\n        self.mock_config.set_by_user.return_value = True\n\n        self.values[\"allow_subset_of_names\"] = True\n        self.values[\"authenticator\"] = \"apache\"\n        self.values[\"rsa_key_size\"] = 1337\n        expected_relevant_values = self.values.copy()\n        self.values[\"hello\"] = \"there\"\n\n        assert self._call(self.values) == expected_relevant_values\n\n    def test_option_unset(self):\n        self.mock_config.set_by_user.return_value = False\n\n        expected_relevant_values = self.values.copy()\n        self.values[\"rsa_key_size\"] = 2048\n\n        assert self._call(self.values) == expected_relevant_values\n\n    def test_deprecated_item(self):\n        deprected_option = 'manual_public_ip_logging_ok'\n        self.mock_config.set_by_user = lambda v: False if v == deprected_option else True\n        # deprecated items should never be relevant to store\n        expected_relevant_values = self.values.copy()\n        self.values[deprected_option] = None\n        assert self._call(self.values) == expected_relevant_values\n        self.values[deprected_option] = True\n        assert self._call(self.values) == expected_relevant_values\n        self.values[deprected_option] = False\n        assert self._call(self.values) == expected_relevant_values\n\n    def test_with_real_parser(self):\n        from certbot._internal.storage import relevant_values\n        from certbot._internal.plugins import disco\n        from certbot._internal import cli\n        from certbot._internal import constants\n\n        PLUGINS = disco.PluginsRegistry.find_all()\n        namespace = cli.prepare_and_parse_args(PLUGINS, [\n            '--allow-subset-of-names',\n            '--authenticator', 'apache',\n            '--preferred-profile', 'fancyprofile',\n        ])\n        expected_relevant_values = {\n            'server': constants.CLI_DEFAULTS['server'],\n            'key_type': 'ecdsa',\n            'allow_subset_of_names': True,\n            'authenticator': 'apache',\n            'preferred_profile': 'fancyprofile',\n        }\n\n        assert relevant_values(namespace) == expected_relevant_values\n\n    def test_with_required_profile(self):\n        self.values[\"required_profile\"] = \"shortlived\"\n        expected_relevant_values = self.values.copy()\n        assert self._call(self.values) == expected_relevant_values\n\nclass BaseRenewableCertTest(test_util.ConfigTestCase):\n    \"\"\"Base class for setting up Renewable Cert tests.\n\n    .. note:: It may be required to write out self.config for\n    your test.  Check :class:`.cli_test.DuplicateCertTest` for an example.\n\n    \"\"\"\n\n    def setUp(self):\n        from certbot._internal import storage\n\n        super().setUp()\n\n        # TODO: maybe provide NamespaceConfig.make_dirs?\n        # TODO: main() should create those dirs, c.f. #902\n        filesystem.makedirs(os.path.join(self.config.config_dir, \"live\", \"example.org\"))\n        archive_path = os.path.join(self.config.config_dir, \"archive\", \"example.org\")\n        filesystem.makedirs(archive_path)\n        filesystem.makedirs(os.path.join(self.config.config_dir, \"renewal\"))\n\n        config_file = configobj.ConfigObj()\n        for kind in ALL_FOUR:\n            kind_path = os.path.join(self.config.config_dir, \"live\", \"example.org\",\n                                        kind + \".pem\")\n            config_file[kind] = kind_path\n        with open(os.path.join(self.config.config_dir, \"live\", \"example.org\",\n                                        \"README\"), 'a'):\n            pass\n        config_file[\"archive\"] = archive_path\n        config_file.filename = os.path.join(self.config.config_dir, \"renewal\",\n                                       \"example.org.conf\")\n        config_file.write()\n        self.config_file = config_file\n\n        # We also create a file that isn't a renewal config in the same\n        # location to test that logic that reads in all-and-only renewal\n        # configs will ignore it and NOT attempt to parse it.\n        with open(os.path.join(self.config.config_dir, \"renewal\", \"IGNORE.THIS\"), \"w\") as junk:\n            junk.write(\"This file should be ignored!\")\n\n        self.defaults = configobj.ConfigObj()\n\n        with mock.patch(\"certbot._internal.storage.RenewableCert._check_symlinks\") as check:\n            check.return_value = True\n            self.test_rc = storage.RenewableCert(config_file.filename, self.config)\n\n    def _write_out_kind(self, kind, ver, value=None):\n        link = getattr(self.test_rc, kind)\n        if os.path.lexists(link):\n            os.unlink(link)\n        os.symlink(os.path.join(os.path.pardir, os.path.pardir, \"archive\",\n                                \"example.org\", \"{0}{1}.pem\".format(kind, ver)),\n                   link)\n        with open(link, \"wb\") as f:\n            f.write(kind.encode('ascii') if value is None else value)\n        if kind == \"privkey\":\n            filesystem.chmod(link, 0o600)\n\n    def _write_out_ex_kinds(self):\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 12)\n            self._write_out_kind(kind, 11)\n\n\nclass RenewableCertTests(BaseRenewableCertTest):\n    \"\"\"Tests for certbot._internal.storage.\"\"\"\n\n    def test_initialization(self):\n        assert self.test_rc.lineagename == \"example.org\"\n        for kind in ALL_FOUR:\n            assert getattr(self.test_rc, kind) == os.path.join(\n                    self.config.config_dir, \"live\", \"example.org\", kind + \".pem\")\n\n    def test_renewal_bad_config(self):\n        \"\"\"Test that the RenewableCert constructor will complain if\n        the renewal configuration file doesn't end in \".conf\"\n\n        \"\"\"\n        from certbot._internal import storage\n        broken = os.path.join(self.config.config_dir, \"broken.conf\")\n        with open(broken, \"w\") as f:\n            f.write(\"[No closing bracket for you!\")\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert(broken, self.config)\n        os.unlink(broken)\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert(\"fun\", self.config)\n\n    def test_renewal_incomplete_config(self):\n        \"\"\"Test that the RenewableCert constructor will complain if\n        the renewal configuration file is missing a required file element.\"\"\"\n        from certbot._internal import storage\n        config = configobj.ConfigObj()\n        config[\"cert\"] = \"imaginary_cert.pem\"\n        # Here the required privkey is missing.\n        config[\"chain\"] = \"imaginary_chain.pem\"\n        config[\"fullchain\"] = \"imaginary_fullchain.pem\"\n        config.filename = os.path.join(self.config.config_dir, \"imaginary_config.conf\")\n        config.write()\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert(config.filename, self.config)\n\n    def test_no_renewal_version(self):\n        from certbot._internal import storage\n\n        self._write_out_ex_kinds()\n        assert \"version\" not in self.config_file\n\n        with mock.patch(\"certbot._internal.storage.logger\") as mock_logger:\n            storage.RenewableCert(self.config_file.filename, self.config)\n        assert mock_logger.warning.called is False\n\n    def test_renewal_newer_version(self):\n        from certbot._internal import storage\n\n        self._write_out_ex_kinds()\n        self.config_file[\"version\"] = \"99.99.99\"\n        self.config_file.write()\n\n        with mock.patch(\"certbot._internal.storage.logger\") as mock_logger:\n            storage.RenewableCert(self.config_file.filename, self.config)\n        assert mock_logger.info.called\n        assert \"version\" in mock_logger.info.call_args[0][0]\n\n    def test_consistent(self):\n        # pylint: disable=protected-access\n        oldcert = self.test_rc.cert\n        self.test_rc.cert = \"relative/path\"\n        # Absolute path for item requirement\n        assert not self.test_rc._consistent()\n        self.test_rc.cert = oldcert\n        # Items must exist requirement\n        assert not self.test_rc._consistent()\n        # Items must be symlinks requirements\n        fill_with_sample_data(self.test_rc)\n        assert not self.test_rc._consistent()\n        unlink_all(self.test_rc)\n        # Items must point to desired place if they are relative\n        for kind in ALL_FOUR:\n            os.symlink(os.path.join(\"..\", kind + \"17.pem\"),\n                       getattr(self.test_rc, kind))\n        assert not self.test_rc._consistent()\n        unlink_all(self.test_rc)\n        # Items must point to desired place if they are absolute\n        for kind in ALL_FOUR:\n            os.symlink(os.path.join(self.config.config_dir, kind + \"17.pem\"),\n                       getattr(self.test_rc, kind))\n        assert not self.test_rc._consistent()\n        unlink_all(self.test_rc)\n        # Items must point to things that exist\n        for kind in ALL_FOUR:\n            os.symlink(os.path.join(\"..\", \"..\", \"archive\", \"example.org\",\n                                    kind + \"17.pem\"),\n                       getattr(self.test_rc, kind))\n        assert not self.test_rc._consistent()\n        # This version should work\n        fill_with_sample_data(self.test_rc)\n        assert self.test_rc._consistent()\n        # Items must point to things that follow the naming convention\n        os.unlink(self.test_rc.fullchain)\n        os.symlink(os.path.join(\"..\", \"..\", \"archive\", \"example.org\",\n                                \"fullchain_17.pem\"), self.test_rc.fullchain)\n        with open(self.test_rc.fullchain, \"w\") as f:\n            f.write(\"wrongly-named fullchain\")\n        assert not self.test_rc._consistent()\n\n    def test_current_target(self):\n        # Relative path logic\n        self._write_out_kind(\"cert\", 17)\n        assert os.path.samefile(self.test_rc.current_target(\"cert\"),\n                                         os.path.join(self.config.config_dir, \"archive\",\n                                                      \"example.org\",\n                                                      \"cert17.pem\"))\n        # Absolute path logic\n        os.unlink(self.test_rc.cert)\n        os.symlink(os.path.join(self.config.config_dir, \"archive\", \"example.org\",\n                                \"cert17.pem\"), self.test_rc.cert)\n        with open(self.test_rc.cert, \"w\") as f:\n            f.write(\"cert\")\n        assert os.path.samefile(self.test_rc.current_target(\"cert\"),\n                                         os.path.join(self.config.config_dir, \"archive\",\n                                                      \"example.org\",\n                                                      \"cert17.pem\"))\n\n    def test_current_version(self):\n        for ver in (1, 5, 10, 20):\n            self._write_out_kind(\"cert\", ver)\n        os.unlink(self.test_rc.cert)\n        os.symlink(os.path.join(\"..\", \"..\", \"archive\", \"example.org\",\n                                \"cert10.pem\"), self.test_rc.cert)\n        assert self.test_rc.current_version(\"cert\") == 10\n\n    def test_no_current_version(self):\n        assert self.test_rc.current_version(\"cert\") is None\n\n    def test_latest_and_next_versions(self):\n        for ver in range(1, 6):\n            for kind in ALL_FOUR:\n                self._write_out_kind(kind, ver)\n        assert self.test_rc.latest_common_version() == 5\n        assert self.test_rc.next_free_version() == 6\n        # Having one kind of file of a later version doesn't change the\n        # result\n        self._write_out_kind(\"privkey\", 7)\n        assert self.test_rc.latest_common_version() == 5\n        # ... although it does change the next free version\n        assert self.test_rc.next_free_version() == 8\n        # Nor does having three out of four change the result\n        self._write_out_kind(\"cert\", 7)\n        self._write_out_kind(\"fullchain\", 7)\n        assert self.test_rc.latest_common_version() == 5\n        # If we have everything from a much later version, it does change\n        # the result\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 17)\n        assert self.test_rc.latest_common_version() == 17\n        assert self.test_rc.next_free_version() == 18\n\n    @mock.patch(\"certbot._internal.storage.logger\")\n    def test_ensure_deployed(self, mock_logger):\n        mock_update = self.test_rc.update_all_links_to = mock.Mock()\n        mock_has_pending = self.test_rc.has_pending_deployment = mock.Mock()\n        self.test_rc.latest_common_version = mock.Mock()\n\n        mock_has_pending.return_value = False\n        assert self.test_rc.ensure_deployed() is True\n        assert mock_update.call_count == 0\n        assert mock_logger.warning.call_count == 0\n\n        mock_has_pending.return_value = True\n        assert self.test_rc.ensure_deployed() is False\n        assert mock_update.call_count == 1\n        assert mock_logger.warning.call_count == 1\n\n\n    def test_update_link_to(self):\n        for ver in range(1, 6):\n            for kind in ALL_FOUR:\n                self._write_out_kind(kind, ver)\n                assert ver == self.test_rc.current_version(kind)\n        # pylint: disable=protected-access\n        self.test_rc._update_link_to(\"cert\", 3)\n        self.test_rc._update_link_to(\"privkey\", 2)\n        assert 3 == self.test_rc.current_version(\"cert\")\n        assert 2 == self.test_rc.current_version(\"privkey\")\n        assert 5 == self.test_rc.current_version(\"chain\")\n        assert 5 == self.test_rc.current_version(\"fullchain\")\n        # Currently we are allowed to update to a version that doesn't exist\n        self.test_rc._update_link_to(\"chain\", 3000)\n        # However, current_version doesn't allow querying the resulting\n        # version (because it's a broken link).\n        assert os.path.basename(filesystem.readlink(self.test_rc.chain)) == \\\n                         \"chain3000.pem\"\n\n    def test_version(self):\n        self._write_out_kind(\"cert\", 12)\n        # TODO: We should probably test that the directory is still the\n        #       same, but it's tricky because we can get an absolute\n        #       path out when we put a relative path in.\n        assert \"cert8.pem\" == \\\n                         os.path.basename(self.test_rc.version(\"cert\", 8))\n\n    def test_update_all_links_to_success(self):\n        for ver in range(1, 6):\n            for kind in ALL_FOUR:\n                self._write_out_kind(kind, ver)\n                assert ver == self.test_rc.current_version(kind)\n        assert self.test_rc.latest_common_version() == 5\n        for ver in range(1, 6):\n            self.test_rc.update_all_links_to(ver)\n            for kind in ALL_FOUR:\n                assert ver == self.test_rc.current_version(kind)\n            assert self.test_rc.latest_common_version() == 5\n\n    def test_update_all_links_to_partial_failure(self):\n        def unlink_or_raise(path, real_unlink=os.unlink):\n            # pylint: disable=missing-docstring\n            basename = os.path.basename(path)\n            if \"fullchain\" in basename and basename.startswith(\"prev\"):\n                raise ValueError\n            real_unlink(path)\n\n        self._write_out_ex_kinds()\n        with mock.patch(\"certbot._internal.storage.os.unlink\") as mock_unlink:\n            mock_unlink.side_effect = unlink_or_raise\n            with pytest.raises(ValueError):\n                self.test_rc.update_all_links_to(12)\n\n        for kind in ALL_FOUR:\n            assert self.test_rc.current_version(kind) == 12\n\n    def test_update_all_links_to_full_failure(self):\n        def unlink_or_raise(path, real_unlink=os.unlink):\n            # pylint: disable=missing-docstring\n            if \"fullchain\" in os.path.basename(path):\n                raise ValueError\n            real_unlink(path)\n\n        self._write_out_ex_kinds()\n        with mock.patch(\"certbot._internal.storage.os.unlink\") as mock_unlink:\n            mock_unlink.side_effect = unlink_or_raise\n            with pytest.raises(ValueError):\n                self.test_rc.update_all_links_to(12)\n\n        for kind in ALL_FOUR:\n            assert self.test_rc.current_version(kind) == 11\n\n    def test_has_pending_deployment(self):\n        for ver in range(1, 6):\n            for kind in ALL_FOUR:\n                self._write_out_kind(kind, ver)\n                assert ver == self.test_rc.current_version(kind)\n        for ver in range(1, 6):\n            self.test_rc.update_all_links_to(ver)\n            for kind in ALL_FOUR:\n                assert ver == self.test_rc.current_version(kind)\n            if ver < 5:\n                assert self.test_rc.has_pending_deployment()\n            else:\n                assert not self.test_rc.has_pending_deployment()\n\n    def test_sans(self):\n        # Trying the current version\n        self._write_out_kind(\"cert\", 12, test_util.load_vector(\"cert-san_512.pem\"))\n\n        assert self.test_rc.sans() == \\\n                         [san.DNSName(\"example.com\"), san.DNSName(\"www.example.com\")]\n\n        # Trying missing cert\n        os.unlink(self.test_rc.cert)\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.sans()\n\n    def test_autorenewal_is_enabled(self):\n        self.test_rc.configuration[\"renewalparams\"] = {}\n        assert self.test_rc.autorenewal_is_enabled()\n        self.test_rc.configuration[\"renewalparams\"][\"autorenew\"] = \"True\"\n        assert self.test_rc.autorenewal_is_enabled()\n\n        self.test_rc.configuration[\"renewalparams\"][\"autorenew\"] = \"False\"\n        assert not self.test_rc.autorenewal_is_enabled()\n\n    @mock.patch(\"certbot._internal.storage.relevant_values\")\n    def test_save_successor(self, mock_rv):\n        # Mock relevant_values() to claim that all values are relevant here\n        # (to avoid instantiating parser)\n        mock_rv.side_effect = lambda x: x.to_dict()\n\n        for ver in range(1, 6):\n            for kind in ALL_FOUR:\n                self._write_out_kind(kind, ver)\n        self.test_rc.update_all_links_to(3)\n        assert 6 == self.test_rc.save_successor(3, b'new cert', None,\n                                           b'new chain', self.config)\n        with open(self.test_rc.version(\"cert\", 6)) as f:\n            assert f.read() == \"new cert\"\n        with open(self.test_rc.version(\"chain\", 6)) as f:\n            assert f.read() == \"new chain\"\n        with open(self.test_rc.version(\"fullchain\", 6)) as f:\n            assert f.read() == \"new cert\" + \"new chain\"\n        # version 6 of the key should be a link back to version 3\n        assert not os.path.islink(self.test_rc.version(\"privkey\", 3))\n        assert os.path.islink(self.test_rc.version(\"privkey\", 6))\n        # Let's try two more updates\n        assert 7 == self.test_rc.save_successor(6, b'again', None,\n                                           b'newer chain', self.config)\n        assert 8 == self.test_rc.save_successor(7, b'hello', None,\n                                           b'other chain', self.config)\n        # All of the subsequent versions should link directly to the original\n        # privkey.\n        for i in (6, 7, 8):\n            assert os.path.islink(self.test_rc.version(\"privkey\", i))\n            assert \"privkey3.pem\" == os.path.basename(filesystem.readlink(\n                self.test_rc.version(\"privkey\", i)))\n\n        for kind in ALL_FOUR:\n            assert self.test_rc.available_versions(kind) == list(range(1, 9))\n            assert self.test_rc.current_version(kind) == 3\n        # Test updating from latest version rather than old version\n        self.test_rc.update_all_links_to(8)\n        assert 9 == self.test_rc.save_successor(8, b'last', None,\n                                           b'attempt', self.config)\n        for kind in ALL_FOUR:\n            assert self.test_rc.available_versions(kind) == \\\n                             list(range(1, 10))\n            assert self.test_rc.current_version(kind) == 8\n        with open(self.test_rc.version(\"fullchain\", 9)) as f:\n            assert f.read() == \"last\" + \"attempt\"\n        temp_config_file = os.path.join(self.config.renewal_configs_dir,\n                                        self.test_rc.lineagename) + \".conf.new\"\n        with open(temp_config_file, \"w\") as f:\n            f.write(\"We previously crashed while writing me :(\")\n        # Test updating when providing a new privkey.  The key should\n        # be saved in a new file rather than creating a new symlink.\n        assert 10 == self.test_rc.save_successor(9, b'with', b'a',\n                                            b'key', self.config)\n        assert os.path.exists(self.test_rc.version(\"privkey\", 10))\n        assert not os.path.islink(self.test_rc.version(\"privkey\", 10))\n        assert not os.path.exists(temp_config_file)\n\n    @test_util.skip_on_windows('Group/everybody permissions are not maintained on Windows.')\n    @mock.patch(\"certbot._internal.storage.relevant_values\")\n    def test_save_successor_maintains_group_mode(self, mock_rv):\n        # Mock relevant_values() to claim that all values are relevant here\n        # (to avoid instantiating parser)\n        mock_rv.side_effect = lambda x: x.to_dict()\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 1)\n        self.test_rc.update_all_links_to(1)\n        assert filesystem.check_mode(self.test_rc.version(\"privkey\", 1), 0o600)\n        filesystem.chmod(self.test_rc.version(\"privkey\", 1), 0o444)\n        # If no new key, permissions should be the same (we didn't write any keys)\n        self.test_rc.save_successor(1, b\"newcert\", None, b\"new chain\", self.config)\n        assert filesystem.check_mode(self.test_rc.version(\"privkey\", 2), 0o444)\n        # If new key, permissions should be kept as 644\n        self.test_rc.save_successor(2, b\"newcert\", b\"new_privkey\", b\"new chain\", self.config)\n        assert filesystem.check_mode(self.test_rc.version(\"privkey\", 3), 0o644)\n        # If permissions reverted, next renewal will also revert permissions of new key\n        filesystem.chmod(self.test_rc.version(\"privkey\", 3), 0o400)\n        self.test_rc.save_successor(3, b\"newcert\", b\"new_privkey\", b\"new chain\", self.config)\n        assert filesystem.check_mode(self.test_rc.version(\"privkey\", 4), 0o600)\n\n    @mock.patch(\"certbot._internal.storage.relevant_values\")\n    @mock.patch(\"certbot._internal.storage.filesystem.copy_ownership_and_apply_mode\")\n    def test_save_successor_maintains_gid(self, mock_ownership, mock_rv):\n        # Mock relevant_values() to claim that all values are relevant here\n        # (to avoid instantiating parser)\n        mock_rv.side_effect = lambda x: x.to_dict()\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 1)\n        self.test_rc.update_all_links_to(1)\n        self.test_rc.save_successor(1, b\"newcert\", None, b\"new chain\", self.config)\n        assert mock_ownership.called is False\n        self.test_rc.save_successor(2, b\"newcert\", b\"new_privkey\", b\"new chain\", self.config)\n        assert mock_ownership.called\n\n    @mock.patch(\"certbot._internal.storage.relevant_values\")\n    def test_new_lineage(self, mock_rv):\n        \"\"\"Test for new_lineage() class method.\"\"\"\n        # Mock relevant_values to say everything is relevant here (so we\n        # don't have to mock the parser to help it decide!)\n        mock_rv.side_effect = lambda x: x.to_dict()\n\n        from certbot._internal import storage\n        result = storage.RenewableCert.new_lineage(\n            \"the-lineage.com\", b\"cert\", b\"privkey\", b\"chain\", self.config)\n        # This consistency check tests most relevant properties about the\n        # newly created cert lineage.\n        # pylint: disable=protected-access\n        assert result._consistent()\n        assert os.path.exists(os.path.join(\n            self.config.renewal_configs_dir, \"the-lineage.com.conf\"))\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"README\"))\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"the-lineage.com\", \"README\"))\n        assert filesystem.check_mode(result.key_path, 0o600)\n        with open(result.fullchain, \"rb\") as f:\n            assert f.read() == b\"cert\" + b\"chain\"\n        # Let's do it again and make sure it makes a different lineage\n        result = storage.RenewableCert.new_lineage(\n            \"the-lineage.com\", b\"cert2\", b\"privkey2\", b\"chain2\", self.config)\n        assert os.path.exists(os.path.join(\n            self.config.renewal_configs_dir, \"the-lineage.com-0001.conf\"))\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"the-lineage.com-0001\", \"README\"))\n        # Allow write to existing but empty dir\n        filesystem.mkdir(os.path.join(self.config.default_archive_dir, \"the-lineage.com-0002\"))\n        result = storage.RenewableCert.new_lineage(\n            \"the-lineage.com\", b\"cert3\", b\"privkey3\", b\"chain3\", self.config)\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"the-lineage.com-0002\", \"README\"))\n        assert filesystem.check_mode(result.key_path, 0o600)\n        # Now trigger the detection of already existing files\n        shutil.copytree(os.path.join(self.config.live_dir, \"the-lineage.com\"),\n                        os.path.join(self.config.live_dir, \"the-lineage.com-0003\"))\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert.new_lineage(\"the-lineage.com\",\n                          b\"cert4\", b\"privkey4\", b\"chain4\", self.config)\n        shutil.copytree(os.path.join(self.config.live_dir, \"the-lineage.com\"),\n                        os.path.join(self.config.live_dir, \"other-example.com\"))\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert.new_lineage(\"other-example.com\", b\"cert5\",\n                          b\"privkey5\", b\"chain5\", self.config)\n        # Make sure it can accept renewal parameters\n        result = storage.RenewableCert.new_lineage(\n            \"the-lineage.com\", b\"cert2\", b\"privkey2\", b\"chain2\", self.config)\n        # TODO: Conceivably we could test that the renewal parameters actually\n        #       got saved\n\n    @mock.patch(\"certbot._internal.storage.relevant_values\")\n    def test_new_lineage_nonexistent_dirs(self, mock_rv):\n        \"\"\"Test that directories can be created if they don't exist.\"\"\"\n        # Mock relevant_values to say everything is relevant here (so we\n        # don't have to mock the parser to help it decide!)\n        mock_rv.side_effect = lambda x: x.to_dict()\n\n        from certbot._internal import storage\n        shutil.rmtree(self.config.renewal_configs_dir)\n        shutil.rmtree(self.config.default_archive_dir)\n        shutil.rmtree(self.config.live_dir)\n\n        storage.RenewableCert.new_lineage(\n            \"the-lineage.com\", b\"cert2\", b\"privkey2\", b\"chain2\", self.config)\n        assert os.path.exists(\n            os.path.join(\n                self.config.renewal_configs_dir, \"the-lineage.com.conf\"))\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"the-lineage.com\", \"privkey.pem\"))\n        assert os.path.exists(os.path.join(\n            self.config.default_archive_dir, \"the-lineage.com\", \"privkey1.pem\"))\n\n    @mock.patch(\"certbot._internal.storage.util.unique_lineage_name\")\n    def test_invalid_config_filename(self, mock_uln):\n        from certbot._internal import storage\n        mock_uln.return_value = \"this_does_not_end_with_dot_conf\", \"yikes\"\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert.new_lineage(\"example.com\",\n                          \"cert\", \"privkey\", \"chain\", self.config)\n\n    def test_bad_kind(self):\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.current_target(\"elephant\")\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.current_version(\"elephant\")\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.version(\"elephant\", 17)\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.available_versions(\"elephant\")\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc.newest_available_version(\"elephant\")\n        # pylint: disable=protected-access\n        with pytest.raises(errors.CertStorageError):\n            self.test_rc._update_link_to(\"elephant\", 17)\n\n    @mock.patch(\"certbot._internal.ocsp.RevocationChecker.ocsp_revoked_by_paths\")\n    def test_ocsp_revoked(self, mock_checker):\n        # Write out test files\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 1)\n        version = self.test_rc.latest_common_version()\n        expected_cert_path = self.test_rc.version(\"cert\", version)\n        expected_chain_path = self.test_rc.version(\"chain\", version)\n\n        # Test with cert revoked\n        mock_checker.return_value = True\n        assert self.test_rc.ocsp_revoked(version)\n        assert mock_checker.call_args[0][0] == expected_cert_path\n        assert mock_checker.call_args[0][1] == expected_chain_path\n\n        # Test with cert not revoked\n        mock_checker.return_value = False\n        assert not self.test_rc.ocsp_revoked(version)\n        assert mock_checker.call_args[0][0] == expected_cert_path\n        assert mock_checker.call_args[0][1] == expected_chain_path\n\n        # Test with error\n        mock_checker.side_effect = ValueError\n        with mock.patch(\"certbot._internal.storage.logger.warning\") as logger:\n            assert not self.test_rc.ocsp_revoked(version)\n        assert mock_checker.call_args[0][0] == expected_cert_path\n        assert mock_checker.call_args[0][1] == expected_chain_path\n        log_msg = logger.call_args[0][0]\n        assert \"An error occurred determining the OCSP status\" in log_msg\n\n    def test_add_time_interval(self):\n        from certbot._internal import storage\n\n        # this month has 30 days, and the next year is a leap year\n        time_1 = datetime.datetime(2003, 11, 20, 11, 59, 21, tzinfo=datetime.timezone.utc)\n\n        # this month has 31 days, and the next year is not a leap year\n        time_2 = datetime.datetime(2012, 10, 18, 21, 31, 16, tzinfo=datetime.timezone.utc)\n\n        # in different time zone (GMT+8)\n        time_3 = datetime.datetime(2015, 10, 26, 22, 25, 41, tzinfo=zoneinfo.ZoneInfo('Asia/Shanghai'))\n\n        intended = {\n            (time_1, \"\"): time_1,\n            (time_2, \"\"): time_2,\n            (time_3, \"\"): time_3,\n            (time_1, \"17 days\"): time_1 + datetime.timedelta(17),\n            (time_2, \"17 days\"): time_2 + datetime.timedelta(17),\n            (time_1, \"30\"): time_1 + datetime.timedelta(30),\n            (time_2, \"30\"): time_2 + datetime.timedelta(30),\n            (time_1, \"7 weeks\"): time_1 + datetime.timedelta(49),\n            (time_2, \"7 weeks\"): time_2 + datetime.timedelta(49),\n            # 1 month is always 30 days, no matter which month it is\n            (time_1, \"1 month\"): time_1 + datetime.timedelta(30),\n            (time_2, \"1 month\"): time_2 + datetime.timedelta(31),\n            # 1 year could be 365 or 366 days, depends on the year\n            (time_1, \"1 year\"): time_1 + datetime.timedelta(366),\n            (time_2, \"1 year\"): time_2 + datetime.timedelta(365),\n            (time_1, \"1 year 1 day\"): time_1 + datetime.timedelta(367),\n            (time_2, \"1 year 1 day\"): time_2 + datetime.timedelta(366),\n            (time_1, \"1 year-1 day\"): time_1 + datetime.timedelta(365),\n            (time_2, \"1 year-1 day\"): time_2 + datetime.timedelta(364),\n            (time_1, \"4 years\"): time_1 + datetime.timedelta(1461),\n            (time_2, \"4 years\"): time_2 + datetime.timedelta(1461),\n        }\n\n        for parameters, excepted in intended.items():\n            base_time, interval = parameters\n            assert storage.add_time_interval(base_time, interval) == \\\n                             excepted\n\n    def test_server(self):\n        self.test_rc.configuration[\"renewalparams\"] = {}\n        assert self.test_rc.server is None\n        rp = self.test_rc.configuration[\"renewalparams\"]\n        rp[\"server\"] = \"https://acme.example/dir\"\n        assert self.test_rc.server == \"https://acme.example/dir\"\n\n    def test_is_test_cert(self):\n        self.test_rc.configuration[\"renewalparams\"] = {}\n        rp = self.test_rc.configuration[\"renewalparams\"]\n        assert self.test_rc.is_test_cert is False\n        rp[\"server\"] = \"https://acme-staging-v02.api.letsencrypt.org/directory\"\n        assert self.test_rc.is_test_cert is True\n        rp[\"server\"] = \"https://staging.someotherca.com/directory\"\n        assert self.test_rc.is_test_cert is True\n        rp[\"server\"] = \"https://acme-v01.api.letsencrypt.org/directory\"\n        assert self.test_rc.is_test_cert is False\n        rp[\"server\"] = \"https://acme-v02.api.letsencrypt.org/directory\"\n        assert self.test_rc.is_test_cert is False\n\n    def test_missing_cert(self):\n        from certbot._internal import storage\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert(self.config_file.filename, self.config)\n        os.symlink(\"missing\", self.config_file[ALL_FOUR[0]])\n        with pytest.raises(errors.CertStorageError):\n            storage.RenewableCert(self.config_file.filename, self.config)\n\n    def test_atomic_rewrite(self):\n        # Mostly tested by the process of creating and updating lineages,\n        # but we can test that this successfully creates files, removes\n        # unneeded items, preserves permissions, and preserves comments.\n        temp = os.path.join(self.config.config_dir, \"sample-file\")\n        with open(temp, \"w\") as f:\n            f.write(\"[renewalparams]\\nuseful = value # A useful value\\n\"\n                    \"useless = value # Not needed\\n\")\n        filesystem.chmod(temp, 0o640)\n        perms = stat.S_IMODE(os.lstat(temp).st_mode)\n        config = configobj.ConfigObj()\n        for x in ALL_FOUR:\n            config[x] = \"somewhere\"\n        config[\"version\"] = certbot.__version__\n        config[\"archive_dir\"] = \"the_archive\"\n        config[\"renewalparams\"] = {\"useful\": \"new_value\"}\n\n        from certbot._internal import storage\n        storage.atomic_rewrite(temp, config)\n\n        with open(temp, \"r\") as f:\n            content = f.read()\n        # useful value was updated\n        assert \"useful = new_value\" in content\n        # associated comment was preserved\n        assert \"A useful value\" in content\n        # useless value was deleted\n        assert \"useless\" not in content\n        # check version was stored\n        assert \"version = {0}\".format(certbot.__version__) in content\n        # ensure permissions are copied\n        assert stat.S_IMODE(os.lstat(temp).st_mode) == perms\n\n    def test_truncate(self):\n        # It should not do anything when there's less than 5 cert history\n        for kind in ALL_FOUR:\n            self._write_out_kind(kind, 1)\n        with mock.patch('certbot.compat.os.unlink') as mock_unlink:\n            self.test_rc.truncate()\n            mock_unlink.assert_not_called()\n\n        # It should truncate the excess when there's more than 5 cert history\n        for kind in ALL_FOUR:\n            for i in range(2, 8):\n                self._write_out_kind(kind, i)\n        with mock.patch('certbot.compat.os.unlink') as mock_unlink:\n            self.test_rc.truncate()\n            assert mock_unlink.call_count == 1 * len(ALL_FOUR)\n            assert \"1.pem\" in mock_unlink.call_args_list[0][0][0]\n\nclass DeleteFilesTest(BaseRenewableCertTest):\n    \"\"\"Tests for certbot._internal.storage.delete_files\"\"\"\n    def setUp(self):\n        super().setUp()\n\n        for kind in ALL_FOUR:\n            kind_path = os.path.join(self.config.config_dir, \"live\", \"example.org\",\n                                        kind + \".pem\")\n            with open(kind_path, 'a'):\n                pass\n        self.config_file.write()\n        assert os.path.exists(os.path.join(\n            self.config.renewal_configs_dir, \"example.org.conf\"))\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert os.path.exists(os.path.join(\n            self.config.config_dir, \"archive\", \"example.org\"))\n\n    def _call(self):\n        from certbot._internal import storage\n        with mock.patch(\"certbot._internal.storage.logger\"):\n            storage.delete_files(self.config, \"example.org\")\n\n    def test_delete_all_files(self):\n        self._call()\n\n        assert not os.path.exists(os.path.join(\n            self.config.renewal_configs_dir, \"example.org.conf\"))\n        assert not os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(os.path.join(\n            self.config.config_dir, \"archive\", \"example.org\"))\n\n    def test_bad_renewal_config(self):\n        with open(self.config_file.filename, 'a') as config_file:\n            config_file.write(\"asdfasfasdfasdf\")\n\n        with pytest.raises(errors.CertStorageError):\n            self._call()\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(os.path.join(\n            self.config.renewal_configs_dir, \"example.org.conf\"))\n\n    def test_no_renewal_config(self):\n        os.remove(self.config_file.filename)\n        with pytest.raises(errors.CertStorageError):\n            self._call()\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(self.config_file.filename)\n\n    def test_no_cert_file(self):\n        os.remove(os.path.join(\n            self.config.live_dir, \"example.org\", \"cert.pem\"))\n        self._call()\n        assert not os.path.exists(self.config_file.filename)\n        assert not os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(os.path.join(\n            self.config.config_dir, \"archive\", \"example.org\"))\n\n    def test_no_readme_file(self):\n        os.remove(os.path.join(\n            self.config.live_dir, \"example.org\", \"README\"))\n        self._call()\n        assert not os.path.exists(self.config_file.filename)\n        assert not os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(os.path.join(\n            self.config.config_dir, \"archive\", \"example.org\"))\n\n    def test_livedir_not_empty(self):\n        with open(os.path.join(\n            self.config.live_dir, \"example.org\", \"other_file\"), 'a'):\n            pass\n        self._call()\n        assert not os.path.exists(self.config_file.filename)\n        assert os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(os.path.join(\n            self.config.config_dir, \"archive\", \"example.org\"))\n\n    def test_no_archive(self):\n        archive_dir = os.path.join(self.config.config_dir, \"archive\", \"example.org\")\n        os.rmdir(archive_dir)\n        self._call()\n        assert not os.path.exists(self.config_file.filename)\n        assert not os.path.exists(os.path.join(\n            self.config.live_dir, \"example.org\"))\n        assert not os.path.exists(archive_dir)\n\nclass CertPathForCertNameTest(BaseRenewableCertTest):\n    \"\"\"Test for certbot._internal.storage.cert_path_for_cert_name\"\"\"\n    def setUp(self):\n        super().setUp()\n        self.config_file.write()\n        self._write_out_ex_kinds()\n        self.fullchain = os.path.join(self.config.config_dir, 'live', 'example.org',\n                'fullchain.pem')\n        self.config.cert_path = self.fullchain\n\n    def _call(self, cli_config, certname):\n        from certbot._internal.storage import cert_path_for_cert_name\n        return cert_path_for_cert_name(cli_config, certname)\n\n    def test_simple_cert_name(self):\n        assert self._call(self.config, 'example.org') == self.fullchain\n\n    def test_no_such_cert_name(self):\n        with pytest.raises(errors.CertStorageError):\n            self._call(self.config, 'fake-example.org')\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/tests/util_test.py",
    "content": "\"\"\"Tests for certbot.util.\"\"\"\nimport argparse\nimport errno\nfrom importlib import reload as reload_module\nimport io\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nimport certbot.tests.util as test_util\n\n\nclass EnvNoSnapForExternalCallsTest(unittest.TestCase):\n    \"\"\"Tests for certbot.util.env_no_snap_for_external_calls.\"\"\"\n    @classmethod\n    def _call(cls):\n        from certbot.util import env_no_snap_for_external_calls\n        return env_no_snap_for_external_calls()\n\n    def test_removed(self):\n        original_path = os.environ['PATH']\n        env_copy_dict = os.environ.copy()\n        env_copy_dict['PATH'] = 'RANDOM_NONSENSE_GARBAGE/blah/blah:' + original_path\n        env_copy_dict['SNAP'] = 'RANDOM_NONSENSE_GARBAGE'\n        env_copy_dict['CERTBOT_SNAPPED'] = 'True'\n        with mock.patch('certbot.compat.os.environ.copy', return_value=env_copy_dict):\n            assert self._call()['PATH'] == original_path\n\n    def test_noop(self):\n        env_copy_dict_unmodified = os.environ.copy()\n        env_copy_dict_unmodified['PATH'] = 'RANDOM_NONSENSE_GARBAGE/blah/blah:' \\\n            + env_copy_dict_unmodified['PATH']\n        env_copy_dict = env_copy_dict_unmodified.copy()\n        with mock.patch('certbot.compat.os.environ.copy', return_value=env_copy_dict):\n            # contains neither necessary key\n            env_copy_dict.pop('SNAP', None)\n            env_copy_dict.pop('CERTBOT_SNAPPED', None)\n            assert self._call()['PATH'] == env_copy_dict_unmodified['PATH']\n            # contains only one necessary key\n            env_copy_dict['SNAP'] = 'RANDOM_NONSENSE_GARBAGE'\n            assert self._call()['PATH'] == env_copy_dict_unmodified['PATH']\n            del env_copy_dict['SNAP']\n            env_copy_dict['CERTBOT_SNAPPED'] = 'True'\n            assert self._call()['PATH'] == env_copy_dict_unmodified['PATH']\n\n\nclass RunScriptTest(unittest.TestCase):\n    \"\"\"Tests for certbot.util.run_script.\"\"\"\n    @classmethod\n    def _call(cls, params):\n        from certbot.util import run_script\n        return run_script(params)\n\n    @mock.patch(\"certbot.util.subprocess.run\")\n    def test_default(self, mock_run):\n        \"\"\"These will be changed soon enough with reload.\"\"\"\n        mock_run().returncode = 0\n        mock_run().stdout = \"stdout\"\n        mock_run().stderr = \"stderr\"\n\n        out, err = self._call([\"test\"])\n        assert out == \"stdout\"\n        assert err == \"stderr\"\n\n    @mock.patch(\"certbot.util.subprocess.run\")\n    def test_bad_process(self, mock_run):\n        mock_run.side_effect = OSError\n\n        with pytest.raises(errors.SubprocessError):\n            self._call([\"test\"])\n\n    @mock.patch(\"certbot.util.subprocess.run\")\n    def test_failure(self, mock_run):\n        mock_run().returncode = 1\n\n        with pytest.raises(errors.SubprocessError):\n            self._call([\"test\"])\n\n\nclass ExeExistsTest(unittest.TestCase):\n    \"\"\"Tests for certbot.util.exe_exists.\"\"\"\n\n    @classmethod\n    def _call(cls, exe):\n        from certbot.util import exe_exists\n        return exe_exists(exe)\n\n    def test_exe_exists(self):\n        with mock.patch(\"certbot.util.filesystem.is_executable\", return_value=True):\n            assert self._call(\"/path/to/exe\")\n\n    def test_exe_not_exists(self):\n        with mock.patch(\"certbot.util.filesystem.is_executable\", return_value=False):\n            assert not self._call(\"/path/to/exe\")\n\n\nclass LockDirUntilExit(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.lock_dir_until_exit.\"\"\"\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.util import lock_dir_until_exit\n        return lock_dir_until_exit(*args, **kwargs)\n\n    def setUp(self):\n        super().setUp()\n        # reset global state from other tests\n        import certbot.util\n        reload_module(certbot.util)\n\n    @mock.patch('certbot.util.logger')\n    @mock.patch('certbot.util.atexit_register')\n    def test_it(self, mock_register, mock_logger):\n        subdir = os.path.join(self.tempdir, 'subdir')\n        filesystem.mkdir(subdir)\n        self._call(self.tempdir)\n        self._call(subdir)\n        self._call(subdir)\n\n        assert mock_register.call_count == 1\n        registered_func = mock_register.call_args[0][0]\n\n        from certbot import util\n\n        # Despite lock_dir_until_exit has been called twice to subdir, its lock should have been\n        # added only once. So we expect to have two lock references: for self.tempdir and subdir\n        assert len(util._LOCKS) == 2  # pylint: disable=protected-access\n        registered_func()  # Exception should not be raised\n        # Logically, logger.debug, that would be invoked in case of unlock failure,\n        # should never been called.\n        assert mock_logger.debug.call_count == 0\n\n\nclass SetUpCoreDirTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.make_or_verify_core_dir.\"\"\"\n\n    def _call(self, *args, **kwargs):\n        from certbot.util import set_up_core_dir\n        return set_up_core_dir(*args, **kwargs)\n\n    @mock.patch('certbot.util.lock_dir_until_exit')\n    def test_success(self, mock_lock):\n        new_dir = os.path.join(self.tempdir, 'new')\n        self._call(new_dir, 0o700, False)\n        assert os.path.exists(new_dir)\n        assert mock_lock.call_count == 1\n\n    @mock.patch('certbot.util.make_or_verify_dir')\n    def test_failure(self, mock_make_or_verify):\n        mock_make_or_verify.side_effect = OSError\n        with pytest.raises(errors.Error):\n            self._call(self.tempdir, 0o700, False)\n\n\nclass MakeOrVerifyDirTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.make_or_verify_dir.\n\n    Note that it is not possible to test for a wrong directory owner,\n    as this testing script would have to be run as root.\n\n    \"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.path = os.path.join(self.tempdir, \"foo\")\n        filesystem.mkdir(self.path, 0o600)\n\n    def _call(self, directory, mode):\n        from certbot.util import make_or_verify_dir\n        return make_or_verify_dir(directory, mode, strict=True)\n\n    def test_creates_dir_when_missing(self):\n        path = os.path.join(self.tempdir, \"bar\")\n        self._call(path, 0o650)\n        assert os.path.isdir(path)\n        assert filesystem.check_mode(path, 0o650)\n\n    def test_existing_correct_mode_does_not_fail(self):\n        self._call(self.path, 0o600)\n        assert filesystem.check_mode(self.path, 0o600)\n\n    def test_existing_wrong_mode_fails(self):\n        with pytest.raises(errors.Error):\n            self._call(self.path, 0o400)\n\n    def test_reraises_os_error(self):\n        with mock.patch.object(filesystem, \"makedirs\") as makedirs:\n            makedirs.side_effect = OSError()\n            with pytest.raises(OSError):\n                self._call(\"bar\", 12312312)\n\n\nclass UniqueFileTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.unique_file.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.default_name = os.path.join(self.tempdir, \"foo.txt\")\n\n    def _call(self, mode=0o600):\n        from certbot.util import unique_file\n        return unique_file(self.default_name, mode)\n\n    def test_returns_fd_for_writing(self):\n        fd, name = self._call()\n        fd.write(\"bar\")\n        fd.close()\n        with open(name) as f:\n            assert f.read() == \"bar\"\n\n    def test_right_mode(self):\n        fd1, name1 = self._call(0o700)\n        fd2, name2 = self._call(0o600)\n        assert filesystem.check_mode(name1, 0o700)\n        assert filesystem.check_mode(name2, 0o600)\n        fd1.close()\n        fd2.close()\n\n    def test_default_exists(self):\n        fd1, name1 = self._call()  # create 0000_foo.txt\n        fd2, name2 = self._call()\n        fd3, name3 = self._call()\n\n        assert name1 != name2\n        assert name1 != name3\n        assert name2 != name3\n\n        assert os.path.dirname(name1) == self.tempdir\n        assert os.path.dirname(name2) == self.tempdir\n        assert os.path.dirname(name3) == self.tempdir\n\n        basename1 = os.path.basename(name2)\n        assert basename1.endswith(\"foo.txt\")\n        basename2 = os.path.basename(name2)\n        assert basename2.endswith(\"foo.txt\")\n        basename3 = os.path.basename(name3)\n        assert basename3.endswith(\"foo.txt\")\n\n        fd1.close()\n        fd2.close()\n        fd3.close()\n\n\nclass UniqueLineageNameTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.unique_lineage_name.\"\"\"\n\n    def _call(self, filename, mode=0o777):\n        from certbot.util import unique_lineage_name\n        return unique_lineage_name(self.tempdir, filename, mode)\n\n    def test_basic(self):\n        f, path = self._call(\"wow\")\n        assert isinstance(f, io.TextIOWrapper)\n        assert os.path.join(self.tempdir, \"wow.conf\") == path\n        f.close()\n\n    def test_multiple(self):\n        items = []\n        for _ in range(10):\n            items.append(self._call(\"wow\"))\n        f, name = items[-1]\n        assert isinstance(f, io.TextIOWrapper)\n        assert isinstance(name, str)\n        assert \"wow-0009.conf\" in name\n        for f, _ in items:\n            f.close()\n\n    def test_failure(self):\n        with mock.patch(\"certbot.compat.filesystem.open\", side_effect=OSError(errno.EIO)):\n            with pytest.raises(OSError):\n                self._call(\"wow\")\n\n\nclass SafelyRemoveTest(test_util.TempDirTestCase):\n    \"\"\"Tests for certbot.util.safely_remove.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.path = os.path.join(self.tempdir, \"foo\")\n\n    def _call(self):\n        from certbot.util import safely_remove\n        return safely_remove(self.path)\n\n    def test_exists(self):\n        with open(self.path, \"w\"):\n            pass  # just create the file\n        self._call()\n        assert not os.path.exists(self.path)\n\n    def test_missing(self):\n        self._call()\n        # no error, yay!\n        assert not os.path.exists(self.path)\n\n    def test_other_error_passthrough(self):\n        with mock.patch(\"certbot.util.os.remove\") as mock_remove:\n            mock_remove.side_effect = OSError\n            with pytest.raises(OSError):\n                self._call()\n\n\nclass SafeEmailTest(unittest.TestCase):\n    \"\"\"Test safe_email.\"\"\"\n    @classmethod\n    def _call(cls, addr):\n        from certbot.util import safe_email\n        return safe_email(addr)\n\n    def test_valid_emails(self):\n        addrs = [\n            \"certbot@certbot.org\",\n            \"tbd.ade@gmail.com\",\n            \"abc_def.jdk@hotmail.museum\",\n        ]\n        for addr in addrs:\n            assert self._call(addr), \"%s failed.\" % addr\n\n    def test_invalid_emails(self):\n        addrs = [\n            \"certbot@certbot..org\",\n            \".tbd.ade@gmail.com\",\n            \"~/abc_def.jdk@hotmail.museum\",\n        ]\n        for addr in addrs:\n            assert not self._call(addr), \"%s failed.\" % addr\n\n\nclass AddDeprecatedArgumentTest(unittest.TestCase):\n    \"\"\"Test add_deprecated_argument.\"\"\"\n    def setUp(self):\n        self.parser = argparse.ArgumentParser()\n\n    def _call(self, argument_name, nargs):\n        from certbot.util import add_deprecated_argument\n        add_deprecated_argument(self.parser.add_argument, argument_name, nargs)\n\n    def test_warning_no_arg(self):\n        self._call(\"--old-option\", 0)\n        with mock.patch(\"certbot.util.logger.warning\") as mock_warn:\n            self.parser.parse_args([\"--old-option\"])\n        assert mock_warn.call_count == 1\n        assert \"is deprecated\" in mock_warn.call_args[0][0]\n        assert \"--old-option\" in mock_warn.call_args[0][1]\n\n    def test_warning_with_arg(self):\n        self._call(\"--old-option\", 1)\n        with mock.patch(\"certbot.util.logger.warning\") as mock_warn:\n            self.parser.parse_args([\"--old-option\", \"42\"])\n        assert mock_warn.call_count == 1\n        assert \"is deprecated\" in mock_warn.call_args[0][0]\n        assert \"--old-option\" in mock_warn.call_args[0][1]\n\n    def test_help(self):\n        self._call(\"--old-option\", 2)\n        stdout = io.StringIO()\n        with mock.patch(\"sys.stdout\", new=stdout):\n            try:\n                self.parser.parse_args([\"-h\"])\n            except SystemExit:\n                pass\n        assert \"--old-option\" not in stdout.getvalue()\n\n    def test_set_constant(self):\n        \"\"\"Test when ACTION_TYPES_THAT_DONT_NEED_A_VALUE is a set.\n\n        This variable is a set in configargparse versions < 0.12.0.\n\n        \"\"\"\n        self._test_constant_common(set)\n\n    def test_tuple_constant(self):\n        \"\"\"Test when ACTION_TYPES_THAT_DONT_NEED_A_VALUE is a tuple.\n\n        This variable is a tuple in configargparse versions >= 0.12.0.\n\n        \"\"\"\n        self._test_constant_common(tuple)\n\n    def _test_constant_common(self, typ):\n        with mock.patch(\"certbot.util.configargparse\") as mock_configargparse:\n            mock_configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE = typ()\n            self._call(\"--old-option\", 1)\n            self._call(\"--old-option2\", 2)\n        assert len(mock_configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE) == 1\n\n\nclass EnforceLeValidity(unittest.TestCase):\n    \"\"\"Test enforce_le_validity.\"\"\"\n    def _call(self, domain):\n        from certbot.util import enforce_le_validity\n        return enforce_le_validity(domain)\n\n    def test_sanity(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"..\")\n\n    def test_invalid_chars(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"hello_world.example.com\")\n\n    def test_leading_hyphen(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"-a.example.com\")\n\n    def test_trailing_hyphen(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"a-.example.com\")\n\n    def test_one_label(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"com\")\n\n    def test_valid_domain(self):\n        assert self._call(u\"example.com\") == u\"example.com\"\n\n    def test_input_with_scheme(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"http://example.com\")\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"https://example.com\")\n\n    def test_valid_input_with_scheme_name(self):\n        assert self._call(u\"http.example.com\") == u\"http.example.com\"\n\n\nclass EnforceDomainSanityTest(unittest.TestCase):\n    \"\"\"Test enforce_domain_sanity.\"\"\"\n\n    def _call(self, domain):\n        from certbot.util import enforce_domain_sanity\n        return enforce_domain_sanity(domain)\n\n    def test_nonascii_str(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"eichh\\u00f6rnchen.example.com\".encode(\"utf-8\"))\n\n    def test_nonascii_unicode(self):\n        with pytest.raises(errors.ConfigurationError):\n            self._call(u\"eichh\\u00f6rnchen.example.com\")\n\n    def test_too_long(self):\n        long_domain = u\"a\"*256\n        with pytest.raises(errors.ConfigurationError):\n            self._call(long_domain)\n\n    def test_not_too_long(self):\n        not_too_long_domain = u\"{0}.{1}.{2}.{3}\".format(\"a\"*63, \"b\"*63, \"c\"*63, \"d\"*63)\n        self._call(not_too_long_domain)\n\n    def test_empty_label(self):\n        empty_label_domain = u\"fizz..example.com\"\n        with pytest.raises(errors.ConfigurationError):\n            self._call(empty_label_domain)\n\n    def test_empty_trailing_label(self):\n        empty_trailing_label_domain = u\"example.com..\"\n        with pytest.raises(errors.ConfigurationError):\n            self._call(empty_trailing_label_domain)\n\n    def test_long_label_1(self):\n        long_label_domain = u\"a\"*64\n        with pytest.raises(errors.ConfigurationError):\n            self._call(long_label_domain)\n\n    def test_long_label_2(self):\n        long_label_domain = u\"{0}.{1}.com\".format(u\"a\"*64, u\"b\"*63)\n        with pytest.raises(errors.ConfigurationError):\n            self._call(long_label_domain)\n\n    def test_not_long_label(self):\n        not_too_long_label_domain = u\"{0}.{1}.com\".format(u\"a\"*63, u\"b\"*63)\n        self._call(not_too_long_label_domain)\n\n    def test_empty_domain(self):\n        empty_domain = u\"\"\n        with pytest.raises(errors.ConfigurationError):\n            self._call(empty_domain)\n\n    def test_punycode_ok(self):\n        # Punycode is now legal, so no longer an error; instead check\n        # that it's _not_ an error (at the initial sanity check stage)\n        self._call('this.is.xn--ls8h.tld')\n\n\nclass IsWildcardDomainTest(unittest.TestCase):\n    \"\"\"Tests for is_wildcard_domain.\"\"\"\n\n    def setUp(self):\n        self.wildcard = u\"*.example.org\"\n        self.no_wildcard = u\"example.org\"\n\n    def _call(self, domain):\n        from certbot.util import is_wildcard_domain\n        return is_wildcard_domain(domain)\n\n    def test_no_wildcard(self):\n        assert not self._call(self.no_wildcard)\n        assert not self._call(self.no_wildcard.encode())\n\n    def test_wildcard(self):\n        assert self._call(self.wildcard)\n        assert self._call(self.wildcard.encode())\n\n\nclass OsInfoTest(unittest.TestCase):\n    \"\"\"Test OS / distribution detection\"\"\"\n\n    @mock.patch(\"certbot.util.distro\")\n    @unittest.skipUnless(sys.platform.startswith(\"linux\"), \"requires Linux\")\n    def test_systemd_os_release_like(self, m_distro):\n        import certbot.util as cbutil\n        m_distro.like.return_value = \"first debian third\"\n        id_likes = cbutil.get_systemd_os_like()\n        assert len(id_likes) == 3\n        assert \"debian\" in id_likes\n\n    @mock.patch(\"certbot.util.distro\")\n    @unittest.skipUnless(sys.platform.startswith(\"linux\"), \"requires Linux\")\n    def test_get_os_info_ua(self, m_distro):\n        import certbot.util as cbutil\n        with mock.patch('platform.system_alias',\n                        return_value=('linux', '42', '42')):\n            m_distro.version.return_value = \"1.0\"\n            # empty value on first call for fallback to \"get_python_os_info\" in get_os_info_ua\n            m_distro.name.side_effect = [\"\", \"something\", \"something\"]\n            assert cbutil.get_os_info_ua() == \\\n                            \" \".join(cbutil.get_python_os_info(pretty=True))\n\n        m_distro.name.side_effect = [\"whatever\"]\n        assert cbutil.get_os_info_ua() == \"whatever\"\n\n    @mock.patch(\"certbot.util.distro\")\n    @unittest.skipUnless(sys.platform.startswith(\"linux\"), \"requires Linux\")\n    def test_get_os_info(self, m_distro):\n        import certbot.util as cbutil\n        with mock.patch(\"platform.system\") as mock_platform:\n            m_distro.id.return_value = \"name\"\n            m_distro.version.return_value = \"version\"\n            mock_platform.return_value = \"linux\"\n            assert cbutil.get_os_info() == (\"name\", \"version\")\n\n            m_distro.id.return_value = \"something\"\n            m_distro.version.return_value = \"else\"\n            assert cbutil.get_os_info() == (\"something\", \"else\")\n\n    def test_non_systemd_os_info(self):\n        import certbot.util as cbutil\n        with mock.patch('certbot.util._USE_DISTRO', False):\n            with mock.patch('platform.system_alias',\n                            return_value=('NonSystemD', '42', '42')):\n                assert cbutil.get_python_os_info()[0] == 'nonsystemd'\n\n            with mock.patch('platform.system_alias',\n                            return_value=('darwin', '', '')):\n                with mock.patch(\"subprocess.run\") as run_mock:\n                    run_mock().stdout = '42.42.42'\n                    assert cbutil.get_python_os_info()[0] == 'darwin'\n                    assert cbutil.get_python_os_info()[1] == '42.42.42'\n\n            with mock.patch('platform.system_alias',\n                            return_value=('freebsd', '9.3-RC3-p1', '')):\n                assert cbutil.get_python_os_info() == (\"freebsd\", \"9\")\n\n            with mock.patch('platform.system_alias',\n                            return_value=('windows', '', '')):\n                with mock.patch('platform.win32_ver',\n                                return_value=('4242', '95', '2', '')):\n                    assert cbutil.get_python_os_info() == \\\n                                    (\"windows\", \"95\")\n\n    @mock.patch(\"certbot.util.distro\")\n    @unittest.skipUnless(sys.platform.startswith(\"linux\"), \"requires Linux\")\n    def test_python_os_info_notfound(self, m_distro):\n        import certbot.util as cbutil\n        m_distro.id.return_value = \"\"\n        m_distro.version.return_value = \"\"\n        assert cbutil.get_python_os_info()[0] == \"linux\"\n\n    @mock.patch(\"certbot.util.distro\")\n    @unittest.skipUnless(sys.platform.startswith(\"linux\"), \"requires Linux\")\n    def test_python_os_info_custom(self, m_distro):\n        import certbot.util as cbutil\n        m_distro.id.return_value = \"testdist\"\n        m_distro.version.return_value = \"42\"\n        assert cbutil.get_python_os_info() == (\"testdist\", \"42\")\n\n\nclass AtexitRegisterTest(unittest.TestCase):\n    \"\"\"Tests for certbot.util.atexit_register.\"\"\"\n    def setUp(self):\n        self.func = mock.MagicMock()\n        self.args = ('hi',)\n        self.kwargs = {'answer': 42}\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.util import atexit_register\n        return atexit_register(*args, **kwargs)\n\n    def test_called(self):\n        self._test_common(os.getpid())\n        self.func.assert_called_with(*self.args, **self.kwargs)\n\n    def test_not_called(self):\n        self._test_common(initial_pid=-1)\n        assert self.func.called is False\n\n    def _test_common(self, initial_pid):\n        with mock.patch('certbot.util._INITIAL_PID', initial_pid):\n            with mock.patch('certbot.util.atexit') as mock_atexit:\n                self._call(self.func, *self.args, **self.kwargs)\n\n            # _INITIAL_PID must be mocked when calling atexit_func\n            assert mock_atexit.register.called\n            args, kwargs = mock_atexit.register.call_args\n            atexit_func = args[0]\n            atexit_func(*args[1:], **kwargs)\n\n\nclass LooseVersionTest(unittest.TestCase):\n    \"\"\"Test for certbot.util.LooseVersion.\n\n    These tests are based on the original tests for\n    distutils.version.LooseVersion at\n    https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/tests/test_version.py#L58-L81.\n\n    \"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.util import LooseVersion\n        return LooseVersion(*args, **kwargs)\n\n    def test_less_than(self):\n        comparisons = (('1.5.1', '1.5.2b2'),\n            ('3.4j', '1996.07.12'),\n            ('2g6', '11g'),\n            ('0.960923', '2.2beta29'),\n            ('1.13++', '5.5.kw'),\n            ('2.0', '2.0.1'),\n            ('a', 'b'))\n        for v1, v2 in comparisons:\n            assert self._call(v1).try_risky_comparison(self._call(v2)) == -1\n\n    def test_equal(self):\n        comparisons = (('8.02', '8.02'),\n            ('1a', '1a'),\n            ('2', '2.0.0'),\n            ('2.0', '2.0.0'))\n        for v1, v2 in comparisons:\n            assert self._call(v1).try_risky_comparison(self._call(v2)) == 0\n\n    def test_greater_than(self):\n        comparisons = (('161', '3.10a'),\n            ('3.2.pl0', '3.1.1.6'))\n        for v1, v2 in comparisons:\n            assert self._call(v1).try_risky_comparison(self._call(v2)) == 1\n\n    def test_incomparible(self):\n        comparisons = (('bookworm/sid', '9'),\n            ('1a', '1.0'))\n        for v1, v2 in comparisons:\n            with pytest.raises(ValueError):\n                assert self._call(v1).try_risky_comparison(self._call(v2))\n\n\nclass ParseLooseVersionTest(unittest.TestCase):\n    \"\"\"Test for certbot.util.parse_loose_version.\n\n    These tests are based on the original tests for\n    distutils.version.LooseVersion at\n    https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/tests/test_version.py#L58-L81.\n\n    \"\"\"\n\n    @classmethod\n    def _call(cls, *args, **kwargs):\n        from certbot.util import parse_loose_version\n        return parse_loose_version(*args, **kwargs)\n\n    def test_less_than(self):\n        comparisons = (('1.5.1', '1.5.2b2'),\n            ('3.4j', '1996.07.12'),\n            ('2g6', '11g'),\n            ('0.960923', '2.2beta29'),\n            ('1.13++', '5.5.kw'))\n        for v1, v2 in comparisons:\n            assert self._call(v1) < self._call(v2)\n\n    def test_equal(self):\n        assert self._call('8.02') == self._call('8.02')\n\n    def test_greater_than(self):\n        comparisons = (('161', '3.10a'),\n            ('3.2.pl0', '3.1.1.6'))\n        for v1, v2 in comparisons:\n            assert self._call(v1) > self._call(v2)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot/src/certbot/_internal/updater.py",
    "content": "\"\"\"Updaters run at renewal\"\"\"\nimport logging\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot._internal import storage\nfrom certbot._internal.plugins import disco as plugin_disco\nfrom certbot._internal.plugins import selection as plug_sel\nfrom certbot.plugins import enhancements\n\nlogger = logging.getLogger(__name__)\n\n\ndef run_generic_updaters(config: configuration.NamespaceConfig, lineage: storage.RenewableCert,\n                         plugins: plugin_disco.PluginsRegistry) -> None:\n    \"\"\"Run updaters that the plugin supports\n\n    :param config: Configuration object\n    :type config: certbot.configuration.NamespaceConfig\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :param plugins: List of plugins\n    :type plugins: certbot._internal.plugins.disco.PluginsRegistry\n\n    :returns: `None`\n    :rtype: None\n    \"\"\"\n    if config.dry_run:\n        logger.debug(\"Skipping updaters in dry-run mode.\")\n        return\n    try:\n        installer = plug_sel.get_unprepared_installer(config, plugins)\n    except errors.Error as e:\n        logger.error(\"Could not choose appropriate plugin for updaters: %s\", e)\n        return\n    if installer:\n        _run_updaters(lineage, installer, config)\n        _run_enhancement_updaters(lineage, installer, config)\n\n\ndef run_renewal_deployer(config: configuration.NamespaceConfig, lineage: storage.RenewableCert,\n                         installer: interfaces.Installer) -> None:\n    \"\"\"Helper function to run deployer interface method if supported by the used\n    installer plugin.\n\n    :param config: Configuration object\n    :type config: certbot.configuration.NamespaceConfig\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :returns: `None`\n    :rtype: None\n    \"\"\"\n    if config.dry_run:\n        logger.debug(\"Skipping renewal deployer in dry-run mode.\")\n        return\n\n    if not config.disable_renew_updates and isinstance(installer,\n                                                       interfaces.RenewDeployer):\n        installer.renew_deploy(lineage)\n    _run_enhancement_deployers(lineage, installer, config)\n\n\ndef _run_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer,\n                  config: configuration.NamespaceConfig) -> None:\n    \"\"\"Helper function to run the updater interface methods if supported by the\n    used installer plugin.\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :returns: `None`\n    :rtype: None\n    \"\"\"\n    if not config.disable_renew_updates:\n        if isinstance(installer, interfaces.GenericUpdater):\n            installer.generic_updates(lineage)\n\n\ndef _run_enhancement_updaters(lineage: storage.RenewableCert, installer: interfaces.Installer,\n                              config: configuration.NamespaceConfig) -> None:\n    \"\"\"Iterates through known enhancement interfaces. If the installer implements\n    an enhancement interface and the enhance interface has an updater method, the\n    updater method gets run.\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :param config: Configuration object\n    :type config: certbot.configuration.NamespaceConfig\n    \"\"\"\n\n    if config.disable_renew_updates:\n        return\n    for enh in enhancements._INDEX:  # pylint: disable=protected-access\n        if isinstance(installer, enh[\"class\"]) and enh[\"updater_function\"]:\n            getattr(installer, enh[\"updater_function\"])(lineage)\n\n\ndef _run_enhancement_deployers(lineage: storage.RenewableCert, installer: interfaces.Installer,\n                               config: configuration.NamespaceConfig) -> None:\n    \"\"\"Iterates through known enhancement interfaces. If the installer implements\n    an enhancement interface and the enhance interface has an deployer method, the\n    deployer method gets run.\n\n    :param lineage: Certificate lineage object\n    :type lineage: storage.RenewableCert\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :param config: Configuration object\n    :type config: certbot.configuration.NamespaceConfig\n    \"\"\"\n\n    if config.disable_renew_updates:\n        return\n    for enh in enhancements._INDEX:  # pylint: disable=protected-access\n        if isinstance(installer, enh[\"class\"]) and enh[\"deployer_function\"]:\n            getattr(installer, enh[\"deployer_function\"])(lineage)\n"
  },
  {
    "path": "certbot/src/certbot/achallenges.py",
    "content": "\"\"\"Client annotated ACME challenges.\n\nPlease use names such as ``achall`` to distinguish from variables \"of type\"\n:class:`acme.challenges.Challenge` (denoted by ``chall``)\nand :class:`.ChallengeBody` (denoted by ``challb``)::\n\n  from acme import challenges\n  from acme import messages\n  from certbot import achallenges\n\n  chall = challenges.DNS(token='foo')\n  challb = messages.ChallengeBody(chall=chall)\n  achall = achallenges.DNS(chall=challb, domain='example.com')\n\nNote, that all annotated challenges act as a proxy objects::\n\n  achall.token == challb.token\n\n\"\"\"\nimport logging\nfrom typing import Any\nimport warnings\n\nimport josepy as jose\n\nfrom acme import challenges, messages\nfrom acme.challenges import Challenge\n\nfrom certbot import errors\n\nlogger = logging.getLogger(__name__)\n\n\nclass AnnotatedChallenge(jose.ImmutableMap):\n    \"\"\"Client annotated challenge.\n\n    Wraps around server provided challenge and annotates with data\n    useful for the client.\n\n    :ivar ~.challb: Wrapped `~.ChallengeBody`.\n\n    \"\"\"\n    __slots__ = ('challb',)\n    _acme_type: type[Challenge] = NotImplemented\n\n    def __getattr__(self, name: str) -> Any:\n        return getattr(self.challb, name)\n\n    def __getattribute__(self, name: str) -> Any:\n        if name == 'domain':\n            warnings.warn(\"The domain attribute is deprecated and will be removed in \"\n                        \"an upcoming release. Access the AnnotatedChallenge.identifier.value \"\n                        \"attribute instead\",\n                        DeprecationWarning)\n        return super().__getattribute__(name)\n\n    def __hash__(self) -> int:\n        with warnings.catch_warnings():\n            warnings.filterwarnings('ignore', 'the domain attribute is deprecated')\n            return super().__hash__()\n\n    def __eq__(self, other: Any) -> bool:\n        with warnings.catch_warnings():\n            warnings.filterwarnings('ignore', 'the domain attribute is deprecated')\n            return super().__eq__(other)\n\n    def __init__(self, **kwargs: Any) -> None:\n        if 'domain' in kwargs:\n            if 'identifier' in kwargs:\n                raise errors.Error(\"AnnotatedChallenge takes either domain or identifier, not both\")\n            warnings.warn(\"The domain attribute is deprecated and will be removed in \"\n                          \"an upcoming release. Replace domain=<domain> with \"\n                          \"identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, \"\n                          \"value=<domain>)\",\n                          DeprecationWarning)\n        if 'identifier' not in kwargs:\n            kwargs['identifier'] = messages.Identifier(\n                typ=messages.IDENTIFIER_FQDN, value=kwargs['domain'])\n        if 'domain' not in kwargs:\n            if kwargs['identifier'].typ == messages.IDENTIFIER_FQDN:\n                kwargs['domain'] = kwargs['identifier'].value\n            else:\n                kwargs['domain'] = None\n        super().__init__(**kwargs)\n\n\nclass KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge):\n    \"\"\"Client annotated `KeyAuthorizationChallenge` challenge.\"\"\"\n    __slots__ = ('challb', 'domain', 'account_key', 'identifier') # pylint: disable=redefined-slots-in-subclass\n\n    def response_and_validation(self, *args: Any, **kwargs: Any\n        ) -> tuple['challenges.KeyAuthorizationChallengeResponse', Any]:\n        \"\"\"Generate response and validation.\"\"\"\n        return self.challb.chall.response_and_validation(\n            self.account_key, *args, **kwargs)\n\n\nclass DNS(AnnotatedChallenge):\n    \"\"\"Client annotated \"dns\" ACME challenge.\"\"\"\n    __slots__ = ('challb', 'domain', 'identifier') # pylint: disable=redefined-slots-in-subclass\n    acme_type = challenges.DNS\n\nclass Other(AnnotatedChallenge):\n    \"\"\"Client annotated ACME challenge of an unknown type.\"\"\"\n    __slots__ = ('challb', 'domain', 'identifier') # pylint: disable=redefined-slots-in-subclass\n    acme_type = challenges.Challenge\n"
  },
  {
    "path": "certbot/src/certbot/compat/__init__.py",
    "content": "\"\"\"\nCompatibility layer to run certbot both on Linux and Windows.\n\nThis package contains all logic that needs to be implemented specifically for Linux and for Windows.\nThen the rest of certbot code relies on this module to be platform agnostic.\n\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/compat/_path.py",
    "content": "\"\"\"\nThis compat module wraps os.path to forbid some functions.\n\nisort:skip_file\n\"\"\"\n\n# NB: Each function defined in compat._path is marked with \"type: ignore\" to avoid mypy\n#     to complain that a function is redefined (because we imported if first from os.path).\n\n# pylint: disable=function-redefined\nfrom __future__ import absolute_import\n\n# First round of wrapping: we import statically all public attributes exposed by the os.path\n# module. This allows in particular to have pylint, mypy, IDEs be aware that most of os.path\n# members are available in certbot.compat.path.\nfrom os.path import *  # pylint: disable=wildcard-import,unused-wildcard-import,os-module-forbidden # noqa: F403\n\n# Second round of wrapping: we import dynamically all attributes from the os.path module that have\n# not yet been imported by the first round (static star import).\nimport os.path as std_os_path  # pylint: disable=os-module-forbidden\nimport sys as std_sys\n\nourselves = std_sys.modules[__name__]\nfor attribute in dir(std_os_path):\n    # Check if the attribute does not already exist in our module. It could be internal attributes\n    # of the module (__name__, __doc__), or attributes from standard os.path already imported with\n    # `from os.path import *`.\n    if not hasattr(ourselves, attribute):\n        setattr(ourselves, attribute, getattr(std_os_path, attribute))\n\n# Clean all remaining importables that are not from the core os.path module.\ndel ourselves, std_os_path, std_sys\n\n\n# Function os.path.realpath is broken on some versions of Python for Windows.\ndef realpath(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.path.realpath() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.path.realpath() is forbidden. '\n                       'Use certbot.compat.filesystem.realpath() instead.')\n"
  },
  {
    "path": "certbot/src/certbot/compat/filesystem.py",
    "content": "\"\"\"Compat module to handle files security on Windows and Linux\"\"\"\nfrom __future__ import absolute_import\n\nfrom contextlib import contextmanager\nimport errno\nimport os  # pylint: disable=os-module-forbidden\nimport stat\nfrom typing import Any\nfrom typing import Generator\nfrom typing import Optional\n\ntry:\n    import ntsecuritycon\n    import pywintypes\n    import win32api\n    import win32con\n    import win32file\n    import win32security\n    import winerror\nexcept ImportError:\n    POSIX_MODE = True\nelse:\n    POSIX_MODE = False\n\n\n# Windows umask implementation, since Windows does not have a concept of umask by default.\n# We choose 022 as initial value since it is the default one on most Linux distributions, and\n# it is a decent choice to not have write permissions for group owner and everybody by default.\n# We use a class here to avoid needing to define a global variable, and the potential mistakes\n# that could happen with this kind of pattern.\nclass _WindowsUmask:\n    \"\"\"Store the current umask to apply on Windows\"\"\"\n    def __init__(self) -> None:\n        self.mask = 0o022\n\n\n_WINDOWS_UMASK = _WindowsUmask()\n\n\ndef chmod(file_path: str, mode: int) -> None:\n    \"\"\"\n    Apply a POSIX mode on given file_path:\n\n      - for Linux, the POSIX mode will be directly applied using chmod,\n      - for Windows, the POSIX mode will be translated into a Windows DACL that make sense for\n        Certbot context, and applied to the file using kernel calls.\n\n    The definition of the Windows DACL that correspond to a POSIX mode, in the context of Certbot,\n    is explained at https://github.com/certbot/certbot/issues/6356 and is implemented by the\n    method `_generate_windows_flags()`.\n\n    :param str file_path: Path of the file\n    :param int mode: POSIX mode to apply\n    \"\"\"\n    if POSIX_MODE:\n        os.chmod(file_path, mode)\n    else:\n        _apply_win_mode(file_path, mode)\n\n\ndef umask(mask: int) -> int:\n    \"\"\"\n    Set the current numeric umask and return the previous umask. On Linux, the built-in umask\n    method is used. On Windows, our Certbot-side implementation is used.\n\n    :param int mask: The user file-creation mode mask to apply.\n    :rtype: int\n    :return: The previous umask value.\n    \"\"\"\n    if POSIX_MODE:\n        return os.umask(mask)\n\n    previous_umask = _WINDOWS_UMASK.mask\n    _WINDOWS_UMASK.mask = mask\n    return previous_umask\n\n\n@contextmanager\ndef temp_umask(mask: int) -> Generator[None, None, None]:\n    \"\"\"\n    Apply a umask temporarily, meant to be used in a `with` block. Uses the Certbot\n    implementation of umask.\n\n    :param int mask: The user file-creation mode mask to apply temporarily\n    \"\"\"\n    old_umask: Optional[int] = None\n    try:\n        old_umask = umask(mask)\n        yield None\n    finally:\n        if old_umask is not None:\n            umask(old_umask)\n\n\n# One could ask why there is no copy_ownership() function, or even a reimplementation\n# of os.chown() that would modify the ownership of file without touching the mode itself.\n# This is because on Windows, it would require recalculating the existing DACL against\n# the new owner, since the DACL is composed of ACEs that targets a specific user, not dynamically\n# the current owner of a file. This action would be necessary to keep consistency between\n# the POSIX mode applied to the file and the current owner of this file.\n# Since copying and editing arbitrary DACL is very difficult, and since we actually know\n# the mode to apply at the time the owner of a file should change, it is easier to just\n# change the owner, then reapply the known mode, as copy_ownership_and_apply_mode() does.\ndef copy_ownership_and_apply_mode(src: str, dst: str, mode: int,\n                                  copy_user: bool, copy_group: bool) -> None:\n    \"\"\"\n    Copy ownership (user and optionally group on Linux) from the source to the\n    destination, then apply given mode in compatible way for Linux and Windows.\n    This replaces the os.chown command.\n\n    :param str src: Path of the source file\n    :param str dst: Path of the destination file\n    :param int mode: Permission mode to apply on the destination file\n    :param bool copy_user: Copy user if `True`\n    :param bool copy_group: Copy group if `True` on Linux (has no effect on Windows)\n    \"\"\"\n    if POSIX_MODE:\n        stats = os.stat(src)\n        user_id = stats.st_uid if copy_user else -1\n        group_id = stats.st_gid if copy_group else -1\n        # On Windows, os.chown does not exist. This is checked through POSIX_MODE value,\n        # but MyPy/PyLint does not know it and raises an error here on Windows.\n        # We disable specifically the check to fix the issue.\n        os.chown(dst, user_id, group_id)\n    elif copy_user:\n        # There is no group handling in Windows\n        _copy_win_ownership(src, dst)\n\n    chmod(dst, mode)\n\n\n# Quite similar to copy_ownership_and_apply_mode, but this time the DACL is copied from\n# the source file on Windows. The DACL stays consistent with the dynamic rights of the\n# equivalent POSIX mode, because ownership and mode are copied altogether on the destination\n# file, so no recomputing of the DACL against the new owner is needed, as it would be\n# for a copy_ownership alone method.\ndef copy_ownership_and_mode(src: str, dst: str,\n                            copy_user: bool = True, copy_group: bool = True) -> None:\n    \"\"\"\n    Copy ownership (user and optionally group on Linux) and mode/DACL\n    from the source to the destination.\n\n    :param str src: Path of the source file\n    :param str dst: Path of the destination file\n    :param bool copy_user: Copy user if `True`\n    :param bool copy_group: Copy group if `True` on Linux (has no effect on Windows)\n    \"\"\"\n    if POSIX_MODE:\n        # On Linux, we just delegate to chown and chmod.\n        stats = os.stat(src)\n        user_id = stats.st_uid if copy_user else -1\n        group_id = stats.st_gid if copy_group else -1\n        os.chown(dst, user_id, group_id)\n        chmod(dst, stats.st_mode)\n    else:\n        if copy_user:\n            # There is no group handling in Windows\n            _copy_win_ownership(src, dst)\n        _copy_win_mode(src, dst)\n\n\ndef check_mode(file_path: str, mode: int) -> bool:\n    \"\"\"\n    Check if the given mode matches the permissions of the given file.\n    On Linux, will make a direct comparison, on Windows, mode will be compared against\n    the security model.\n\n    :param str file_path: Path of the file\n    :param int mode: POSIX mode to test\n    :rtype: bool\n    :return: True if the POSIX mode matches the file permissions\n    \"\"\"\n    if POSIX_MODE:\n        return stat.S_IMODE(os.stat(file_path).st_mode) == mode\n\n    return _check_win_mode(file_path, mode)\n\n\ndef check_owner(file_path: str) -> bool:\n    \"\"\"\n    Check if given file is owned by current user.\n\n    :param str file_path: File path to check\n    :rtype: bool\n    :return: True if given file is owned by current user, False otherwise.\n    \"\"\"\n    if POSIX_MODE:\n        return os.stat(file_path).st_uid == os.getuid()\n\n    # Get owner sid of the file\n    security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION)\n    user = security.GetSecurityDescriptorOwner()\n\n    # Compare sids\n    return _get_current_user() == user\n\n\ndef check_permissions(file_path: str, mode: int) -> bool:\n    \"\"\"\n    Check if given file has the given mode and is owned by current user.\n\n    :param str file_path: File path to check\n    :param int mode: POSIX mode to check\n    :rtype: bool\n    :return: True if file has correct mode and owner, False otherwise.\n    \"\"\"\n    return check_owner(file_path) and check_mode(file_path, mode)\n\n\ndef open(file_path: str, flags: int, mode: int = 0o777) -> int:  # pylint: disable=redefined-builtin\n    \"\"\"\n    Wrapper of original os.open function, that will ensure on Windows that given mode\n    is correctly applied.\n\n    :param str file_path: The file path to open\n    :param int flags: Flags to apply on file while opened\n    :param int mode: POSIX mode to apply on file when opened,\n        Python defaults will be applied if ``None``\n    :returns: the file descriptor to the opened file\n    :rtype: int\n    :raise: OSError(errno.EEXIST) if the file already exists and os.O_CREAT & os.O_EXCL are set,\n            OSError(errno.EACCES) on Windows if the file already exists and is a directory, and\n            os.O_CREAT is set.\n    \"\"\"\n    if POSIX_MODE:\n        # On Linux, invoke os.open directly.\n        return os.open(file_path, flags, mode)\n\n    # Windows: handle creation of the file atomically with proper permissions.\n    if flags & os.O_CREAT:\n        # If os.O_EXCL is set, we will use the \"CREATE_NEW\", that will raise an exception if\n        # file exists, matching the API contract of this bit flag. Otherwise, we use\n        # \"CREATE_ALWAYS\" that will always create the file whether it exists or not.\n        disposition = win32con.CREATE_NEW if flags & os.O_EXCL else win32con.CREATE_ALWAYS\n\n        attributes = win32security.SECURITY_ATTRIBUTES()\n        security = attributes.SECURITY_DESCRIPTOR\n        user = _get_current_user()\n        dacl = _generate_dacl(user, mode, _WINDOWS_UMASK.mask)\n        # We set second parameter to 0 (`False`) to say that this security descriptor is\n        # NOT constructed from a default mechanism, but is explicitly set by the user.\n        # See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptorowner  # pylint: disable=line-too-long\n        security.SetSecurityDescriptorOwner(user, 0)\n        # We set first parameter to 1 (`True`) to say that this security descriptor contains\n        # a DACL. Otherwise second and third parameters are ignored.\n        # We set third parameter to 0 (`False`) to say that this security descriptor is\n        # NOT constructed from a default mechanism, but is explicitly set by the user.\n        # See https://docs.microsoft.com/en-us/windows/desktop/api/securitybaseapi/nf-securitybaseapi-setsecuritydescriptordacl  # pylint: disable=line-too-long\n        security.SetSecurityDescriptorDacl(1, dacl, 0)\n\n        handle = None\n        try:\n            handle = win32file.CreateFile(file_path, win32file.GENERIC_READ,\n                                          win32file.FILE_SHARE_READ & win32file.FILE_SHARE_WRITE,\n                                          attributes, disposition, 0, None)\n        except pywintypes.error as err:\n            # Handle native windows errors into python errors to be consistent with the API\n            # of os.open in the situation of a file already existing or locked.\n            if err.winerror == winerror.ERROR_FILE_EXISTS:\n                raise OSError(errno.EEXIST, err.strerror)\n            if err.winerror == winerror.ERROR_SHARING_VIOLATION:\n                raise OSError(errno.EACCES, err.strerror)\n            raise err\n        finally:\n            if handle:\n                handle.Close()\n\n        # At this point, the file that did not exist has been created with proper permissions,\n        # so os.O_CREAT and os.O_EXCL are not needed anymore. We remove them from the flags to\n        # avoid a FileExists exception before calling os.open.\n        return os.open(file_path, flags ^ os.O_CREAT ^ os.O_EXCL)\n\n    # Windows: general case, we call os.open, let exceptions be thrown, then chmod if all is fine.\n    fd = os.open(file_path, flags)\n    chmod(file_path, mode)\n    return fd\n\n\ndef makedirs(file_path: str, mode: int = 0o777) -> None:\n    \"\"\"\n    Rewrite of original os.makedirs function, that will ensure on Windows that given mode\n    is correctly applied.\n\n    :param str file_path: The file path to open\n    :param int mode: POSIX mode to apply on leaf directory when created, Python defaults\n                     will be applied if ``None``\n    \"\"\"\n    current_umask = umask(0)\n    try:\n        # Since Python 3.7, os.makedirs does not set the given mode to the intermediate\n        # directories that could be created in the process. To keep things safe and consistent\n        # on all Python versions, we set the umask accordingly to have all directories\n        # (intermediate and leaf) created with the given mode.\n        umask(current_umask | 0o777 ^ mode)\n\n        if POSIX_MODE:\n            return os.makedirs(file_path, mode)\n\n        orig_mkdir_fn = os.mkdir\n        try:\n            # As we know that os.mkdir is called internally by os.makedirs, we will swap the\n            # function in os module for the time of makedirs execution on Windows.\n            os.mkdir = mkdir  # type: ignore\n            return os.makedirs(file_path, mode)\n        finally:\n            os.mkdir = orig_mkdir_fn\n    finally:\n        umask(current_umask)\n\n\ndef mkdir(file_path: str, mode: int = 0o777) -> None:\n    \"\"\"\n    Rewrite of original os.mkdir function, that will ensure on Windows that given mode\n    is correctly applied.\n\n    :param str file_path: The file path to open\n    :param int mode: POSIX mode to apply on directory when created, Python defaults\n                     will be applied if ``None``\n    \"\"\"\n    if POSIX_MODE:\n        return os.mkdir(file_path, mode)\n\n    attributes = win32security.SECURITY_ATTRIBUTES()\n    security = attributes.SECURITY_DESCRIPTOR\n    user = _get_current_user()\n    dacl = _generate_dacl(user, mode, _WINDOWS_UMASK.mask)\n    security.SetSecurityDescriptorOwner(user, False)\n    security.SetSecurityDescriptorDacl(1, dacl, 0)\n\n    try:\n        win32file.CreateDirectory(file_path, attributes)\n    except pywintypes.error as err:\n        # Handle native windows error into python error to be consistent with the API\n        # of os.mkdir in the situation of a directory already existing.\n        if err.winerror == winerror.ERROR_ALREADY_EXISTS:\n            raise OSError(errno.EEXIST, err.strerror, file_path, err.winerror)\n        raise err\n\n    return None\n\n\ndef replace(src: str, dst: str) -> None:\n    \"\"\"\n    Rename a file to a destination path and handles situations where the destination exists.\n\n    :param str src: The current file path.\n    :param str dst: The new file path.\n    \"\"\"\n    if hasattr(os, 'replace'):\n        # Use replace if possible. Since we don't support Python 2 on Windows\n        # and os.replace() was added in Python 3.3, we can assume that\n        # os.replace() is always available on Windows.\n        getattr(os, 'replace')(src, dst)\n    else:\n        # Otherwise, use os.rename() that behaves like os.replace() on Linux.\n        os.rename(src, dst)\n\n\ndef realpath(file_path: str) -> str:\n    \"\"\"\n    Find the real path for the given path. This method resolves symlinks, including\n    recursive symlinks, and is protected against symlinks that creates an infinite loop.\n\n    :param str file_path: The path to resolve\n    :returns: The real path for the given path\n    :rtype: str\n    \"\"\"\n    original_path = file_path\n\n    # os.path.realpath also resolves symlinks\n    path = os.path.realpath(file_path)\n    if os.path.islink(path):\n        # If path returned by realpath is still a link, it means that it failed to\n        # resolve the symlink because of a loop.\n        # See realpath code: https://github.com/python/cpython/blob/master/Lib/posixpath.py\n        raise RuntimeError('Error, link {0} is a loop!'.format(original_path))\n    return path\n\n\ndef readlink(link_path: str) -> str:\n    \"\"\"\n    Return a string representing the path to which the symbolic link points.\n\n    :param str link_path: The symlink path to resolve\n    :return: The path the symlink points to\n    :returns: str\n    :raise: ValueError if a long path (260> characters) is encountered on Windows\n    \"\"\"\n    path = os.readlink(link_path)\n\n    if POSIX_MODE or not path.startswith('\\\\\\\\?\\\\'):\n        return path\n\n    # At this point, we know we are on Windows and that the path returned uses\n    # the extended form which begins with the prefix \\\\?\\\n\n    # Max length of a normal path is 260 characters on Windows, including the non printable\n    # termination character \"<NUL>\". The termination character is not included in Python\n    # strings, giving a max length of 259 characters, + 4 characters for the extended form\n    # prefix, to an effective max length 263 characters on a string representing a normal path.\n    if len(path) < 264:\n        return path[4:]\n\n    raise ValueError(\"Long paths are not supported by Certbot on Windows.\")\n\n\n# On Windows is_executable run from an unprivileged shell may claim that a path is\n# executable when it is executable only if run from a privileged shell. This result\n# is due to the fact that GetEffectiveRightsFromAcl calculate effective rights\n# without taking into consideration if the target user has currently required the\n# elevated privileges or not. However this is not a problem since certbot always\n# requires to be run under a privileged shell, so the user will always benefit\n# from the highest (privileged one) set of permissions on a given file.\ndef is_executable(path: str) -> bool:\n    \"\"\"\n    Is path an executable file?\n\n    :param str path: path to test\n    :return: True if path is an executable file\n    :rtype: bool\n    \"\"\"\n    if POSIX_MODE:\n        return os.path.isfile(path) and os.access(path, os.X_OK)\n\n    return _win_is_executable(path)\n\n\ndef has_world_permissions(path: str) -> bool:\n    \"\"\"\n    Check if everybody/world has any right (read/write/execute) on a file given its path.\n\n    :param str path: path to test\n    :return: True if everybody/world has any right to the file\n    :rtype: bool\n    \"\"\"\n    if POSIX_MODE:\n        return bool(stat.S_IMODE(os.stat(path).st_mode) & stat.S_IRWXO)\n\n    security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)\n    dacl = security.GetSecurityDescriptorDacl()\n\n    return bool(dacl.GetEffectiveRightsFromAcl({\n        'TrusteeForm': win32security.TRUSTEE_IS_SID,\n        'TrusteeType': win32security.TRUSTEE_IS_USER,\n        'Identifier': win32security.ConvertStringSidToSid('S-1-1-0'),\n    }))\n\n\ndef compute_private_key_mode(old_key: str, base_mode: int) -> int:\n    \"\"\"\n    Calculate the POSIX mode to apply to a private key given the previous private key.\n\n    :param str old_key: path to the previous private key\n    :param int base_mode: the minimum modes to apply to a private key\n    :return: the POSIX mode to apply\n    :rtype: int\n    \"\"\"\n    if POSIX_MODE:\n        # On Linux, we keep read/write/execute permissions\n        # for group and read permissions for everybody.\n        old_mode = (stat.S_IMODE(os.stat(old_key).st_mode) &\n                    (stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP | stat.S_IROTH))\n        return base_mode | old_mode\n\n    # On Windows, the mode returned by os.stat is not reliable,\n    # so we do not keep any permission from the previous private key.\n    return base_mode\n\n\ndef has_same_ownership(path1: str, path2: str) -> bool:\n    \"\"\"\n    Return True if the ownership of two files given their respective path is the same.\n    On Windows, ownership is checked against owner only, since files do not have a group owner.\n\n    :param str path1: path to the first file\n    :param str path2: path to the second file\n    :return: True if both files have the same ownership, False otherwise\n    :rtype: bool\n\n    \"\"\"\n    if POSIX_MODE:\n        stats1 = os.stat(path1)\n        stats2 = os.stat(path2)\n        return (stats1.st_uid, stats1.st_gid) == (stats2.st_uid, stats2.st_gid)\n\n    security1 = win32security.GetFileSecurity(path1, win32security.OWNER_SECURITY_INFORMATION)\n    user1 = security1.GetSecurityDescriptorOwner()\n\n    security2 = win32security.GetFileSecurity(path2, win32security.OWNER_SECURITY_INFORMATION)\n    user2 = security2.GetSecurityDescriptorOwner()\n\n    return user1 == user2\n\n\ndef has_min_permissions(path: str, min_mode: int) -> bool:\n    \"\"\"\n    Check if a file given its path has at least the permissions defined by the given minimal mode.\n    On Windows, group permissions are ignored since files do not have a group owner.\n\n    :param str path: path to the file to check\n    :param int min_mode: the minimal permissions expected\n    :return: True if the file matches the minimal permissions expectations, False otherwise\n    :rtype: bool\n    \"\"\"\n    if POSIX_MODE:\n        st_mode = os.stat(path).st_mode\n        return st_mode == st_mode | min_mode\n\n    # Resolve symlinks, to get a consistent result with os.stat on Linux,\n    # that follows symlinks by default.\n    path = realpath(path)\n\n    # Get owner sid of the file\n    security = win32security.GetFileSecurity(\n        path, win32security.OWNER_SECURITY_INFORMATION | win32security.DACL_SECURITY_INFORMATION)\n    user = security.GetSecurityDescriptorOwner()\n    dacl = security.GetSecurityDescriptorDacl()\n    min_dacl = _generate_dacl(user, min_mode)\n\n    for index in range(min_dacl.GetAceCount()):\n        min_ace = min_dacl.GetAce(index)\n\n        # On a given ACE, index 0 is the ACE type, 1 is the permission mask, and 2 is the SID.\n        # See: http://timgolden.me.uk/pywin32-docs/PyACL__GetAce_meth.html\n        mask = min_ace[1]\n        user = min_ace[2]\n\n        effective_mask = dacl.GetEffectiveRightsFromAcl({\n            'TrusteeForm': win32security.TRUSTEE_IS_SID,\n            'TrusteeType': win32security.TRUSTEE_IS_USER,\n            'Identifier': user,\n        })\n\n        if effective_mask != effective_mask | mask:\n            return False\n\n    return True\n\n\ndef _win_is_executable(path: str) -> bool:\n    if not os.path.isfile(path):\n        return False\n\n    security = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)\n    dacl = security.GetSecurityDescriptorDacl()\n\n    mode = dacl.GetEffectiveRightsFromAcl({\n        'TrusteeForm': win32security.TRUSTEE_IS_SID,\n        'TrusteeType': win32security.TRUSTEE_IS_USER,\n        'Identifier': _get_current_user(),\n    })\n\n    return mode & ntsecuritycon.FILE_GENERIC_EXECUTE == ntsecuritycon.FILE_GENERIC_EXECUTE\n\n\ndef _apply_win_mode(file_path: str, mode: int) -> None:\n    \"\"\"\n    This function converts the given POSIX mode into a Windows ACL list, and applies it to the\n    file given its path. If the given path is a symbolic link, it will resolved to apply the\n    mode on the targeted file.\n    \"\"\"\n    file_path = realpath(file_path)\n    # Get owner sid of the file\n    security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION)\n    user = security.GetSecurityDescriptorOwner()\n\n    # New DACL, that will overwrite existing one (including inherited permissions)\n    dacl = _generate_dacl(user, mode)\n\n    # Apply the new DACL\n    security.SetSecurityDescriptorDacl(1, dacl, 0)\n    win32security.SetFileSecurity(file_path, win32security.DACL_SECURITY_INFORMATION, security)\n\n\ndef _generate_dacl(user_sid: Any, mode: int, mask: Optional[int] = None) -> Any:\n    if mask:\n        mode = mode & (0o777 - mask)\n    analysis = _analyze_mode(mode)\n\n    # Get standard accounts from \"well-known\" sid\n    # See the list here:\n    # https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems\n    system = win32security.ConvertStringSidToSid('S-1-5-18')\n    admins = win32security.ConvertStringSidToSid('S-1-5-32-544')\n    everyone = win32security.ConvertStringSidToSid('S-1-1-0')\n\n    # New dacl, without inherited permissions\n    dacl = win32security.ACL()\n\n    # If user is already system or admins, any ACE defined here would be superseded by\n    # the full control ACE that will be added after.\n    if user_sid not in [system, admins]:\n        # Handle user rights\n        user_flags = _generate_windows_flags(analysis['user'])\n        if user_flags:\n            dacl.AddAccessAllowedAce(win32security.ACL_REVISION, user_flags, user_sid)\n\n    # Handle everybody rights\n    everybody_flags = _generate_windows_flags(analysis['all'])\n    if everybody_flags:\n        dacl.AddAccessAllowedAce(win32security.ACL_REVISION, everybody_flags, everyone)\n\n    # Handle administrator rights\n    full_permissions = _generate_windows_flags({'read': True, 'write': True, 'execute': True})\n    dacl.AddAccessAllowedAce(win32security.ACL_REVISION, full_permissions, system)\n    dacl.AddAccessAllowedAce(win32security.ACL_REVISION, full_permissions, admins)\n\n    return dacl\n\n\ndef _analyze_mode(mode: int) -> dict[str, dict[str, int]]:\n    return {\n        'user': {\n            'read': mode & stat.S_IRUSR,\n            'write': mode & stat.S_IWUSR,\n            'execute': mode & stat.S_IXUSR,\n        },\n        'all': {\n            'read': mode & stat.S_IROTH,\n            'write': mode & stat.S_IWOTH,\n            'execute': mode & stat.S_IXOTH,\n        },\n    }\n\n\ndef _copy_win_ownership(src: str, dst: str) -> None:\n    # Resolve symbolic links\n    src = realpath(src)\n\n    security_src = win32security.GetFileSecurity(src, win32security.OWNER_SECURITY_INFORMATION)\n    user_src = security_src.GetSecurityDescriptorOwner()\n\n    security_dst = win32security.GetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION)\n    # Second parameter indicates, if `False`, that the owner of the file is not provided by some\n    # default mechanism, but is explicitly set instead. This is obviously what we are doing here.\n    security_dst.SetSecurityDescriptorOwner(user_src, False)\n\n    win32security.SetFileSecurity(dst, win32security.OWNER_SECURITY_INFORMATION, security_dst)\n\n\ndef _copy_win_mode(src: str, dst: str) -> None:\n    # Resolve symbolic links\n    src = realpath(src)\n\n    # Copy the DACL from src to dst.\n    security_src = win32security.GetFileSecurity(src, win32security.DACL_SECURITY_INFORMATION)\n    dacl = security_src.GetSecurityDescriptorDacl()\n\n    security_dst = win32security.GetFileSecurity(dst, win32security.DACL_SECURITY_INFORMATION)\n    security_dst.SetSecurityDescriptorDacl(1, dacl, 0)\n    win32security.SetFileSecurity(dst, win32security.DACL_SECURITY_INFORMATION, security_dst)\n\n\ndef _generate_windows_flags(rights_desc: dict[str, int]) -> int:\n    # Some notes about how each POSIX right is interpreted.\n    #\n    # For the rights read and execute, we have a pretty bijective relation between\n    # POSIX flags and their generic counterparts on Windows, so we use them directly\n    # (respectively ntsecuritycon.FILE_GENERIC_READ and ntsecuritycon.FILE_GENERIC_EXECUTE).\n    #\n    # But ntsecuritycon.FILE_GENERIC_WRITE does not correspond to what one could expect from a\n    # write access on Linux: for Windows, FILE_GENERIC_WRITE does not include delete, move or\n    # rename. This is something that requires ntsecuritycon.FILE_ALL_ACCESS.\n    # So to reproduce the write right as POSIX, we will apply ntsecuritycon.FILE_ALL_ACCESS\n    # subtracted of the rights corresponding to POSIX read and POSIX execute.\n    #\n    # Finally, having read + write + execute gives a ntsecuritycon.FILE_ALL_ACCESS,\n    # so a \"Full Control\" on the file.\n    #\n    # A complete list of the rights defined on NTFS can be found here:\n    # https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc783530(v=ws.10)#permissions-for-files-and-folders\n    flag = 0\n    if rights_desc['read']:\n        flag = flag | ntsecuritycon.FILE_GENERIC_READ\n    if rights_desc['write']:\n        flag = flag | (ntsecuritycon.FILE_ALL_ACCESS\n                       ^ ntsecuritycon.FILE_GENERIC_READ\n                       ^ ntsecuritycon.FILE_GENERIC_EXECUTE)\n    if rights_desc['execute']:\n        flag = flag | ntsecuritycon.FILE_GENERIC_EXECUTE\n\n    return flag\n\n\ndef _check_win_mode(file_path: str, mode: int) -> bool:\n    # Resolve symbolic links\n    file_path = realpath(file_path)\n    # Get current dacl file\n    security = win32security.GetFileSecurity(file_path, win32security.OWNER_SECURITY_INFORMATION\n                                             | win32security.DACL_SECURITY_INFORMATION)\n    dacl = security.GetSecurityDescriptorDacl()\n\n    # Get current file owner sid\n    user = security.GetSecurityDescriptorOwner()\n\n    if not dacl:\n        # No DACL means full control to everyone\n        # This is not a deterministic permissions set.\n        return False\n\n    # Calculate the target dacl\n    ref_dacl = _generate_dacl(user, mode)\n\n    return _compare_dacls(dacl, ref_dacl)\n\n\ndef _compare_dacls(dacl1: Any, dacl2: Any) -> bool:\n    \"\"\"\n    This method compare the two given DACLs to check if they are identical.\n    Identical means here that they contains the same set of ACEs in the same order.\n    \"\"\"\n    return ([dacl1.GetAce(index) for index in range(dacl1.GetAceCount())] ==\n            [dacl2.GetAce(index) for index in range(dacl2.GetAceCount())])\n\n\ndef _get_current_user() -> Any:\n    \"\"\"\n    Return the pySID corresponding to the current user.\n    \"\"\"\n    # We craft the account_name ourselves instead of calling for instance win32api.GetUserNameEx,\n    # because this function returns nonsense values when Certbot is run under NT AUTHORITY\\SYSTEM.\n    # To run Certbot under NT AUTHORITY\\SYSTEM, you can open a shell using the instructions here:\n    # https://blogs.technet.microsoft.com/ben_parker/2010/10/27/how-do-i-run-powershell-execommand-prompt-as-the-localsystem-account-on-windows-7/\n    account_name = r\"{0}\\{1}\".format(win32api.GetDomainName(), win32api.GetUserName())\n    # LookupAccountName() expects the system name as first parameter. By passing None to it,\n    # we instruct Windows to first search the matching account in the machine local accounts,\n    # then into the primary domain accounts, if the machine has joined a domain, then finally\n    # into the trusted domains accounts. This is the preferred lookup mechanism to use in Windows\n    # if there is no reason to use a specific lookup mechanism.\n    # See https://docs.microsoft.com/en-us/windows/desktop/api/winbase/nf-winbase-lookupaccountnamea\n    return win32security.LookupAccountName(None, account_name)[0]\n"
  },
  {
    "path": "certbot/src/certbot/compat/misc.py",
    "content": "\"\"\"\nThis compat module handles various platform specific calls that do not fall into one\nparticular category.\n\"\"\"\nfrom __future__ import absolute_import\n\nimport logging\nimport select\nimport subprocess\nimport sys\nfrom typing import Optional\n\nfrom certbot import errors\nfrom certbot.compat import os\n\ntry:\n    from pywintypes import error as pywinerror\n    from win32com.shell import shell as shellwin32\n    from win32console import GetStdHandle\n    from win32console import STD_OUTPUT_HANDLE\n    POSIX_MODE = False\nexcept ImportError:  # pragma: no cover\n    POSIX_MODE = True\n\n\nlogger = logging.getLogger(__name__)\n\n# For Linux: define OS specific standard binary directories\nSTANDARD_BINARY_DIRS = [\"/usr/sbin\", \"/usr/local/bin\", \"/usr/local/sbin\"] if POSIX_MODE else []\n\n\ndef raise_for_non_administrative_windows_rights() -> None:\n    \"\"\"\n    On Windows, raise if current shell does not have the administrative rights.\n    Do nothing on Linux.\n\n    :raises .errors.Error: If the current shell does not have administrative rights on Windows.\n    \"\"\"\n    if not POSIX_MODE and shellwin32.IsUserAnAdmin() == 0:  # pragma: no cover\n        raise errors.Error('Error, certbot must be run on a shell with administrative rights.')\n\n\ndef prepare_virtual_console() -> None:\n    \"\"\"\n    On Windows, ensure that Console Virtual Terminal Sequences are enabled.\n\n    \"\"\"\n    if POSIX_MODE:\n        return\n\n    # https://docs.microsoft.com/en-us/windows/console/setconsolemode\n    ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004\n\n    # stdout/stderr will be the same console screen buffer, but this could return None or raise\n    try:\n        h = GetStdHandle(STD_OUTPUT_HANDLE)\n        if h:\n            h.SetConsoleMode(h.GetConsoleMode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)\n    except pywinerror:\n        logger.debug(\"Failed to set console mode\", exc_info=True)\n\n\ndef readline_with_timeout(timeout: float, prompt: Optional[str]) -> str:\n    \"\"\"\n    Read user input to return the first line entered, or raise after specified timeout.\n\n    :param float timeout: The timeout in seconds given to the user.\n    :param str prompt: The prompt message to display to the user.\n\n    :returns: The first line entered by the user.\n    :rtype: str\n\n    \"\"\"\n    try:\n        # Linux specific\n        #\n        # Call to select can only be done like this on UNIX\n        rlist, _, _ = select.select([sys.stdin], [], [], timeout)\n        if not rlist:\n            raise errors.Error(\n                \"Timed out waiting for answer to prompt '{0}'\".format(prompt if prompt else \"\"))\n        return rlist[0].readline()\n    except OSError:\n        # Windows specific\n        #\n        # No way with select to make a timeout to the user input on Windows,\n        # as select only supports socket in this case.\n        # So no timeout on Windows for now.\n        return sys.stdin.readline()\n\n\nWINDOWS_DEFAULT_FOLDERS = {\n    'config': 'C:\\\\Certbot',\n    'work': 'C:\\\\Certbot\\\\lib',\n    'logs': 'C:\\\\Certbot\\\\log',\n}\nLINUX_DEFAULT_FOLDERS = {\n    'config': '/etc/letsencrypt',\n    'work': '/var/lib/letsencrypt',\n    'logs': '/var/log/letsencrypt',\n}\n\n\ndef get_default_folder(folder_type: str) -> str:\n    \"\"\"\n    Return the relevant default folder for the current OS\n\n    :param str folder_type: The type of folder to retrieve (config, work or logs)\n\n    :returns: The relevant default folder.\n    :rtype: str\n\n    \"\"\"\n    if os.name != 'nt':\n        # Linux specific\n        return LINUX_DEFAULT_FOLDERS[folder_type]\n    # Windows specific\n    return WINDOWS_DEFAULT_FOLDERS[folder_type]\n\n\ndef underscores_for_unsupported_characters_in_path(path: str) -> str:\n    \"\"\"\n    Replace unsupported characters in path for current OS by underscores.\n    :param str path: the path to normalize\n    :return: the normalized path\n    :rtype: str\n    \"\"\"\n    if os.name != 'nt':\n        # Linux specific\n        return path\n\n    # Windows specific\n    drive, tail = os.path.splitdrive(path)\n    return drive + tail.replace(':', '_')\n\n\ndef execute_command_status(cmd_name: str, shell_cmd: str,\n                           env: Optional[dict] = None) -> tuple[int, str, str]:\n    \"\"\"\n    Run a command:\n        - on Linux command will be run by the standard shell selected with\n          subprocess.run(shell=True)\n        - on Windows command will be run in a Powershell shell\n\n    This function returns the exit code, and does not log the result and output\n    of the command.\n\n    :param str cmd_name: the user facing name of the hook being run\n    :param str shell_cmd: shell command to execute\n    :param dict env: environ to pass into subprocess.run\n\n    :returns: `tuple` (`int` returncode, `str` stderr, `str` stdout)\n    \"\"\"\n    logger.info(\"Running %s command: %s\", cmd_name, shell_cmd)\n\n    if POSIX_MODE:\n        proc = subprocess.run(shell_cmd, shell=True, stdout=subprocess.PIPE,\n                              stderr=subprocess.PIPE, universal_newlines=True,\n                              check=False, env=env)\n    else:\n        line = ['powershell.exe', '-Command', shell_cmd]\n        proc = subprocess.run(line, stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                              universal_newlines=True, check=False, env=env)\n\n    # universal_newlines causes stdout and stderr to be str objects instead of\n    # bytes in Python 3\n    out, err = proc.stdout, proc.stderr\n    return proc.returncode, err, out\n"
  },
  {
    "path": "certbot/src/certbot/compat/os.py",
    "content": "\"\"\"\nThis compat modules is a wrapper of the core os module that forbids usage of specific operations\n(e.g. chown, chmod, getuid) that would be harmful to the Windows file security model of Certbot.\nThis module is intended to replace standard os module throughout certbot projects (except acme).\n\nThis module has the same API as the os module in the Python standard library\nexcept for the functions defined below.\n\nisort:skip_file\n\"\"\"\n\n# NB1: If adding a new documented function to compat.os, ensure that it is added to the\n#       ':members:' list in certbot/docs/api/certbot.compat.os.rst.\n\n# NB2: Each function defined in compat.os is marked with \"type: ignore\" to avoid mypy\n#      to complain that a function is redefined (because we imported if first from os).\n\n# pylint: disable=function-redefined\nfrom __future__ import absolute_import\n\n# First round of wrapping: we import statically all public attributes exposed by the os module\n# This allows in particular to have pylint, mypy, IDEs be aware that most of os members are\n# available in certbot.compat.os.\nfrom os import *  # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin,os-module-forbidden # noqa: F403\n\n# Second round of wrapping: we import dynamically all attributes from the os module that have not\n# yet been imported by the first round (static import). This covers in particular the case of\n# specific python 3.x versions where not all public attributes are in the special __all__ of os,\n# and so not in `from os import *`.\nimport os as std_os  # pylint: disable=os-module-forbidden\nimport sys as std_sys\n\nourselves = std_sys.modules[__name__]\n# Adding all of stdlib os to this module confuses Sphinx so we skip this when\n# building the documentation.\nif not std_os.environ.get(\"CERTBOT_DOCS\") == \"1\":\n    for attribute in dir(std_os):\n        # Check if the attribute does not already exist in our module. It could\n        # be internal attributes of the module (__name__, __doc__), or\n        # attributes from standard os already imported with `from os import *`.\n        if not hasattr(ourselves, attribute):\n            setattr(ourselves, attribute, getattr(std_os, attribute))\n\n# Import our internal path module, then allow certbot.compat.os.path\n# to behave as a module (similarly to os.path).\nfrom certbot.compat import _path as path  # type: ignore  # pylint: disable=wrong-import-position # noqa: E402\nstd_sys.modules[__name__ + '.path'] = path\n\n# Clean all remaining importables that are not from the core os module.\ndel ourselves, std_os, std_sys\n\n\n# Chmod is the root of all evil for our security model on Windows. With the default implementation\n# of os.chmod on Windows, almost all bits on mode will be ignored, and only a general RO or RW will\n# be applied. The DACL, the inner mechanism to control file access on Windows, will stay on its\n# default definition, giving effectively at least read permissions to any one, as the default\n# permissions on root path will be inherit by the file (as NTFS state), and root path can be read\n# by anyone. So the given mode needs to be translated into a secured and not inherited DACL that\n# will be applied to this file using filesystem.chmod, calling internally the win32security\n# module to construct and apply the DACL. Complete security model to translate a POSIX mode into\n# a suitable DACL on Windows for Certbot can be found here:\n# https://github.com/certbot/certbot/issues/6356\n# Basically, it states that appropriate permissions will be set for the owner, nothing for the\n# group, appropriate permissions for the \"Everyone\" group, and all permissions to the\n# \"Administrators\" group + \"System\" user, as they can do everything anyway.\ndef chmod(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.chmod() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.chmod() is forbidden. '\n                       'Use certbot.compat.filesystem.chmod() instead.')\n\n\n# Since there is no mode on Windows, there is no umask either, and so this method is a noop for\n# this platform. In order to have a consistent behavior between Linux and Windows on Certbot files\n# and directories, the filesystem umask method must be used instead, since it implements umask for\n# Windows.\ndef umask(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.chmod() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.umask() is forbidden. '\n                       'Use certbot.compat.filesystem.umask() instead.')\n\n\n# Because uid is not a concept on Windows, chown is useless. In fact, it is not even available\n# on Python for Windows. So to be consistent on both platforms for Certbot, this method is\n# always forbidden.\ndef chown(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.chown() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.chown() is forbidden.'\n                       'Use certbot.compat.filesystem.copy_ownership_and_apply_mode() instead.')\n\n\n# The os.open function on Windows has the same effect as a call to os.chown concerning the file\n# modes: these modes lack the correct control over the permissions given to the file. Instead,\n# filesystem.open invokes the Windows native API `CreateFile` to ensure that permissions are\n# atomically set in case of file creation, or invokes filesystem.chmod to properly set the\n# permissions for the other cases.\ndef open(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.open() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.open() is forbidden. '\n                       'Use certbot.compat.filesystem.open() instead.')\n\n\n# Very similarly to os.open, os.mkdir has the same effects on Windows and creates an unsecured\n# folder. So a similar mitigation to security.chmod is provided on this platform.\ndef mkdir(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.mkdir() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.mkdir() is forbidden. '\n                       'Use certbot.compat.filesystem.mkdir() instead.')\n\n\n# As said above, os.makedirs would call the original os.mkdir function recursively on Windows,\n# creating the same flaws for every actual folder created. This method is modified to ensure\n# that our modified os.mkdir is called on Windows, by monkey patching temporarily the mkdir method\n# on the original os module, executing the modified logic to correctly protect newly created\n# folders, then restoring original mkdir method in the os module.\ndef makedirs(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.makedirs() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.makedirs() is forbidden. '\n                       'Use certbot.compat.filesystem.makedirs() instead.')\n\n\n# Because of the blocking strategy on file handlers on Windows, rename does not behave as expected\n# with POSIX systems: an exception will be raised if dst already exists.\ndef rename(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.rename() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.rename() is forbidden. '\n                       'Use certbot.compat.filesystem.replace() instead.')\n\n\n# Behavior of os.replace is consistent between Windows and Linux. However, it is not supported on\n# Python 2.x. So, as for os.rename, we forbid it in favor of filesystem.replace.\ndef replace(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.replace() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.replace() is forbidden. '\n                       'Use certbot.compat.filesystem.replace() instead.')\n\n\n# Results given by os.access are inconsistent or partial on Windows, because this platform is not\n# following the POSIX approach.\ndef access(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.access() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.access() is forbidden. '\n                       'Use certbot.compat.filesystem.check_mode() or '\n                       'certbot.compat.filesystem.is_executable() instead.')\n\n\n# On Windows os.stat call result is inconsistent, with a lot of flags that are not set or\n# meaningless. We need to use specialized functions from the certbot.compat.filesystem module.\ndef stat(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.stat() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.stat() is forbidden. '\n                       'Use certbot.compat.filesystem functions instead '\n                       '(eg. has_min_permissions, has_same_ownership).')\n\n\n# Method os.fstat has the same problem than os.stat, since it is the same function,\n# but accepting a file descriptor instead of a path.\ndef fstat(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.stat() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.fstat() is forbidden. '\n                       'Use certbot.compat.filesystem functions instead '\n                       '(eg. has_min_permissions, has_same_ownership).')\n\n\n# On Windows, os.readlink \"typically\" returns the resolved path in its \"extended-style\" form which\n# allows use of more than 259 characters and its string representation is prepended with \"\\\\?\\\". See\n# https://docs.python.org/3/library/os.html#os.readlink. This causes problems for us because the\n# behavior is not consistent (see https://github.com/certbot/certbot/pull/10136) and paths that have\n# this prefix won't match those that do not in simple equality comparisons.\ndef readlink(*unused_args, **unused_kwargs):  # type: ignore\n    \"\"\"Method os.readlink() is forbidden\"\"\"\n    raise RuntimeError('Usage of os.readlink() is forbidden. '\n                       'Use certbot.compat.filesystem.realpath() instead.')\n"
  },
  {
    "path": "certbot/src/certbot/configuration.py",
    "content": "\"\"\"Certbot user-supplied configuration.\"\"\"\nimport argparse\nimport copy\nimport enum\nimport logging\nfrom typing import Any\nfrom typing import Optional\nfrom urllib import parse\n\nfrom certbot import errors\nfrom certbot._internal import constants\nfrom certbot.compat import misc\nfrom certbot.compat import os\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass ArgumentSource(enum.Enum):\n    \"\"\"Enum for describing where a configuration argument was set.\"\"\"\n\n    COMMAND_LINE = enum.auto()\n    \"\"\"Argument was specified on the command line\"\"\"\n    CONFIG_FILE = enum.auto()\n    \"\"\"Argument was specified in a .ini config file\"\"\"\n    DEFAULT = enum.auto()\n    \"\"\"Argument was not set by the user, and was assigned its default value\"\"\"\n    ENV_VAR = enum.auto()\n    \"\"\"Argument was specified in an environment variable\"\"\"\n    RUNTIME = enum.auto()\n    \"\"\"Argument was set at runtime by certbot\"\"\"\n\n\nclass NamespaceConfig:\n    \"\"\"Configuration wrapper around :class:`argparse.Namespace`.\n\n    Please note that the following attributes are dynamically resolved using\n    :attr:`~certbot.configuration.NamespaceConfig.work_dir` and relative\n    paths defined in :py:mod:`certbot._internal.constants`:\n\n      - `accounts_dir`\n      - `in_progress_dir`\n      - `temp_checkpoint_dir`\n\n    And the following paths are dynamically resolved using\n    :attr:`~certbot.configuration.NamespaceConfig.config_dir` and relative\n    paths defined in :py:mod:`certbot._internal.constants`:\n\n      - `default_archive_dir`\n      - `live_dir`\n      - `renewal_configs_dir`\n\n    :ivar namespace: Namespace typically produced by\n        :meth:`argparse.ArgumentParser.parse_args`.\n    :type namespace: :class:`argparse.Namespace`\n\n    \"\"\"\n\n    def __init__(self, namespace: argparse.Namespace) -> None:\n        self.namespace: argparse.Namespace\n        # Avoid recursion loop because of the delegation defined in __setattr__\n        object.__setattr__(self, 'namespace', namespace)\n        object.__setattr__(self, '_argument_sources', None)\n        object.__setattr__(self, '_previously_accessed_mutables', {})\n\n        self.namespace.config_dir = os.path.abspath(self.namespace.config_dir)\n        self.namespace.work_dir = os.path.abspath(self.namespace.work_dir)\n        self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir)\n\n        # Check command line parameters sanity, and error out in case of problem.\n        _check_config_sanity(self)\n\n    def set_argument_sources(self, argument_sources: dict[str, ArgumentSource]) -> None:\n        \"\"\"\n        Associate the NamespaceConfig with a dictionary describing where each of\n        its arguments came from, e.g. `{ 'email': ArgumentSource.CONFIG_FILE }`.\n        This is necessary for making runtime evaluations on whether an argument\n        was specified by the user or not (see `set_by_user`).\n\n        For an example of how to build such a dictionary, see\n        `certbot._internal.cli.helpful.HelpfulArgumentParser._build_sources_dict`\n\n        :ivar argument_sources: dictionary of argument names to their :class:`ArgumentSource`\n        :type argument_sources: :class:`Dict[str, ArgumentSource]`\n        \"\"\"\n\n        # Avoid recursion loop because of the delegation defined in __setattr__\n        object.__setattr__(self, '_argument_sources', argument_sources)\n\n\n    def set_by_user(self, var: str) -> bool:\n        \"\"\"\n        Return True if a particular config variable has been set by the user\n        (via CLI or config file) including if the user explicitly set it to the\n        default, or if it was dynamically set at runtime.  Returns False if the\n        variable was assigned a default value.\n\n        Raises an exception if `argument_sources` is not set.\n        \"\"\"\n        from certbot._internal.cli.cli_constants import DEPRECATED_OPTIONS\n        from certbot._internal.cli.cli_constants import VAR_MODIFIERS\n        from certbot._internal.plugins import selection\n\n        if self.argument_sources is None:\n            raise RuntimeError(\n                \"NamespaceConfig.set_by_user called without an ArgumentSources dict. \"\n                \"See NamespaceConfig.set_argument_sources().\")\n\n        # We should probably never actually hit this code. But if we do,\n        # a deprecated option has logically never been set by the CLI.\n        if var in DEPRECATED_OPTIONS:\n            return False\n\n        if var in ['authenticator', 'installer']:\n            auth, inst = selection.cli_plugin_requests(self)\n            if var == 'authenticator':\n                return auth is not None\n            if var == 'installer':\n                return inst is not None\n\n        if var in self.argument_sources and self.argument_sources[var] != ArgumentSource.DEFAULT:\n            logger.debug(\"Var %s=%s (set by user).\", var, getattr(self, var))\n            return True\n\n        for modifier in VAR_MODIFIERS.get(var, []):\n            if self.set_by_user(modifier):\n                logger.debug(\"Var %s=%s (set by user).\",\n                    var, VAR_MODIFIERS.get(var, []))\n                return True\n\n        return False\n\n    def to_dict(self) -> dict[str, Any]:\n        \"\"\"\n        Returns a dictionary mapping all argument names to their values\n        \"\"\"\n        return vars(self.namespace)\n\n    def _mark_runtime_override(self, name: str) -> None:\n        \"\"\"\n        If an argument_sources dict was set, overwrites an argument's source to\n        be ArgumentSource.RUNTIME. Used when certbot sets an argument's values\n        at runtime. This also clears the modified value from\n        _previously_accessed_mutables since it is no longer needed.\n        \"\"\"\n        if self._argument_sources is not None:\n            self._argument_sources[name] = ArgumentSource.RUNTIME\n            if name in self._previously_accessed_mutables:\n                del self._previously_accessed_mutables[name]\n\n    @property\n    def argument_sources(self) -> Optional[dict[str, ArgumentSource]]:\n        \"\"\"Returns _argument_sources after handling any changes to accessed mutable values.\"\"\"\n        # We keep values in _previously_accessed_mutables until we've detected a modification to try\n        # to provide up-to-date information when argument_sources is accessed. Once a mutable object\n        # has been accessed, it can be modified at any time if a reference to it was kept somewhere\n        # else.\n\n        # We copy _previously_accessed_mutables because _mark_runtime_override modifies it.\n        for name, prev_value in self._previously_accessed_mutables.copy().items():\n            current_value = getattr(self.namespace, name)\n            if current_value != prev_value:\n                self._mark_runtime_override(name)\n        return self._argument_sources\n\n    # Delegate any attribute not explicitly defined to the underlying namespace object.\n    #\n    # If any mutable namespace attributes are explicitly defined in the future, you'll probably want\n    # to take an approach like the one used in __getattr__ and the argument_sources property.\n\n    def __getattr__(self, name: str) -> Any:\n        arg_sources = self.argument_sources\n        value = getattr(self.namespace, name)\n        if arg_sources is not None:\n            # If the requested attribute was already modified at runtime, we don't need to track any\n            # future changes.\n            if name not in arg_sources or arg_sources[name] != ArgumentSource.RUNTIME:\n                # If name is already in _previously_accessed_mutables, we don't need to make a copy\n                # of it again. If its value was changed, this would have been caught while preparing\n                # the return value of the property self.argument_sources accessed earlier in this\n                # function.\n                if name not in self._previously_accessed_mutables and not _is_immutable(value):\n                    self._previously_accessed_mutables[name] = copy.deepcopy(value)\n        return value\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        self._mark_runtime_override(name)\n        setattr(self.namespace, name, value)\n\n    @property\n    def server(self) -> str:\n        \"\"\"ACME Directory Resource URI.\"\"\"\n        return self.namespace.server\n\n    @server.setter\n    def server(self, server_: str) -> None:\n        self._mark_runtime_override('server')\n        self.namespace.server = server_\n\n    @property\n    def email(self) -> Optional[str]:\n        \"\"\"Email used for registration and recovery contact.\n\n        Use comma to register multiple emails,\n        ex: u1@example.com,u2@example.com. (default: Ask).\n        \"\"\"\n        return self.namespace.email\n\n    @email.setter\n    def email(self, mail: str) -> None:\n        self._mark_runtime_override('email')\n        self.namespace.email = mail\n\n    @property\n    def rsa_key_size(self) -> int:\n        \"\"\"Size of the RSA key.\"\"\"\n        return self.namespace.rsa_key_size\n\n    @rsa_key_size.setter\n    def rsa_key_size(self, ksize: int) -> None:\n        \"\"\"Set the rsa_key_size property\"\"\"\n        self._mark_runtime_override('rsa_key_size')\n        self.namespace.rsa_key_size = ksize\n\n    @property\n    def elliptic_curve(self) -> str:\n        \"\"\"The SECG elliptic curve name to use.\n\n        Please see RFC 8446 for supported values.\n        \"\"\"\n        return self.namespace.elliptic_curve\n\n    @elliptic_curve.setter\n    def elliptic_curve(self, ecurve: str) -> None:\n        \"\"\"Set the elliptic_curve property\"\"\"\n        self._mark_runtime_override('elliptic_curve')\n        self.namespace.elliptic_curve = ecurve\n\n    @property\n    def key_type(self) -> str:\n        \"\"\"Type of generated private key.\n\n        Only *ONE* per invocation can be provided at this time.\n        \"\"\"\n        return self.namespace.key_type\n\n    @key_type.setter\n    def key_type(self, ktype: str) -> None:\n        \"\"\"Set the key_type property\"\"\"\n        self._mark_runtime_override('key_type')\n        self.namespace.key_type = ktype\n\n    @property\n    def must_staple(self) -> bool:\n        \"\"\"Adds the OCSP Must-Staple extension to the certificate.\n\n        Autoconfigures OCSP Stapling for supported setups\n        (Apache version >= 2.3.3 ).\n        \"\"\"\n        return self.namespace.must_staple\n\n    @property\n    def config_dir(self) -> str:\n        \"\"\"Configuration directory.\"\"\"\n        return self.namespace.config_dir\n\n    @property\n    def work_dir(self) -> str:\n        \"\"\"Working directory.\"\"\"\n        return self.namespace.work_dir\n\n    @property\n    def accounts_dir(self) -> str:\n        \"\"\"Directory where all account information is stored.\"\"\"\n        return self.accounts_dir_for_server_path(self.server_path)\n\n    @property\n    def backup_dir(self) -> str:\n        \"\"\"Configuration backups directory.\"\"\"\n        return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)\n\n    @property\n    def in_progress_dir(self) -> str:\n        \"\"\"Directory used before a permanent checkpoint is finalized.\"\"\"\n        return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)\n\n    @property\n    def temp_checkpoint_dir(self) -> str:\n        \"\"\"Temporary checkpoint directory.\"\"\"\n        return os.path.join(\n            self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)\n\n    @property\n    def no_verify_ssl(self) -> bool:\n        \"\"\"Disable verification of the ACME server's certificate.\n\n        The root certificates trusted by Certbot can be overridden by setting the\n        REQUESTS_CA_BUNDLE environment variable.\n        \"\"\"\n        return self.namespace.no_verify_ssl\n\n    @property\n    def http01_port(self) -> int:\n        \"\"\"Port used in the http-01 challenge.\n\n        This only affects the port Certbot listens on.\n        A conforming ACME server will still attempt to connect on port 80.\n        \"\"\"\n        return self.namespace.http01_port\n\n    @property\n    def http01_address(self) -> str:\n        \"\"\"The address the server listens to during http-01 challenge.\"\"\"\n        return self.namespace.http01_address\n\n    @property\n    def https_port(self) -> int:\n        \"\"\"Port used to serve HTTPS.\n\n        This affects which port Nginx will listen on after a LE certificate\n        is installed.\n        \"\"\"\n        return self.namespace.https_port\n\n    @property\n    def pref_challs(self) -> list[str]:\n        \"\"\"List of user specified preferred challenges.\n\n        Sorted with the most preferred challenge listed first.\n        \"\"\"\n        return self.namespace.pref_challs\n\n    @property\n    def allow_subset_of_names(self) -> bool:\n        \"\"\"Allow only a subset of names to be authorized to perform validations.\n\n        When performing domain validation, do not consider it a failure\n        if authorizations can not be obtained for a strict subset of\n        the requested domains. This may be useful for allowing renewals for\n        multiple domains to succeed even if some domains no longer point\n        at this system.\n        \"\"\"\n        return self.namespace.allow_subset_of_names\n\n    @property\n    def strict_permissions(self) -> bool:\n        \"\"\"Enable strict permissions checks.\n\n        Require that all configuration files are owned by the current\n        user; only needed if your config is somewhere unsafe like /tmp/.\n        \"\"\"\n        return self.namespace.strict_permissions\n\n    @property\n    def disable_renew_updates(self) -> bool:\n        \"\"\"Disable renewal updates.\n\n        If updates provided by installer enhancements when Certbot is being run\n        with \\\"renew\\\" verb should be disabled.\n        \"\"\"\n        return self.namespace.disable_renew_updates\n\n    @property\n    def required_profile(self) -> Optional[str]:\n        \"\"\"Request the given profile name from the ACME server.\n\n        If the ACME server returns an error, issuance (or renewal) will fail.\n        For long-term reliability, setting preferred_profile instead may be\n        preferable because it allows fallback to a default. Use this setting\n        when renewal failure is preferable to fallback.\n        \"\"\"\n        return self.namespace.required_profile\n\n    @property\n    def preferred_profile(self) -> Optional[str]:\n        \"\"\"Request the given profile name from the ACME server, or fallback to default.\n\n        If the given profile name exists in the ACME directory, use it to request a\n        a certificate. Otherwise, fall back to requesting a certificate without a profile\n        (which means the CA will use its default profile). This allows renewals to\n        succeed even if the CA deprecates and removes a given profile.\n        \"\"\"\n        return self.namespace.preferred_profile\n\n    @property\n    def preferred_chain(self) -> Optional[str]:\n        \"\"\"Set the preferred certificate chain.\n\n        If the CA offers multiple certificate chains, prefer the chain whose\n        topmost certificate was issued from this Subject Common Name.\n        If no match, the default offered chain will be used.\n        \"\"\"\n        return self.namespace.preferred_chain\n\n    @property\n    def server_path(self) -> str:\n        \"\"\"File path based on ``server``.\"\"\"\n        parsed = parse.urlparse(self.namespace.server)\n        return (parsed.netloc + parsed.path).replace('/', os.path.sep)\n\n    def accounts_dir_for_server_path(self, server_path: str) -> str:\n        \"\"\"Path to accounts directory based on server_path\"\"\"\n        server_path = misc.underscores_for_unsupported_characters_in_path(server_path)\n        return os.path.join(\n            self.namespace.config_dir, constants.ACCOUNTS_DIR, server_path)\n\n    @property\n    def default_archive_dir(self) -> str:  # pylint: disable=missing-function-docstring\n        return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)\n\n    @property\n    def live_dir(self) -> str:  # pylint: disable=missing-function-docstring\n        return os.path.join(self.namespace.config_dir, constants.LIVE_DIR)\n\n    @property\n    def renewal_configs_dir(self) -> str:  # pylint: disable=missing-function-docstring\n        return os.path.join(\n            self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)\n\n    @property\n    def renewal_hooks_dir(self) -> str:\n        \"\"\"Path to directory with hooks to run when getting or renewing certificates.\"\"\"\n        return os.path.join(self.namespace.config_dir,\n                            constants.RENEWAL_HOOKS_DIR)\n\n    @property\n    def renewal_pre_hooks_dir(self) -> str:\n        \"\"\"Path to the pre-hook directory for hooks to run\n        before attempting to get or renew certs.\n        \"\"\"\n        return os.path.join(self.renewal_hooks_dir,\n                            constants.RENEWAL_PRE_HOOKS_DIR)\n\n    @property\n    def renewal_deploy_hooks_dir(self) -> str:\n        \"\"\"Path to the deploy-hook directory for hooks to run\n        upon successfully getting or renewing certs.\n        \"\"\"\n        return os.path.join(self.renewal_hooks_dir,\n                            constants.RENEWAL_DEPLOY_HOOKS_DIR)\n\n    @property\n    def renewal_post_hooks_dir(self) -> str:\n        \"\"\"Path to the post-hook directory for hooks to run\n        after attempting to get or renew certs.\n        \"\"\"\n        return os.path.join(self.renewal_hooks_dir,\n                            constants.RENEWAL_POST_HOOKS_DIR)\n\n    @property\n    def issuance_timeout(self) -> int:\n        \"\"\"This option specifies how long (in seconds) Certbot will wait\n        for the server to issue a certificate.\n        \"\"\"\n        return self.namespace.issuance_timeout\n\n    @property\n    def new_key(self) -> bool:\n        \"\"\"This option specifies whether Certbot should generate a new private\n        key when replacing a certificate, even if reuse_key is set.\n        \"\"\"\n        return self.namespace.new_key\n\n    # Magic methods\n\n    def __deepcopy__(self, _memo: Any) -> 'NamespaceConfig':\n        # Work around https://bugs.python.org/issue1515 for py26 tests :( :(\n        new_ns = copy.deepcopy(self.namespace)\n        new_config = type(self)(new_ns)\n        # Avoid recursion loop because of the delegation defined in __setattr__\n        object.__setattr__(new_config, '_argument_sources', copy.deepcopy(self.argument_sources))\n        object.__setattr__(new_config, '_previously_accessed_mutables',\n                           copy.deepcopy(self._previously_accessed_mutables))\n        return new_config\n\n\ndef _check_config_sanity(config: NamespaceConfig) -> None:\n    \"\"\"Validate command line options and display error message if\n    requirements are not met.\n\n    :param config: NamespaceConfig instance holding user configuration\n    :type args: :class:`certbot.configuration.NamespaceConfig`\n\n    \"\"\"\n    # Port check\n    if config.http01_port == config.https_port:\n        raise errors.ConfigurationError(\n            \"Trying to run http-01 and https-port \"\n            \"on the same port ({0})\".format(config.https_port))\n\n\ndef _is_immutable(value: Any) -> bool:\n    \"\"\"Is value of an immutable type?\"\"\"\n    if isinstance(value, tuple):\n        # tuples are only immutable if all contained values are immutable.\n        return all(_is_immutable(subvalue) for subvalue in value)\n    for immutable_type in (int, float, complex, str, bytes, bool, frozenset,):\n        if isinstance(value, immutable_type):\n            return True\n    # The last case we consider here is None which is also immutable.\n    return value is None\n"
  },
  {
    "path": "certbot/src/certbot/crypto_util.py",
    "content": "\"\"\"Certbot client crypto utility functions.\n\n.. todo:: Make the transition to use PSS rather than PKCS1_v1_5 when the server\n    is capable of handling the signatures.\n\n\"\"\"\nimport datetime\nimport hashlib\nimport ipaddress\nimport logging\nimport re\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\nfrom typing import Union\nimport warnings\n\nfrom cryptography import x509\nfrom cryptography.exceptions import InvalidSignature\nfrom cryptography.exceptions import UnsupportedAlgorithm\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives.asymmetric.dsa import DSAPublicKey\nfrom cryptography.hazmat.primitives.asymmetric.ec import ECDSA\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePublicKey\nfrom cryptography.hazmat.primitives.asymmetric.padding import PKCS1v15\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom cryptography.hazmat.primitives.serialization import NoEncryption\nfrom cryptography.hazmat.primitives.serialization import PrivateFormat\nfrom OpenSSL import SSL\n\nfrom acme import crypto_util as acme_crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot.compat import os\n\n# Cryptography ed448 and ed25519 modules do not exist on oldest tests\nif TYPE_CHECKING:\n    from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PublicKey\n    from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey\n    from cryptography.hazmat.primitives.asymmetric.x448 import X448PublicKey\n    from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PublicKey\n\nlogger = logging.getLogger(__name__)\n\n\n# High level functions\n\ndef generate_key(key_size: int, key_dir: Optional[str], key_type: str = \"rsa\",\n                 elliptic_curve: str = \"secp256r1\", keyname: str = \"key-certbot.pem\",\n                 strict_permissions: bool = True) -> util.Key:\n    \"\"\"Initializes and saves a privkey.\n\n    Inits key and saves it in PEM format on the filesystem.\n\n    .. note:: keyname is the attempted filename, it may be different if a file\n        already exists at the path.\n\n    :param int key_size: key size in bits if key size is rsa.\n    :param str key_dir: Optional key save directory.\n    :param str key_type: Key Type [rsa, ecdsa]\n    :param str elliptic_curve: Name of the elliptic curve if key type is ecdsa.\n    :param str keyname: Filename of key\n    :param bool strict_permissions: If true and key_dir exists, an exception is raised if\n        the directory doesn't have 0700 permissions or isn't owned by the current user.\n\n    :returns: Key\n    :rtype: :class:`certbot.util.Key`\n\n    :raises ValueError: If unable to generate the key given key_size.\n\n    \"\"\"\n    try:\n        key_pem = make_key(\n            bits=key_size, elliptic_curve=elliptic_curve or \"secp256r1\", key_type=key_type,\n        )\n    except ValueError as err:\n        logger.debug(\"\", exc_info=True)\n        logger.error(\"Encountered error while making key: %s\", str(err))\n        raise err\n\n    # Save file\n    key_path = None\n    if key_dir:\n        util.make_or_verify_dir(key_dir, 0o700, strict_permissions)\n        key_f, key_path = util.unique_file(\n            os.path.join(key_dir, keyname), 0o600, \"wb\")\n        with key_f:\n            key_f.write(key_pem)\n        if key_type == 'rsa':\n            logger.debug(\"Generating RSA key (%d bits): %s\", key_size, key_path)\n        else:\n            logger.debug(\"Generating ECDSA key (%d bits): %s\", key_size, key_path)\n\n    return util.Key(key_path, key_pem)\n\n\ndef generate_csr(privkey: util.Key, names: Union[list[str], set[str]], path: Optional[str],\n                 must_staple: bool = False, strict_permissions: bool = True,\n                 ipaddrs: list[ipaddress.IPv4Address | ipaddress.IPv6Address] | None = None,\n                 ) -> util.CSR:\n    \"\"\"Initialize a CSR with the given private key.\n\n    :param privkey: Key to include in the CSR\n    :type privkey: :class:`certbot.util.Key`\n    :param set names: `str` names to include in the CSR\n    :param str path: Optional certificate save directory.\n    :param bool must_staple: If true, include the TLS Feature extension \"OCSP Must-Staple\"\n    :param bool strict_permissions: If true and path exists, an exception is raised if\n        the directory doesn't have 0755 permissions or isn't owned by the current user.\n\n    :returns: CSR\n    :rtype: :class:`certbot.util.CSR`\n\n    \"\"\"\n    csr_pem = acme_crypto_util.make_csr(\n        privkey.pem, names, must_staple=must_staple, ipaddrs=ipaddrs)\n\n    # Save CSR, if requested\n    csr_filename = None\n    if path:\n        util.make_or_verify_dir(path, 0o755, strict_permissions)\n        csr_f, csr_filename = util.unique_file(\n            os.path.join(path, \"csr-certbot.pem\"), 0o644, \"wb\")\n        with csr_f:\n            csr_f.write(csr_pem)\n        logger.debug(\"Creating CSR: %s\", csr_filename)\n\n    return util.CSR(csr_filename, csr_pem, \"pem\")\n\n\n# WARNING: the csr and private key file are possible attack vectors for TOCTOU\n# We should either...\n# A. Do more checks to verify that the CSR is trusted/valid\n# B. Audit the parsing code for vulnerabilities\n\ndef valid_csr(csr: bytes) -> bool:\n    \"\"\"Validate CSR.\n\n    Check if `csr` is a valid CSR with a correct self-signed signature.\n\n    :param bytes csr: CSR in PEM.\n\n    :returns: Validity of CSR.\n    :rtype: bool\n\n    \"\"\"\n    try:\n        req = x509.load_pem_x509_csr(csr)\n        return req.is_signature_valid\n    except (ValueError, TypeError):\n        logger.debug(\"\", exc_info=True)\n        return False\n\n\ndef csr_matches_pubkey(csr: bytes, privkey: bytes) -> bool:\n    \"\"\"Does private key correspond to the subject public key in the CSR?\n\n    :param bytes csr: CSR in PEM.\n    :param bytes privkey: Private key file contents (PEM)\n\n    :returns: Correspondence of private key to CSR subject public key.\n    :rtype: bool\n\n    \"\"\"\n    req = x509.load_pem_x509_csr(csr)\n    pkey = serialization.load_pem_private_key(privkey, password=None)\n    return req.is_signature_valid and req.public_key() == pkey.public_key()\n\n\ndef read_csr_file(\n    csrfile: str, data: bytes\n) -> util.CSR:\n    \"\"\"Reads a CSR file, which can be either PEM or DER, and returns a\n    `certbot.util.CSR` object.\n\n    :param str csrfile: CSR filename\n    :param bytes data: contents of the CSR file\n\n    :returns: object representing the CSR\n    :rtype: util.CSR\n\n    \"\"\"\n    try:\n        # Try to parse as DER first, then fall back to PEM.\n        csr = x509.load_der_x509_csr(data)\n    except ValueError:\n        try:\n            csr = x509.load_pem_x509_csr(data)\n        except ValueError:\n            raise errors.Error(\"Failed to parse CSR file: {0}\".format(csrfile))\n\n    # Internally we always use PEM, so re-encode as PEM before returning.\n    data_pem = csr.public_bytes(serialization.Encoding.PEM)\n    return util.CSR(file=csrfile, data=data_pem, form=\"pem\")\n\n\ndef import_csr_file(\n    csrfile: str, data: bytes\n) -> tuple['acme_crypto_util.Format', util.CSR, list[str]]:\n    \"\"\"Import a CSR file, which can be either PEM or DER.\n\n    :param str csrfile: CSR filename\n    :param bytes data: contents of the CSR file\n\n    :returns: (`acme_crypto_util.Format.PEM`,\n               util.CSR object representing the CSR,\n               list of domains requested in the CSR)\n    :rtype: tuple\n\n    \"\"\"\n    warnings.warn(\"certbot.crypto_util.import_csr_file is deprecated and \"\n        \"will be removed in the next major release. Please use \"\n        \"certbot.crypto_util.read_csr_file instead.\", DeprecationWarning)\n    try:\n        # Try to parse as DER first, then fall back to PEM.\n        csr = x509.load_der_x509_csr(data)\n    except ValueError:\n        try:\n            csr = x509.load_pem_x509_csr(data)\n        except ValueError:\n            raise errors.Error(\"Failed to parse CSR file: {0}\".format(csrfile))\n\n    domains = acme_crypto_util.get_names_from_subject_and_extensions(csr.subject, csr.extensions)\n    # Internally we always use PEM, so re-encode as PEM before returning.\n    data_pem = csr.public_bytes(serialization.Encoding.PEM)\n    with warnings.catch_warnings():\n        warnings.filterwarnings(\"ignore\", \"acme.crypto_util.Format is deprecated\")\n        return (\n            acme_crypto_util.Format.PEM,\n            util.CSR(file=csrfile, data=data_pem, form=\"pem\"),\n            domains,\n        )\n\n\ndef make_key(bits: int = 2048, key_type: str = \"rsa\",\n             elliptic_curve: Optional[str] = None) -> bytes:\n    \"\"\"Generate PEM encoded RSA|EC key.\n\n    :param int bits: Number of bits if key_type=rsa. At least 2048 for RSA.\n    :param str key_type: The type of key to generate, but be rsa or ecdsa\n    :param str elliptic_curve: The elliptic curve to use.\n\n    :returns: new RSA or ECDSA key in PEM form with specified number of bits\n              or of type ec_curve when key_type ecdsa is used.\n    :rtype: bytes\n\n    \"\"\"\n    key: Union[rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey]\n    if key_type == 'rsa':\n        if bits < 2048:\n            raise errors.Error(\"Unsupported RSA key length: {}\".format(bits))\n\n        key = rsa.generate_private_key(public_exponent=65537, key_size=bits)\n    elif key_type == 'ecdsa':\n        if not elliptic_curve:\n            raise errors.Error(\"When key_type == ecdsa, elliptic_curve must be set.\")\n        try:\n            name = elliptic_curve.upper()\n            if name in ('SECP256R1', 'SECP384R1', 'SECP521R1'):\n                curve = getattr(ec, elliptic_curve.upper())\n                if not curve:\n                    raise errors.Error(f\"Invalid curve type: {elliptic_curve}\")\n                key = ec.generate_private_key(\n                    curve=curve(),\n                    backend=default_backend()\n                )\n            else:\n                raise errors.Error(\"Unsupported elliptic curve: {}\".format(elliptic_curve))\n        except TypeError:\n            raise errors.Error(\"Unsupported elliptic curve: {}\".format(elliptic_curve))\n        except UnsupportedAlgorithm as e:\n            raise e from errors.Error(str(e))\n    else:\n        raise errors.Error(\"Invalid key_type specified: {}.  Use [rsa|ecdsa]\".format(key_type))\n    return key.private_bytes(\n        encoding=Encoding.PEM,\n        format=PrivateFormat.PKCS8,\n        encryption_algorithm=NoEncryption()\n    )\n\n\ndef valid_privkey(privkey: Union[str, bytes]) -> bool:\n    \"\"\"Is valid RSA private key?\n\n    :param privkey: Private key file contents in PEM\n\n    :returns: Validity of private key.\n    :rtype: bool\n\n    \"\"\"\n    if isinstance(privkey, str):\n        privkey = privkey.encode()\n    try:\n        serialization.load_pem_private_key(privkey, password=None)\n    except ValueError:\n        return False\n    else:\n        return True\n\n\ndef verify_renewable_cert(renewable_cert: interfaces.RenewableCert) -> None:\n    \"\"\"For checking that your certs were not corrupted on disk.\n\n    Several things are checked:\n        1. Signature verification for the cert.\n        2. That fullchain matches cert and chain when concatenated.\n        3. Check that the private key matches the certificate.\n\n    :param renewable_cert: cert to verify\n    :type renewable_cert: certbot.interfaces.RenewableCert\n\n    :raises errors.Error: If verification fails.\n    \"\"\"\n    verify_renewable_cert_sig(renewable_cert)\n    verify_fullchain(renewable_cert)\n    verify_cert_matches_priv_key(renewable_cert.cert_path, renewable_cert.key_path)\n\n\ndef verify_renewable_cert_sig(renewable_cert: interfaces.RenewableCert) -> None:\n    \"\"\"Verifies the signature of a RenewableCert object.\n\n    :param renewable_cert: cert to verify\n    :type renewable_cert: certbot.interfaces.RenewableCert\n\n    :raises errors.Error: If signature verification fails.\n    \"\"\"\n    try:\n        with open(renewable_cert.chain_path, 'rb') as chain_file:\n            chain = x509.load_pem_x509_certificate(chain_file.read(), default_backend())\n        with open(renewable_cert.cert_path, 'rb') as cert_file:\n            cert = x509.load_pem_x509_certificate(cert_file.read(), default_backend())\n        pk = chain.public_key()\n        assert cert.signature_hash_algorithm # always present for RSA and ECDSA\n        verify_signed_payload(pk, cert.signature, cert.tbs_certificate_bytes,\n                                cert.signature_hash_algorithm)\n    except (OSError, ValueError, InvalidSignature) as e:\n        error_str = \"verifying the signature of the certificate located at {0} has failed. \\\n                Details: {1}\".format(renewable_cert.cert_path, e)\n        logger.exception(error_str)\n        raise errors.Error(error_str)\n\n\ndef verify_signed_payload(public_key: Union[DSAPublicKey, 'Ed25519PublicKey', 'Ed448PublicKey',\n                                            EllipticCurvePublicKey, RSAPublicKey,\n                                            'X25519PublicKey', 'X448PublicKey'],\n                          signature: bytes, payload: bytes,\n                          signature_hash_algorithm: hashes.HashAlgorithm) -> None:\n    \"\"\"Check the signature of a payload.\n\n    :param RSAPublicKey/EllipticCurvePublicKey public_key: the public_key to check signature\n    :param bytes signature: the signature bytes\n    :param bytes payload: the payload bytes\n    :param hashes.HashAlgorithm signature_hash_algorithm: algorithm used to hash the payload\n\n    :raises InvalidSignature: If signature verification fails.\n    :raises errors.Error: If public key type is not supported\n    \"\"\"\n    if isinstance(public_key, RSAPublicKey):\n        public_key.verify(\n            signature, payload, PKCS1v15(), signature_hash_algorithm\n        )\n    elif isinstance(public_key, EllipticCurvePublicKey):\n        public_key.verify(\n            signature, payload, ECDSA(signature_hash_algorithm)\n        )\n    else:\n        raise errors.Error(\"Unsupported public key type.\")\n\n\ndef verify_cert_matches_priv_key(cert_path: str, key_path: str) -> None:\n    \"\"\" Verifies that the private key and cert match.\n\n    :param str cert_path: path to a cert in PEM format\n    :param str key_path: path to a private key file\n\n    :raises errors.Error: If they don't match.\n    \"\"\"\n    try:\n        context = SSL.Context(SSL.TLS_METHOD)\n        context.use_certificate_file(cert_path)\n        context.use_privatekey_file(key_path)\n        context.check_privatekey()\n    except (OSError, SSL.Error) as e:\n        error_str = \"verifying the certificate located at {0} matches the \\\n                private key located at {1} has failed. \\\n                Details: {2}\".format(cert_path,\n                        key_path, e)\n        logger.exception(error_str)\n        raise errors.Error(error_str)\n\n\ndef verify_fullchain(renewable_cert: interfaces.RenewableCert) -> None:\n    \"\"\" Verifies that fullchain is indeed cert concatenated with chain.\n\n    :param renewable_cert: cert to verify\n    :type renewable_cert: certbot.interfaces.RenewableCert\n\n    :raises errors.Error: If cert and chain do not combine to fullchain.\n    \"\"\"\n    try:\n        with open(renewable_cert.chain_path) as chain_file:\n            chain = chain_file.read()\n        with open(renewable_cert.cert_path) as cert_file:\n            cert = cert_file.read()\n        with open(renewable_cert.fullchain_path) as fullchain_file:\n            fullchain = fullchain_file.read()\n        if (cert + chain) != fullchain:\n            error_str = \"fullchain does not match cert + chain for {0}!\"\n            error_str = error_str.format(renewable_cert.lineagename)\n            raise errors.Error(error_str)\n    except OSError as e:\n        error_str = \"reading one of cert, chain, or fullchain has failed: {0}\".format(e)\n        logger.exception(error_str)\n        raise errors.Error(error_str)\n    except errors.Error as e:\n        raise e\n\n\ndef get_sans_from_cert(\n    cert: bytes, typ: 'acme_crypto_util.Format | int | None' = None\n) -> list[str]:\n    \"\"\"Get a list of Subject Alternative Names from a certificate.\n\n    :param str cert: Certificate (encoded).\n    :param Format typ: Which format the `cert` bytes are in.\n\n    :returns: A list of Subject Alternative Names.\n    :rtype: list\n\n    \"\"\"\n    warnings.warn(\"get_sans_from_cert is deprecated and will be removed in the next \"\n        \"major release.\", DeprecationWarning)\n    with warnings.catch_warnings():\n        warnings.filterwarnings(\"ignore\", \"acme.crypto_util.Format is deprecated\")\n        if typ is None:\n            typ = acme_crypto_util.Format.PEM\n        typ = acme_crypto_util.Format(typ)\n        if typ == acme_crypto_util.Format.PEM:\n            x509_cert = x509.load_pem_x509_certificate(cert)\n        else:\n            assert typ == acme_crypto_util.Format.DER\n            x509_cert = x509.load_der_x509_certificate(cert)\n\n    try:\n        san_ext = x509_cert.extensions.get_extension_for_class(\n            x509.SubjectAlternativeName\n        )\n    except x509.ExtensionNotFound:\n        return []\n\n    return san_ext.value.get_values_for_type(x509.DNSName)\n\n\ndef get_names_from_cert(\n    cert: bytes, typ: 'acme_crypto_util.Format | int | None' = None\n) -> list[str]:\n    \"\"\"Get a list of domains from a cert, including the CN if it is set.\n\n    :param str cert: Certificate (encoded).\n    :param Format typ: Which format the `cert` bytes are in.\n\n    :returns: A list of domain names.\n    :rtype: list\n\n    \"\"\"\n    warnings.warn(\"get_names_from_cert is deprecated and will be removed in the next \"\n        \"major release.\", DeprecationWarning)\n    with warnings.catch_warnings():\n        warnings.filterwarnings(\"ignore\", \"acme.crypto_util.Format is deprecated\")\n        if typ is None:\n            typ = acme_crypto_util.Format.PEM\n        typ = acme_crypto_util.Format(typ)\n        if typ == acme_crypto_util.Format.PEM:\n            x509_cert = x509.load_pem_x509_certificate(cert)\n        else:\n            assert typ == acme_crypto_util.Format.DER\n            x509_cert = x509.load_der_x509_certificate(cert)\n    return acme_crypto_util.get_names_from_subject_and_extensions(\n        x509_cert.subject, x509_cert.extensions\n    )\n\n\ndef get_names_from_req(\n    csr: bytes, typ: 'acme_crypto_util.Format | int | None' = None\n) -> list[str]:\n    \"\"\"Get a list of domains from a CSR, including the CN if it is set.\n\n    :param str csr: CSR (encoded).\n    :param acme_crypto_util.Format typ: Which format the `csr` bytes are in.\n    :returns: A list of domain names.\n    :rtype: list\n\n    \"\"\"\n    warnings.warn(\"get_names_from_req is deprecated and will be removed in the next \"\n        \"major release.\", DeprecationWarning)\n    with warnings.catch_warnings():\n        warnings.filterwarnings(\"ignore\", \"acme.crypto_util.Format is deprecated\")\n        if typ is None:\n            typ = acme_crypto_util.Format.PEM\n        typ = acme_crypto_util.Format(typ)\n        if typ == acme_crypto_util.Format.PEM:\n            x509_req = x509.load_pem_x509_csr(csr)\n        else:\n            assert typ == acme_crypto_util.Format.DER\n            x509_req = x509.load_der_x509_csr(csr)\n    return acme_crypto_util.get_names_from_subject_and_extensions(\n        x509_req.subject, x509_req.extensions\n    )\n\n\ndef notBefore(cert_path: str) -> datetime.datetime:\n    \"\"\"When does the cert at cert_path start being valid?\n\n    :param str cert_path: path to a cert in PEM format\n\n    :returns: the notBefore value from the cert at cert_path\n    :rtype: :class:`datetime.datetime`\n\n    \"\"\"\n    with open(cert_path, \"rb\") as f:\n        cert = x509.load_pem_x509_certificate(f.read())\n    return cert.not_valid_before_utc\n\n\ndef notAfter(cert_path: str) -> datetime.datetime:\n    \"\"\"When does the cert at cert_path stop being valid?\n\n    :param str cert_path: path to a cert in PEM format\n\n    :returns: the notAfter value from the cert at cert_path\n    :rtype: :class:`datetime.datetime`\n\n    \"\"\"\n    with open(cert_path, \"rb\") as f:\n        cert = x509.load_pem_x509_certificate(f.read())\n    return cert.not_valid_after_utc\n\n\ndef sha256sum(filename: str) -> str:\n    \"\"\"Compute a sha256sum of a file.\n\n    NB: In given file, platform specific newlines characters will be converted\n    into their equivalent unicode counterparts before calculating the hash.\n\n    :param str filename: path to the file whose hash will be computed\n\n    :returns: sha256 digest of the file in hexadecimal\n    :rtype: str\n    \"\"\"\n    sha256 = hashlib.sha256()\n    with open(filename, 'r') as file_d:\n        sha256.update(file_d.read().encode('UTF-8'))\n    return sha256.hexdigest()\n\n# Finds one CERTIFICATE stricttextualmsg according to rfc7468#section-3.\n# Does not validate the base64text - use crypto.load_certificate.\nCERT_PEM_REGEX = re.compile(\n    b\"\"\"-----BEGIN CERTIFICATE-----\\r?\n.+?\\r?\n-----END CERTIFICATE-----\\r?\n\"\"\",\n    re.DOTALL # DOTALL (/s) because the base64text may include newlines\n)\n\n\ndef cert_and_chain_from_fullchain(fullchain_pem: str) -> tuple[str, str]:\n    \"\"\"Split fullchain_pem into cert_pem and chain_pem\n\n    :param str fullchain_pem: concatenated cert + chain\n\n    :returns: tuple of string cert_pem and chain_pem\n    :rtype: tuple\n\n    :raises errors.Error: If there are less than 2 certificates in the chain.\n\n    \"\"\"\n    # First pass: find the boundary of each certificate in the chain.\n    # TODO: This will silently skip over any \"explanatory text\" in between boundaries,\n    # which is prohibited by RFC8555.\n    certs = CERT_PEM_REGEX.findall(fullchain_pem.encode())\n    if len(certs) < 2:\n        raise errors.Error(\"failed to parse fullchain into cert and chain: \" +\n                           \"less than 2 certificates in chain\")\n\n    # Second pass: for each certificate found, parse it using cryptography and re-encode it,\n    # with the effect of normalizing any encoding variations (e.g. CRLF, whitespace).\n    certs_normalized: list[str] = []\n    for cert_pem in certs:\n        cert = x509.load_pem_x509_certificate(cert_pem)\n        cert_pem = cert.public_bytes(Encoding.PEM)\n        certs_normalized.append(cert_pem.decode())\n\n    # Since each normalized cert has a newline suffix, no extra newlines are required.\n    return (certs_normalized[0], \"\".join(certs_normalized[1:]))\n\n\ndef get_serial_from_cert(cert_path: str) -> int:\n    \"\"\"Retrieve the serial number of a certificate from certificate path\n\n    :param str cert_path: path to a cert in PEM format\n\n    :returns: serial number of the certificate\n    :rtype: int\n    \"\"\"\n    with open(cert_path, \"rb\") as f:\n        cert = x509.load_pem_x509_certificate(f.read())\n    return cert.serial_number\n\n\ndef find_chain_with_issuer(fullchains: list[str], issuer_cn: str,\n                           warn_on_no_match: bool = False) -> str:\n    \"\"\"Chooses the first certificate chain from fullchains whose topmost\n    intermediate has an Issuer Common Name matching issuer_cn (in other words\n    the first chain which chains to a root whose name matches issuer_cn).\n\n    :param fullchains: The list of fullchains in PEM chain format.\n    :type fullchains: `list` of `str`\n    :param `str` issuer_cn: The exact Subject Common Name to match against any\n        issuer in the certificate chain.\n\n    :returns: The best-matching fullchain, PEM-encoded, or the first if none match.\n    :rtype: `str`\n    \"\"\"\n    for chain in fullchains:\n        certs = CERT_PEM_REGEX.findall(chain.encode())\n        top_cert = x509.load_pem_x509_certificate(certs[-1], default_backend())\n        top_issuer_cn = top_cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)\n        if top_issuer_cn and top_issuer_cn[0].value == issuer_cn:\n            return chain\n\n    # Nothing matched, return whatever was first in the list.\n    if warn_on_no_match:\n        logger.warning(\"Certbot has been configured to prefer certificate chains with \"\n                    \"issuer '%s', but no chain from the CA matched this issuer. Using \"\n                    \"the default certificate chain instead.\", issuer_cn)\n    return fullchains[0]\n"
  },
  {
    "path": "certbot/src/certbot/display/__init__.py",
    "content": "\"\"\"Certbot display utilities.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/display/ops.py",
    "content": "\"\"\"Contains UI methods for LE user operations.\"\"\"\nimport logging\nfrom textwrap import indent\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import util\nfrom certbot._internal import account\nfrom certbot._internal.display import util as internal_display_util\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_email(invalid: bool = False, **kwargs: Any) -> str:\n    \"\"\"Prompt for valid email address.\n\n    :param bool invalid: True if an invalid address was provided by the user\n\n    :returns: e-mail address\n    :rtype: str\n\n    :raises errors.Error: if the user cancels\n\n    \"\"\"\n    # pylint: disable=unused-argument\n    invalid_prefix = \"\"\n    if invalid:\n        invalid_prefix = \"The server reported a problem with your email address. \"\n    msg = \"Enter email address or hit Enter to skip.\\n\"\n\n    while True:\n        code, email = display_util.input_text(invalid_prefix + msg, default=\"\")\n\n        if code != display_util.OK:\n            raise errors.Error(\"Error getting email address.\")\n        if email == \"\":\n            return \"\"\n        if util.safe_email(email):\n            return email\n        invalid_prefix = \"There is a problem with your email address. \"\n\n\ndef choose_account(accounts: list[account.Account]) -> Optional[account.Account]:\n    \"\"\"Choose an account.\n\n    :param list accounts: Containing at least one\n        :class:`~certbot._internal.account.Account`\n\n    \"\"\"\n    # Note this will get more complicated once we start recording authorizations\n    labels = [acc.slug for acc in accounts]\n\n    code, index = display_util.menu(\"Please choose an account\", labels, force_interactive=True)\n    if code == display_util.OK:\n        return accounts[index]\n    return None\n\n\ndef choose_values(values: list[str], question: Optional[str] = None) -> list[str]:\n    \"\"\"Display screen to let user pick one or multiple values from the provided\n    list.\n\n    :param list values: Values to select from\n    :param str question: Question to ask to user while choosing values\n\n    :returns: List of selected values\n    :rtype: list\n    \"\"\"\n    code, items = display_util.checklist(question if question else \"\", tags=values,\n                                         force_interactive=True)\n    if code == display_util.OK and items:\n        return items\n    return []\n\n\ndef choose_names(installer: Optional[interfaces.Installer],\n                 question: Optional[str] = None) -> list[str]:\n    \"\"\"Display screen to select domains to validate.\n\n    :param installer: An installer object\n    :type installer: :class:`certbot.interfaces.Installer`\n\n    :param `str` question: Overriding default question to ask the user if asked\n        to choose from domain names.\n\n    :returns: List of selected names\n    :rtype: `list` of `str`\n\n    \"\"\"\n    if installer is None:\n        logger.debug(\"No installer, picking names manually\")\n        return _choose_names_manually()\n\n    domains = list(installer.get_all_names())\n    names = get_valid_domains(domains)\n\n    if not names:\n        return _choose_names_manually()\n\n    code, names = _filter_names(names, question)\n    if code == display_util.OK and names:\n        return names\n    return []\n\n\ndef get_valid_domains(domains: Iterable[str]) -> list[str]:\n    \"\"\"Helper method for choose_names that implements basic checks\n     on domain names\n\n    :param list domains: Domain names to validate\n    :return: List of valid domains\n    :rtype: list\n    \"\"\"\n    valid_domains: list[str] = []\n    for domain in domains:\n        try:\n            valid_domains.append(util.enforce_domain_sanity(domain))\n        except errors.ConfigurationError:\n            continue\n    return valid_domains\n\n\ndef _sort_names(FQDNs: Iterable[str]) -> list[str]:\n    \"\"\"Sort FQDNs by SLD (and if many, by their subdomains)\n\n    :param list FQDNs: list of domain names\n\n    :returns: Sorted list of domain names\n    :rtype: list\n    \"\"\"\n    return sorted(FQDNs, key=lambda fqdn: fqdn.split('.')[::-1][1:])\n\n\ndef _filter_names(names: Iterable[str],\n                  override_question: Optional[str] = None) -> tuple[str, list[str]]:\n    \"\"\"Determine which names the user would like to select from a list.\n\n    :param list names: domain names\n\n    :returns: tuple of the form (`code`, `names`) where\n        `code` - str display exit code\n        `names` - list of names selected\n    :rtype: tuple\n\n    \"\"\"\n    # Sort by domain first, and then by subdomain\n    sorted_names = _sort_names(names)\n    if override_question:\n        question = override_question\n    else:\n        question = (\n            \"Which names would you like to activate HTTPS for?\\n\"\n            \"We recommend selecting either all domains, or all domains in a VirtualHost/server \"\n            \"block.\")\n    code, names = display_util.checklist(\n        question, tags=sorted_names, cli_flag=\"--domains\", force_interactive=True)\n    return code, [str(s) for s in names]\n\n\ndef _choose_names_manually(prompt_prefix: str = \"\") -> list[str]:\n    \"\"\"Manually input names for those without an installer.\n\n    :param str prompt_prefix: string to prepend to prompt for domains\n\n    :returns: list of provided names\n    :rtype: `list` of `str`\n\n    \"\"\"\n    code, input_ = display_util.input_text(\n        prompt_prefix +\n        \"Please enter the domain name(s) you would like on your certificate \"\n        \"(comma and/or space separated)\",\n        cli_flag=\"--domains\", force_interactive=True)\n\n    if code == display_util.OK:\n        invalid_domains = {}\n        retry_message = \"\"\n        try:\n            domain_list = internal_display_util.separate_list_input(input_)\n        except UnicodeEncodeError:\n            domain_list = []\n            retry_message = (\n                \"Internationalized domain names are not presently \"\n                \"supported.{0}{0}Would you like to re-enter the \"\n                \"names?{0}\").format(os.linesep)\n\n        for i, domain in enumerate(domain_list):\n            try:\n                domain_list[i] = util.enforce_domain_sanity(domain)\n            except errors.ConfigurationError as e:\n                invalid_domains[domain] = str(e)\n\n        if invalid_domains:\n            retry_message = (\n                \"One or more of the entered domain names was not valid:\"\n                \"{0}{0}\").format(os.linesep)\n            for invalid_domain, err in invalid_domains.items():\n                retry_message = retry_message + \"{1}: {2}{0}\".format(\n                    os.linesep, invalid_domain, err)\n            retry_message = retry_message + (\n                \"{0}Would you like to re-enter the names?{0}\").format(\n                    os.linesep)\n\n        if retry_message:\n            # We had error in input\n            retry = display_util.yesno(retry_message, force_interactive=True)\n            if retry:\n                return _choose_names_manually()\n        else:\n            return domain_list\n    return []\n\n\ndef success_installation(domains: list[str]) -> None:\n    \"\"\"Display a box confirming the installation of HTTPS.\n\n    :param list domains: domain names which were enabled\n\n    \"\"\"\n    display_util.notify(\n        \"Congratulations! You have successfully enabled HTTPS on {0}\"\n        .format(_gen_https_names(domains))\n    )\n\n\ndef success_renewal(unused_domains: list[str]) -> None:\n    \"\"\"Display a box confirming the renewal of an existing certificate.\n\n    :param list domains: domain names which were renewed\n\n    \"\"\"\n    display_util.notify(\n        \"Your existing certificate has been successfully renewed, and the \"\n        \"new certificate has been installed.\"\n    )\n\n\ndef success_revocation(cert_path: str) -> None:\n    \"\"\"Display a message confirming a certificate has been revoked.\n\n    :param list cert_path: path to certificate which was revoked.\n\n    \"\"\"\n    display_util.notify(\n        \"Congratulations! You have successfully revoked the certificate \"\n        \"that was located at {0}.\".format(cert_path)\n    )\n\n\ndef report_executed_command(command_name: str, returncode: int, stdout: str, stderr: str) -> None:\n    \"\"\"Display a message describing the success or failure of an executed process (e.g. hook).\n\n    :param str command_name: Human-readable description of the executed command\n    :param int returncode: The exit code of the executed command\n    :param str stdout: The stdout output of the executed command\n    :param str stderr: The stderr output of the executed command\n\n    \"\"\"\n    out_s, err_s = stdout.strip(), stderr.strip()\n    if returncode != 0:\n        logger.warning(\"%s reported error code %d\", command_name, returncode)\n    if out_s:\n        display_util.notify(f\"{command_name} ran with output:\\n{indent(out_s, ' ')}\")\n    if err_s:\n        logger.warning(\"%s ran with error output:\\n%s\", command_name, indent(err_s, ' '))\n\n\ndef _gen_https_names(domains: list[str]) -> str:\n    \"\"\"Returns a string of the https domains.\n\n    Domains are formatted nicely with ``https://`` prepended to each.\n\n    :param list domains: Each domain is a 'str'\n\n    \"\"\"\n    if len(domains) == 1:\n        return \"https://{0}\".format(domains[0])\n    elif len(domains) == 2:\n        return \"https://{dom[0]} and https://{dom[1]}\".format(dom=domains)\n    elif len(domains) > 2:\n        return \"{0}{1}{2}\".format(\n            \", \".join(\"https://%s\" % dom for dom in domains[:-1]),\n            \", and https://\",\n            domains[-1])\n\n    return \"\"\n\n\ndef _get_validated(method: Callable[..., tuple[str, str]],\n                   validator: Callable[[str], Any], message: str,\n                   default: Optional[str] = None, **kwargs: Any) -> tuple[str, str]:\n    if default is not None:\n        try:\n            validator(default)\n        except errors.Error:\n            logger.debug('Encountered invalid default value \"%s\" when prompting for \"%s\"',\n                         default,\n                         message,\n                         exc_info=True)\n            raise AssertionError('Invalid default \"{0}\"'.format(default))\n\n    while True:\n        code, raw = method(message, default=default, **kwargs)\n        if code == display_util.OK:\n            try:\n                validator(raw)\n                return code, raw\n            except errors.Error as error:\n                logger.debug('Validator rejected \"%s\" when prompting for \"%s\"',\n                             raw,\n                             message,\n                             exc_info=True)\n                display_util.notification(str(error), pause=False)\n        else:\n            return code, raw\n\n\ndef validated_input(validator: Callable[[str], Any],\n                    *args: Any, **kwargs: Any) -> tuple[str, str]:\n    \"\"\"Like `~certbot.display.util.input_text`, but with validation.\n\n    :param callable validator: A method which will be called on the\n        supplied input. If the method raises an `errors.Error`, its\n        text will be displayed and the user will be re-prompted.\n    :param list `*args`: Arguments to be passed to `~certbot.display.util.input_text`.\n    :param dict `**kwargs`: Arguments to be passed to `~certbot.display.util.input_text`.\n    :return: as `~certbot.display.util.input_text`\n    :rtype: tuple\n    \"\"\"\n    return _get_validated(display_util.input_text, validator, *args, **kwargs)\n\n\ndef validated_directory(validator: Callable[[str], Any],\n                        *args: Any, **kwargs: Any) -> tuple[str, str]:\n    \"\"\"Like `~certbot.display.util.directory_select`, but with validation.\n\n    :param callable validator: A method which will be called on the\n        supplied input. If the method raises an `errors.Error`, its\n        text will be displayed and the user will be re-prompted.\n    :param list `*args`: Arguments to be passed to `~certbot.display.util.directory_select`.\n    :param dict `**kwargs`: Arguments to be passed to\n        `~certbot.display.util.directory_select`.\n    :return: as `~certbot.display.util.directory_select`\n    :rtype: tuple\n    \"\"\"\n    return _get_validated(display_util.directory_select, validator, *args, **kwargs)\n"
  },
  {
    "path": "certbot/src/certbot/display/util.py",
    "content": "\"\"\"Certbot display.\n\nThis module (`certbot.display.util`) or its companion `certbot.display.ops`\nshould be used whenever:\n\n- Displaying status information to the user on the terminal\n- Collecting information from the user via prompts\n\nOther messages can use the `logging` module. See `log.py`.\n\n\"\"\"\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot._internal.display import obj\n\n# These constants are defined this way to make them easier to document with\n# Sphinx and to not couple our public docstrings to our internal ones.\nOK = obj.OK\n\"\"\"Display exit code indicating user acceptance.\"\"\"\n\nCANCEL = obj.CANCEL\n\"\"\"Display exit code for a user canceling the display.\"\"\"\n\nWIDTH = 72\n\ndef notify(msg: str) -> None:\n    \"\"\"Display a basic status message.\n\n    :param str msg: message to display\n\n    \"\"\"\n    obj.get_display().notification(msg, pause=False, decorate=False, wrap=False)\n\n\ndef notification(message: str, pause: bool = True, wrap: bool = True,\n                 force_interactive: bool = False, decorate: bool = True) -> None:\n    \"\"\"Displays a notification and waits for user acceptance.\n\n    :param str message: Message to display\n    :param bool pause: Whether or not the program should pause for the\n        user's confirmation\n    :param bool wrap: Whether or not the application should wrap text\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n    :param bool decorate: Whether to surround the message with a\n        decorated frame\n\n    \"\"\"\n    obj.get_display().notification(message, pause=pause, wrap=wrap,\n                                   force_interactive=force_interactive, decorate=decorate)\n\n\ndef menu(message: str, choices: Union[list[str], list[tuple[str, str]]],\n         default: Optional[int] = None, cli_flag: Optional[str] = None,\n         force_interactive: bool = False) -> tuple[str, int]:\n    \"\"\"Display a menu.\n\n    .. todo:: This doesn't enable the help label/button (I wasn't sold on\n        any interface I came up with for this). It would be a nice feature.\n\n    :param str message: title of menu\n    :param choices: Menu lines, len must be > 0\n    :type choices: list of tuples (tag, item) or\n        list of descriptions (tags will be enumerated)\n    :param default: default value to return, if interaction is not possible\n    :param str cli_flag: option used to set this value with the CLI\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n\n    :returns: tuple of (`code`, `index`) where\n        `code` - str display exit code\n        `index` - int index of the user's selection\n\n    :rtype: tuple\n\n    \"\"\"\n    return obj.get_display().menu(message, choices, default=default, cli_flag=cli_flag,\n                                  force_interactive=force_interactive)\n\n\ndef input_text(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,\n               force_interactive: bool = False) -> tuple[str, str]:\n    \"\"\"Accept input from the user.\n\n    :param str message: message to display to the user\n    :param default: default value to return, if interaction is not possible\n    :param str cli_flag: option used to set this value with the CLI\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n\n    :returns: tuple of (`code`, `input`) where\n        `code` - str display exit code\n        `input` - str of the user's input\n    :rtype: tuple\n\n    \"\"\"\n    return obj.get_display().input(message, default=default, cli_flag=cli_flag,\n                                   force_interactive=force_interactive)\n\n\ndef yesno(message: str, yes_label: str = \"Yes\", no_label: str = \"No\",\n          default: Optional[bool] = None, cli_flag: Optional[str] = None,\n          force_interactive: bool = False) -> bool:\n    \"\"\"Query the user with a yes/no question.\n\n    Yes and No label must begin with different letters, and must contain at\n    least one letter each.\n\n    :param str message: question for the user\n    :param str yes_label: Label of the \"Yes\" parameter\n    :param str no_label: Label of the \"No\" parameter\n    :param default: default value to return, if interaction is not possible\n    :param str cli_flag: option used to set this value with the CLI\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n\n    :returns: True for \"Yes\", False for \"No\"\n    :rtype: bool\n\n    \"\"\"\n    return obj.get_display().yesno(message, yes_label=yes_label, no_label=no_label, default=default,\n                                   cli_flag=cli_flag, force_interactive=force_interactive)\n\n\ndef checklist(message: str, tags: list[str], default: Optional[list[str]] = None,\n              cli_flag: Optional[str] = None,\n              force_interactive: bool = False) -> tuple[str, list[str]]:\n    \"\"\"Display a checklist.\n\n    :param str message: Message to display to user\n    :param list tags: `str` tags to select, len(tags) > 0\n    :param default: default value to return, if interaction is not possible\n    :param str cli_flag: option used to set this value with the CLI\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n\n    :returns: tuple of (`code`, `tags`) where\n        `code` - str display exit code\n        `tags` - list of selected tags\n    :rtype: tuple\n\n    \"\"\"\n    return obj.get_display().checklist(message, tags, default=default, cli_flag=cli_flag,\n                                       force_interactive=force_interactive)\n\n\ndef directory_select(message: str, default: Optional[str] = None, cli_flag: Optional[str] = None,\n                     force_interactive: bool = False) -> tuple[str, str]:\n    \"\"\"Display a directory selection screen.\n\n    :param str message: prompt to give the user\n    :param default: default value to return, if interaction is not possible\n    :param str cli_flag: option used to set this value with the CLI\n    :param bool force_interactive: True if it's safe to prompt the user\n        because it won't cause any workflow regressions\n\n    :returns: tuple of the form (`code`, `string`) where\n        `code` - display exit code\n        `string` - input entered by the user\n\n    \"\"\"\n    return obj.get_display().directory_select(message, default=default, cli_flag=cli_flag,\n                                              force_interactive=force_interactive)\n\n\ndef assert_valid_call(prompt: str, default: str, cli_flag: str, force_interactive: bool) -> None:\n    \"\"\"Verify that provided arguments is a valid display call.\n\n    :param str prompt: prompt for the user\n    :param default: default answer to prompt\n    :param str cli_flag: command line option for setting an answer\n        to this question\n    :param bool force_interactive: if interactivity is forced\n\n    \"\"\"\n    msg = \"Invalid display call for this prompt:\\n{0}\".format(prompt)\n    if cli_flag:\n        msg += (\"\\nYou can set an answer to \"\n                \"this prompt with the {0} flag\".format(cli_flag))\n    assert default is not None or force_interactive, msg\n"
  },
  {
    "path": "certbot/src/certbot/errors.py",
    "content": "\"\"\"Certbot client errors.\"\"\"\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    from certbot.achallenges import AnnotatedChallenge\n\n\nclass Error(Exception):\n    \"\"\"Generic Certbot client error.\"\"\"\n\n\nclass AccountStorageError(Error):\n    \"\"\"Generic `.AccountStorage` error.\"\"\"\n\n\nclass AccountNotFound(AccountStorageError):\n    \"\"\"Account not found error.\"\"\"\n\n\nclass ReverterError(Error):\n    \"\"\"Certbot Reverter error.\"\"\"\n\n\nclass SubprocessError(Error):\n    \"\"\"Subprocess handling error.\"\"\"\n\n\nclass CertStorageError(Error):\n    \"\"\"Generic `.CertStorage` error.\"\"\"\n\n\nclass HookCommandNotFound(Error):\n    \"\"\"Failed to find a hook command in the PATH.\"\"\"\n\n\nclass SignalExit(Error):\n    \"\"\"A Unix signal was received while in the ErrorHandler context manager.\"\"\"\n\nclass OverlappingMatchFound(Error):\n    \"\"\"Multiple lineages matched what should have been a unique result.\"\"\"\n\nclass LockError(Error):\n    \"\"\"File locking error.\"\"\"\n\n\n# Auth Handler Errors\nclass AuthorizationError(Error):\n    \"\"\"Authorization error.\"\"\"\n\n\nclass FailedChallenges(AuthorizationError):\n    \"\"\"Failed challenges error.\n\n    :ivar set failed_achalls: Failed `.AnnotatedChallenge` instances.\n\n    \"\"\"\n    def __init__(self, failed_achalls: set['AnnotatedChallenge']) -> None:\n        assert failed_achalls\n        self.failed_achalls = failed_achalls\n        super().__init__()\n\n    def __str__(self) -> str:\n        return \"Failed authorization procedure. {0}\".format(\n            \", \".join(\n                \"{0} ({1}): {2}\".format(achall.identifier.value, achall.typ, achall.error)\n                for achall in self.failed_achalls if achall.error is not None))\n\n\n# Plugin Errors\nclass PluginError(Error):\n    \"\"\"Certbot Plugin error.\"\"\"\n\n\nclass PluginEnhancementAlreadyPresent(Error):\n    \"\"\" Enhancement was already set \"\"\"\n\n\nclass PluginSelectionError(Error):\n    \"\"\"A problem with plugin/configurator selection or setup\"\"\"\n\n\nclass NoInstallationError(PluginError):\n    \"\"\"Certbot No Installation error.\"\"\"\n\n\nclass MisconfigurationError(PluginError):\n    \"\"\"Certbot Misconfiguration error.\"\"\"\n\n\nclass NotSupportedError(PluginError):\n    \"\"\"Certbot Plugin function not supported error.\"\"\"\n\n\nclass PluginStorageError(PluginError):\n    \"\"\"Certbot Plugin Storage error.\"\"\"\n\n\nclass StandaloneBindError(Error):\n    \"\"\"Standalone plugin bind error.\"\"\"\n\n    def __init__(self, socket_error: OSError, port: int) -> None:\n        super().__init__(\n            \"Problem binding to port {0}: {1}\".format(port, socket_error))\n        self.socket_error = socket_error\n        self.port = port\n\n\nclass ConfigurationError(Error):\n    \"\"\"Configuration sanity error.\"\"\"\n\n# NoninteractiveDisplay error:\n\nclass MissingCommandlineFlag(Error):\n    \"\"\"A command line argument was missing in noninteractive usage\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/interfaces.py",
    "content": "\"\"\"Certbot client interfaces.\"\"\"\nfrom abc import ABCMeta\nfrom abc import abstractmethod\nfrom argparse import ArgumentParser\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom acme.challenges import Challenge\nfrom acme.challenges import ChallengeResponse\nfrom acme.client import ClientV2\nfrom certbot import configuration\nfrom certbot.achallenges import AnnotatedChallenge\n\ntry:\n    from zope.interface import Interface as ZopeInterface\nexcept ImportError:\n    ZopeInterface = object\n\nif TYPE_CHECKING:\n    from certbot._internal.account import Account\n\n\nclass AccountStorage(metaclass=ABCMeta):\n    \"\"\"Accounts storage interface.\"\"\"\n\n    @abstractmethod\n    def find_all(self) -> list['Account']:  # pragma: no cover\n        \"\"\"Find all accounts.\n\n        :returns: All found accounts.\n        :rtype: list\n\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def load(self, account_id: str) -> 'Account':  # pragma: no cover\n        \"\"\"Load an account by its id.\n\n        :raises .AccountNotFound: if account could not be found\n        :raises .AccountStorageError: if account could not be loaded\n\n        :returns: The account loaded\n        :rtype: .Account\n\n        \"\"\"\n        raise NotImplementedError()\n\n    @abstractmethod\n    def save(self, account: 'Account', client: ClientV2) -> None:  # pragma: no cover\n        \"\"\"Save account.\n\n        :raises .AccountStorageError: if account could not be saved\n\n        \"\"\"\n        raise NotImplementedError()\n\n\nclass Plugin(metaclass=ABCMeta):\n    \"\"\"Certbot plugin.\n\n    Objects providing this interface will be called without satisfying\n    any entry point \"extras\" (extra dependencies) you might have defined\n    for your plugin, e.g (excerpt from ``setup.py`` script)::\n\n      setup(\n          ...\n          entry_points={\n              'certbot.plugins': [\n                  'name=example_project.plugin[plugin_deps]',\n              ],\n          },\n          extras_require={\n              'plugin_deps': ['dep1', 'dep2'],\n          }\n      )\n\n    Therefore, make sure such objects are importable and usable without\n    extras. This is necessary, because CLI does the following operations\n    (in order):\n\n      - loads an entry point,\n      - calls `inject_parser_options`,\n      - requires an entry point,\n      - creates plugin instance (`__call__`).\n\n    \"\"\"\n\n    description: str = NotImplemented\n    \"\"\"Short plugin description\"\"\"\n\n    name: str = NotImplemented\n    \"\"\"Unique name of the plugin\"\"\"\n\n    @abstractmethod\n    def __init__(self, config: Optional[configuration.NamespaceConfig], name: str) -> None:\n        \"\"\"Create a new `Plugin`.\n\n        :param configuration.NamespaceConfig config: Configuration.\n        :param str name: Unique plugin name.\n\n        \"\"\"\n        super().__init__()\n\n    @abstractmethod\n    def prepare(self) -> None:\n        \"\"\"Prepare the plugin.\n\n        Finish up any additional initialization.\n\n        :raises .PluginError:\n            when full initialization cannot be completed.\n        :raises .MisconfigurationError:\n            when full initialization cannot be completed. Plugin will\n            be displayed on a list of available plugins.\n        :raises .NoInstallationError:\n            when the necessary programs/files cannot be located. Plugin\n            will NOT be displayed on a list of available plugins.\n        :raises .NotSupportedError:\n            when the installation is recognized, but the version is not\n            currently supported.\n\n        \"\"\"\n\n    @abstractmethod\n    def more_info(self) -> str:\n        \"\"\"Human-readable string to help the user.\n\n        Should describe the steps taken and any relevant info to help the user\n        decide which plugin to use.\n\n        :rtype str:\n\n        \"\"\"\n\n    @classmethod\n    @abstractmethod\n    def inject_parser_options(cls, parser: ArgumentParser, name: str) -> None:\n        \"\"\"Inject argument parser options (flags).\n\n        1. Be nice and prepend all options and destinations with\n        `~.common.option_namespace` and `~common.dest_namespace`.\n\n        2. Inject options (flags) only. Positional arguments are not\n        allowed, as this would break the CLI.\n\n        :param ArgumentParser parser: (Almost) top-level CLI parser.\n        :param str name: Unique plugin name.\n\n        \"\"\"\n\n\nclass Authenticator(Plugin):\n    \"\"\"Generic Certbot Authenticator.\n\n    Class represents all possible tools processes that have the\n    ability to perform challenges and attain a certificate.\n\n    \"\"\"\n\n    @abstractmethod\n    def get_chall_pref(self, identifier: str) -> Iterable[type[Challenge]]:\n        \"\"\"Return `collections.Iterable` of challenge preferences.\n\n        :param str identifier: Domain or IP address for which challenge preferences are sought.\n\n        :returns: `collections.Iterable` of challenge types (subclasses of\n            :class:`acme.challenges.Challenge`) with the most\n            preferred challenges first. If a type is not specified, it means the\n            Authenticator cannot perform the challenge.\n        :rtype: `collections.Iterable`\n\n        \"\"\"\n\n    @abstractmethod\n    def perform(self, achalls: list[AnnotatedChallenge]) -> list[ChallengeResponse]:\n        \"\"\"Perform the given challenge.\n\n        :param list achalls: Non-empty (guaranteed) list of\n            :class:`~certbot.achallenges.AnnotatedChallenge`\n            instances, such that it contains types found within\n            :func:`get_chall_pref` only.\n\n        :returns: list of ACME\n            :class:`~acme.challenges.ChallengeResponse` instances corresponding to each provided\n            :class:`~acme.challenges.Challenge`.\n        :rtype: :class:`collections.List` of\n            :class:`acme.challenges.ChallengeResponse`,\n            where responses are required to be returned in\n            the same order as corresponding input challenges\n\n        :raises .PluginError: If some or all challenges cannot be performed\n\n        \"\"\"\n\n    @abstractmethod\n    def cleanup(self, achalls: list[AnnotatedChallenge]) -> None:\n        \"\"\"Revert changes and shutdown after challenges complete.\n\n        This method should be able to revert all changes made by\n        perform, even if perform exited abnormally.\n\n        :param list achalls: Non-empty (guaranteed) list of\n            :class:`~certbot.achallenges.AnnotatedChallenge`\n            instances, a subset of those previously passed to :func:`perform`.\n\n        :raises PluginError: if original configuration cannot be restored\n\n        \"\"\"\n\n\nclass Installer(Plugin):\n    \"\"\"Generic Certbot Installer Interface.\n\n    Represents any server that an X509 certificate can be placed.\n\n    It is assumed that :func:`save` is the only method that finalizes a\n    checkpoint. This is important to ensure that checkpoints are\n    restored in a consistent manner if requested by the user or in case\n    of an error.\n\n    Using :class:`certbot.reverter.Reverter` to implement checkpoints,\n    rollback, and recovery can dramatically simplify plugin development.\n\n    \"\"\"\n\n    @abstractmethod\n    def get_all_names(self) -> Iterable[str]:\n        \"\"\"Returns all names that may be authenticated.\n\n        :rtype: `collections.Iterable` of `str`\n\n        \"\"\"\n\n    @abstractmethod\n    def deploy_cert(self, domain: str, cert_path: str, key_path: str,\n                    chain_path: str, fullchain_path: str) -> None:\n        \"\"\"Deploy certificate.\n\n        :param str domain: domain to deploy certificate file\n        :param str cert_path: absolute path to the certificate file\n        :param str key_path: absolute path to the private key file\n        :param str chain_path: absolute path to the certificate chain file\n        :param str fullchain_path: absolute path to the certificate fullchain\n            file (cert plus chain)\n\n        :raises .PluginError: when cert cannot be deployed\n\n        \"\"\"\n\n    @abstractmethod\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[list[str], str]] = None) -> None:\n        \"\"\"Perform a configuration enhancement.\n\n        :param str domain: domain for which to provide enhancement\n        :param str enhancement: An enhancement as defined in\n            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n        :param options: Flexible options parameter for enhancement.\n            Check documentation of\n            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n            for expected options for each enhancement.\n\n        :raises .PluginError: If Enhancement is not supported, or if\n            an error occurs during the enhancement.\n\n        \"\"\"\n\n    @abstractmethod\n    def supported_enhancements(self) -> list[str]:\n        \"\"\"Returns a `collections.Iterable` of supported enhancements.\n\n        :returns: supported enhancements which should be a subset of\n            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n        :rtype: :class:`collections.Iterable` of :class:`str`\n\n        \"\"\"\n\n    @abstractmethod\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        \"\"\"Saves all changes to the configuration files.\n\n        Both title and temporary are needed because a save may be\n        intended to be permanent, but the save is not ready to be a full\n        checkpoint.\n\n        It is assumed that at most one checkpoint is finalized by this\n        method. Additionally, if an exception is raised, it is assumed a\n        new checkpoint was not finalized.\n\n        :param str title: The title of the save. If a title is given, the\n            configuration will be saved as a new checkpoint and put in a\n            timestamped directory. `title` has no effect if temporary is true.\n\n        :param bool temporary: Indicates whether the changes made will\n            be quickly reversed in the future (challenges)\n\n        :raises .PluginError: when save is unsuccessful\n\n        \"\"\"\n\n    @abstractmethod\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        \"\"\"Revert `rollback` number of configuration checkpoints.\n\n        :raises .PluginError: when configuration cannot be fully reverted\n\n        \"\"\"\n\n    @abstractmethod\n    def recovery_routine(self) -> None:\n        \"\"\"Revert configuration to most recent finalized checkpoint.\n\n        Remove all changes (temporary and permanent) that have not been\n        finalized. This is useful to protect against crashes and other\n        execution interruptions.\n\n        :raises .errors.PluginError: If unable to recover the configuration\n\n        \"\"\"\n\n    @abstractmethod\n    def config_test(self) -> None:\n        \"\"\"Make sure the configuration is valid.\n\n        :raises .MisconfigurationError: when the config is not in a usable state\n\n        \"\"\"\n\n    @abstractmethod\n    def restart(self) -> None:\n        \"\"\"Restart or refresh the server content.\n\n        :raises .PluginError: when server cannot be restarted\n\n        \"\"\"\n\n\nclass RenewableCert(metaclass=ABCMeta):\n    \"\"\"Interface to a certificate lineage.\"\"\"\n\n    @property\n    @abstractmethod\n    def cert_path(self) -> str:\n        \"\"\"Path to the certificate file.\n\n        :rtype: str\n\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def key_path(self) -> str:\n        \"\"\"Path to the private key file.\n\n        :rtype: str\n\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def chain_path(self) -> str:\n        \"\"\"Path to the certificate chain file.\n\n        :rtype: str\n\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def fullchain_path(self) -> str:\n        \"\"\"Path to the full chain file.\n\n        The full chain is the certificate file plus the chain file.\n\n        :rtype: str\n\n        \"\"\"\n\n    @property\n    @abstractmethod\n    def lineagename(self) -> str:\n        \"\"\"Name given to the certificate lineage.\n\n        :rtype: str\n\n        \"\"\"\n\n    @abstractmethod\n    def names(self) -> list[str]:\n        \"\"\"What are the subject names of this certificate?\n\n        :returns: the subject names\n        :rtype: `list` of `str`\n        :raises .CertStorageError: if could not find cert file.\n\n        \"\"\"\n\n# Updater interfaces\n#\n# When \"certbot renew\" is run, Certbot will iterate over each lineage and check\n# if the selected installer for that lineage is a subclass of each updater\n# class. If it is and the update of that type is configured to be run for that\n# lineage, the relevant update function will be called for it. These functions\n# are never called for other subcommands, so if an installer wants to perform\n# an update during the run or install subcommand, it should do so when\n# :func:`IInstaller.deploy_cert` is called.\n\n\nclass GenericUpdater(metaclass=ABCMeta):\n    \"\"\"Interface for update types not currently specified by Certbot.\n\n    This class allows plugins to perform types of updates that Certbot hasn't\n    defined (yet).\n\n    To make use of this interface, the installer should implement the interface\n    methods, and interfaces.GenericUpdater.register(InstallerClass) should\n    be called from the installer code.\n\n    The plugins implementing this enhancement are responsible of handling\n    the saving of configuration checkpoints as well as other calls to\n    interface methods of `interfaces.Installer` such as prepare() and restart()\n    \"\"\"\n\n    @abstractmethod\n    def generic_updates(self, lineage: RenewableCert, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Perform any update types defined by the installer.\n\n        If an installer is a subclass of the class containing this method, this\n        function will always be called when \"certbot renew\" is run. If the\n        update defined by the installer should be run conditionally, the\n        installer needs to handle checking the conditions itself.\n\n        This method is called once for each lineage.\n\n        :param lineage: Certificate lineage object\n        :type lineage: RenewableCert\n\n        \"\"\"\n\n\nclass RenewDeployer(metaclass=ABCMeta):\n    \"\"\"Interface for update types run when a lineage is renewed\n\n    This class allows plugins to perform types of updates that need to run at\n    lineage renewal that Certbot hasn't defined (yet).\n\n    To make use of this interface, the installer should implement the interface\n    methods, and interfaces.RenewDeployer.register(InstallerClass) should\n    be called from the installer code.\n    \"\"\"\n\n    @abstractmethod\n    def renew_deploy(self, lineage: RenewableCert, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Perform updates defined by installer when a certificate has been renewed\n\n        If an installer is a subclass of the class containing this method, this\n        function will always be called when a certificate has been renewed by\n        running \"certbot renew\". For example if a plugin needs to copy a\n        certificate over, or change configuration based on the new certificate.\n\n        This method is called once for each lineage renewed\n\n        :param lineage: Certificate lineage object\n        :type lineage: RenewableCert\n\n        \"\"\"\n\n\nclass IPluginFactory(ZopeInterface):\n    \"\"\"Compatibility shim for plugins that still use Certbot's old zope.interface classes.\"\"\"\n\nclass IPlugin(ZopeInterface):\n    \"\"\"Compatibility shim for plugins that still use Certbot's old zope.interface classes.\"\"\"\n\nclass IAuthenticator(IPlugin):\n    \"\"\"Compatibility shim for plugins that still use Certbot's old zope.interface classes.\"\"\"\n\nclass IInstaller(IPlugin):\n    \"\"\"Compatibility shim for plugins that still use Certbot's old zope.interface classes.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/main.py",
    "content": "\"\"\"Certbot main public entry point.\"\"\"\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot._internal import main as internal_main\n\n\ndef main(cli_args: Optional[list[str]] = None) -> Optional[Union[str, int]]:\n    \"\"\"Run Certbot.\n\n    :param cli_args: command line to Certbot, defaults to ``sys.argv[1:]``\n    :type cli_args: `list` of `str`\n\n    :returns: value for `sys.exit` about the exit status of Certbot\n    :rtype: `str` or `int` or `None`\n\n    \"\"\"\n    return internal_main.main(cli_args)\n"
  },
  {
    "path": "certbot/src/certbot/ocsp.py",
    "content": "\"\"\"Deprecated tools for checking certificate revocation.\"\"\"\nimport warnings\n\n# ruff: disable[F403]\nfrom certbot._internal.ocsp import *  # pylint: disable=wildcard-import,unused-wildcard-import\n# ruff: enable[F403]\n\nwarnings.warn(\"certbot.ocsp is deprecated and will be removed in the next major\"\n              \" release\", DeprecationWarning, stacklevel=2)\n"
  },
  {
    "path": "certbot/src/certbot/plugins/__init__.py",
    "content": "\"\"\"Certbot plugins.\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/plugins/common.py",
    "content": "\"\"\"Plugin common functions.\"\"\"\nfrom abc import ABCMeta\nfrom abc import abstractmethod\nimport argparse\nimport importlib.resources\nimport logging\nimport re\nimport shutil\nimport tempfile\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TypeVar\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import configuration\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot import reverter\nfrom certbot._internal import constants\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.interfaces import Installer as AbstractInstaller\nfrom certbot.interfaces import Plugin as AbstractPlugin\nfrom certbot.plugins.storage import PluginStorage\n\nlogger = logging.getLogger(__name__)\n\n\ndef option_namespace(name: str) -> str:\n    \"\"\"ArgumentParser options namespace (prefix of all options).\"\"\"\n    return name + \"-\"\n\n\ndef dest_namespace(name: str) -> str:\n    \"\"\"ArgumentParser dest namespace (prefix of all destinations).\"\"\"\n    return name.replace(\"-\", \"_\") + \"_\"\n\n\nprivate_ips_regex = re.compile(\n    r\"(^127\\.0\\.0\\.1)|(^10\\.)|(^172\\.1[6-9]\\.)|\"\n    r\"(^172\\.2[0-9]\\.)|(^172\\.3[0-1]\\.)|(^192\\.168\\.)\")\nhostname_regex = re.compile(\n    r\"^(([a-z0-9]|[a-z0-9][a-z0-9\\-]*[a-z0-9])\\.)*[a-z]+$\", re.IGNORECASE)\n\n\nclass Plugin(AbstractPlugin, metaclass=ABCMeta):\n    \"\"\"Generic plugin.\"\"\"\n\n    def __init__(self, config: configuration.NamespaceConfig, name: str) -> None:\n        super().__init__(config, name)\n        self.config = config\n        self.name = name\n\n    @classmethod\n    @abstractmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        \"\"\"Add plugin arguments to the CLI argument parser.\n\n        :param callable add: Function that proxies calls to\n            `argparse.ArgumentParser.add_argument` prepending options\n            with unique plugin name prefix.\n\n        \"\"\"\n\n    @classmethod\n    def inject_parser_options(cls, parser: argparse.ArgumentParser, name: str) -> None:\n        \"\"\"Inject parser options.\n\n        See `~.certbot.interfaces.Plugin.inject_parser_options` for docs.\n\n        \"\"\"\n        # dummy function, doesn't check if dest.startswith(self.dest_namespace)\n        def add(arg_name_no_prefix: str, *args: Any, **kwargs: Any) -> None:\n            parser.add_argument(\n                \"--{0}{1}\".format(option_namespace(name), arg_name_no_prefix),\n                *args, **kwargs)\n        return cls.add_parser_arguments(add)\n\n    @property\n    def option_namespace(self) -> str:\n        \"\"\"ArgumentParser options namespace (prefix of all options).\"\"\"\n        return option_namespace(self.name)\n\n    def option_name(self, name: str) -> str:\n        \"\"\"Option name (include plugin namespace).\"\"\"\n        return self.option_namespace + name\n\n    @property\n    def dest_namespace(self) -> str:\n        \"\"\"ArgumentParser dest namespace (prefix of all destinations).\"\"\"\n        return dest_namespace(self.name)\n\n    def dest(self, var: str) -> str:\n        \"\"\"Find a destination for given variable ``var``.\"\"\"\n        # this should do exactly the same what ArgumentParser(arg),\n        # does to \"arg\" to compute \"dest\"\n        return self.dest_namespace + var.replace(\"-\", \"_\")\n\n    def conf(self, var: str) -> Any:\n        \"\"\"Find a configuration value for variable ``var``.\"\"\"\n        return getattr(self.config, self.dest(var))\n\n    def auth_hint(self, failed_achalls: list[achallenges.AnnotatedChallenge]) -> str:\n        \"\"\"Human-readable string to help the user troubleshoot the authenticator.\n\n        Shown to the user if one or more of the attempted challenges were not a success.\n\n        Should describe, in simple language, what the authenticator tried to do, what went\n        wrong and what the user should try as their \"next steps\".\n\n        TODO: auth_hint belongs in Authenticator but can't be added until the next major\n        version of Certbot. For now, it lives in .Plugin and auth_handler will only call it\n        on authenticators that subclass .Plugin. For now, inherit from `.Plugin` to implement\n        and/or override the method.\n\n        :param list failed_achalls: List of one or more failed challenges\n                                    (:class:`achallenges.AnnotatedChallenge` subclasses).\n\n        :rtype str:\n        \"\"\"\n        # This is a fallback hint. Authenticators should implement their own auth_hint that\n        # addresses the specific mechanics of that authenticator.\n        challs = \" and \".join(sorted({achall.typ for achall in failed_achalls}))\n        return (\"The Certificate Authority couldn't externally verify that the {name} plugin \"\n                \"completed the required {challs} challenges. Ensure the plugin is configured \"\n                \"correctly and that the changes it makes are accessible from the internet.\"\n                .format(name=self.name, challs=challs))\n\n\nclass Installer(AbstractInstaller, Plugin, metaclass=ABCMeta):\n    \"\"\"An installer base class with reverter and ssl_dhparam methods defined.\n\n    Installer plugins do not have to inherit from this class.\n\n    \"\"\"\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.storage = PluginStorage(self.config, self.name)\n        self.reverter = reverter.Reverter(self.config)\n\n    def add_to_checkpoint(self, save_files: set[str], save_notes: str,\n                          temporary: bool = False) -> None:\n        \"\"\"Add files to a checkpoint.\n\n        :param set save_files: set of filepaths to save\n        :param str save_notes: notes about changes during the save\n        :param bool temporary: True if the files should be added to a\n            temporary checkpoint rather than a permanent one. This is\n            usually used for changes that will soon be reverted.\n\n        :raises .errors.PluginError: when unable to add to checkpoint\n\n        \"\"\"\n        if temporary:\n            checkpoint_func = self.reverter.add_to_temp_checkpoint\n        else:\n            checkpoint_func = self.reverter.add_to_checkpoint\n\n        try:\n            checkpoint_func(save_files, save_notes)\n        except errors.ReverterError as err:\n            raise errors.PluginError(str(err))\n\n    def finalize_checkpoint(self, title: str) -> None:\n        \"\"\"Timestamp and save changes made through the reverter.\n\n        :param str title: Title describing checkpoint\n\n        :raises .errors.PluginError: when an error occurs\n\n        \"\"\"\n        try:\n            self.reverter.finalize_checkpoint(title)\n        except errors.ReverterError as err:\n            raise errors.PluginError(str(err))\n\n    def recovery_routine(self) -> None:\n        \"\"\"Revert all previously modified files.\n\n        Reverts all modified files that have not been saved as a checkpoint\n\n        :raises .errors.PluginError: If unable to recover the configuration\n\n        \"\"\"\n        try:\n            self.reverter.recovery_routine()\n        except errors.ReverterError as err:\n            raise errors.PluginError(str(err))\n\n    def revert_temporary_config(self) -> None:\n        \"\"\"Rollback temporary checkpoint.\n\n        :raises .errors.PluginError: when unable to revert config\n\n        \"\"\"\n        try:\n            self.reverter.revert_temporary_config()\n        except errors.ReverterError as err:\n            raise errors.PluginError(str(err))\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        \"\"\"Rollback saved checkpoints.\n\n        :param int rollback: Number of checkpoints to revert\n\n        :raises .errors.PluginError: If there is a problem with the input or\n            the function is unable to correctly revert the configuration\n\n        \"\"\"\n        try:\n            self.reverter.rollback_checkpoints(rollback)\n        except errors.ReverterError as err:\n            raise errors.PluginError(str(err))\n\n    @property\n    def ssl_dhparams(self) -> str:\n        \"\"\"Full absolute path to ssl_dhparams file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.SSL_DHPARAMS_DEST)\n\n    @property\n    def updated_ssl_dhparams_digest(self) -> str:\n        \"\"\"Full absolute path to digest of updated ssl_dhparams file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.UPDATED_SSL_DHPARAMS_DIGEST)\n\n    def install_ssl_dhparams(self) -> None:\n        \"\"\"Copy Certbot's ssl_dhparams file into the system's config dir if required.\"\"\"\n        install_version_controlled_file(\n            self.ssl_dhparams,\n            self.updated_ssl_dhparams_digest,\n            constants.SSL_DHPARAMS_SRC,\n            constants.ALL_SSL_DHPARAMS_HASHES)\n\n\nclass Configurator(Installer, interfaces.Authenticator, metaclass=ABCMeta):\n    \"\"\"\n    A plugin that extends certbot.plugins.common.Installer\n    and implements certbot.interfaces.Authenticator\n    \"\"\"\n\n\nGenericAddr = TypeVar(\"GenericAddr\", bound=\"Addr\")\n\n\nclass Addr:\n    r\"\"\"Represents an virtual host address.\n\n    :param str addr: addr part of vhost address\n    :param str port: port number or \\*, or \"\"\n\n    \"\"\"\n    def __init__(self, tup: tuple[str, str], ipv6: bool = False):\n        self.tup = tup\n        self.ipv6 = ipv6\n\n    @classmethod\n    def fromstring(cls: type[GenericAddr], str_addr: str) -> GenericAddr:\n        \"\"\"Initialize Addr from string.\"\"\"\n        if str_addr.startswith('['):\n            # ipv6 addresses starts with [\n            endIndex = str_addr.rfind(']')\n            host = str_addr[:endIndex + 1]\n            port = ''\n            if len(str_addr) > endIndex + 2 and str_addr[endIndex + 1] == ':':\n                port = str_addr[endIndex + 2:]\n            return cls((host, port), ipv6=True)\n        else:\n            tup = str_addr.partition(':')\n            return cls((tup[0], tup[2]))\n\n    def __str__(self) -> str:\n        if self.tup[1]:\n            return \"%s:%s\" % self.tup\n        return self.tup[0]\n\n    def normalized_tuple(self) -> tuple[str, str]:\n        \"\"\"Normalized representation of addr/port tuple\n        \"\"\"\n        if self.ipv6:\n            return self.get_ipv6_exploded(), self.tup[1]\n        return self.tup\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            # compare normalized to take different\n            # styles of representation into account\n            return self.normalized_tuple() == other.normalized_tuple()\n\n        return False\n\n    def __hash__(self) -> int:\n        return hash(self.tup)\n\n    def get_addr(self) -> str:\n        \"\"\"Return addr part of Addr object.\"\"\"\n        return self.tup[0]\n\n    def get_port(self) -> str:\n        \"\"\"Return port.\"\"\"\n        return self.tup[1]\n\n    def get_addr_obj(self: GenericAddr, port: str) -> GenericAddr:\n        \"\"\"Return new address object with same addr and new port.\"\"\"\n        return self.__class__((self.tup[0], port), self.ipv6)\n\n    def _normalize_ipv6(self, addr: str) -> list[str]:\n        \"\"\"Return IPv6 address in normalized form, helper function\"\"\"\n        addr = addr.lstrip(\"[\")\n        addr = addr.rstrip(\"]\")\n        return self._explode_ipv6(addr)\n\n    def get_ipv6_exploded(self) -> str:\n        \"\"\"Return IPv6 in normalized form\"\"\"\n        if self.ipv6:\n            return \":\".join(self._normalize_ipv6(self.tup[0]))\n        return \"\"\n\n    def _explode_ipv6(self, addr: str) -> list[str]:\n        \"\"\"Explode IPv6 address for comparison\"\"\"\n        result = ['0', '0', '0', '0', '0', '0', '0', '0']\n        addr_list = addr.split(\":\")\n        if len(addr_list) > len(result):\n            # too long, truncate\n            addr_list = addr_list[0:len(result)]\n        append_to_end = False\n        for i, block in enumerate(addr_list):\n            if not block:\n                # encountered ::, so rest of the blocks should be\n                # appended to the end\n                append_to_end = True\n                continue\n            if len(block) > 1:\n                # remove leading zeros\n                block = block.lstrip(\"0\")\n            if not append_to_end:\n                result[i] = str(block)\n            else:\n                # count the location from the end using negative indices\n                result[i-len(addr_list)] = str(block)\n        return result\n\n\nclass ChallengePerformer:\n    \"\"\"Abstract base for challenge performers.\n\n    :ivar configurator: Authenticator and installer plugin\n    :ivar achalls: Annotated challenges\n    :vartype achalls: `list` of `.KeyAuthorizationAnnotatedChallenge`\n    :ivar indices: Holds the indices of challenges from a larger array\n        so the user of the class doesn't have to.\n    :vartype indices: `list` of `int`\n\n    \"\"\"\n\n    def __init__(self, configurator: Configurator):\n        self.configurator = configurator\n        self.achalls: list[achallenges.KeyAuthorizationAnnotatedChallenge] = []\n        self.indices: list[int] = []\n\n    def add_chall(self, achall: achallenges.KeyAuthorizationAnnotatedChallenge,\n                  idx: Optional[int] = None) -> None:\n        \"\"\"Store challenge to be performed when perform() is called.\n\n        :param .KeyAuthorizationAnnotatedChallenge achall: Annotated\n            challenge.\n        :param int idx: index to challenge in a larger array\n\n        \"\"\"\n        self.achalls.append(achall)\n        if idx is not None:\n            self.indices.append(idx)\n\n    def perform(self) -> list[challenges.KeyAuthorizationChallengeResponse]:\n        \"\"\"Perform all added challenges.\n\n        :returns: challenge responses\n        :rtype: `list` of `acme.challenges.KeyAuthorizationChallengeResponse`\n\n\n        \"\"\"\n        raise NotImplementedError()\n\n\ndef install_version_controlled_file(dest_path: str, digest_path: str, src_path: str,\n                                    all_hashes: Iterable[str]) -> None:\n    \"\"\"Copy a file into an active location (likely the system's config dir) if required.\n\n       :param str dest_path: destination path for version controlled file\n       :param str digest_path: path to save a digest of the file in\n       :param str src_path: path to version controlled file found in distribution\n       :param list all_hashes: hashes of every released version of the file\n    \"\"\"\n    current_hash = crypto_util.sha256sum(src_path)\n\n    def _write_current_hash() -> None:\n        with open(digest_path, \"w\") as file_h:\n            file_h.write(current_hash)\n\n    def _install_current_file() -> None:\n        shutil.copyfile(src_path, dest_path)\n        _write_current_hash()\n\n    # Check to make sure options-ssl.conf is installed\n    if not os.path.isfile(dest_path):\n        _install_current_file()\n        return\n    # there's already a file there. if it's up to date, do nothing. if it's not but\n    # it matches a known file hash, we can update it.\n    # otherwise, print a warning once per new version.\n    active_file_digest = crypto_util.sha256sum(dest_path)\n    if active_file_digest == current_hash: # already up to date\n        return\n    if active_file_digest in all_hashes: # safe to update\n        _install_current_file()\n    else:  # has been manually modified, not safe to update\n        # did they modify the current version or an old version?\n        if os.path.isfile(digest_path):\n            with open(digest_path, \"r\") as f:\n                saved_digest = f.read()\n            # they modified it after we either installed or told them about this version, so return\n            if saved_digest == current_hash:\n                return\n        # there's a new version but we couldn't update the file, or they deleted the digest.\n        # save the current digest so we only print this once, and print a warning\n        _write_current_hash()\n        logger.warning(\"%s has been manually modified; updated file \"\n            \"saved to %s. We recommend updating %s for security purposes.\",\n            dest_path, src_path, dest_path)\n\n\n# test utils used by certbot_apache/certbot_nginx (hence\n# \"pragma: no cover\") TODO: this might quickly lead to dead code (also\n# c.f. #383)\n\ndef dir_setup(test_dir: str, pkg: str) -> tuple[str, str, str]:  # pragma: no cover\n    \"\"\"Setup the directories necessary for the configurator.\"\"\"\n    def expanded_tempdir(prefix: str) -> str:\n        \"\"\"Return the real path of a temp directory with the specified prefix\n\n        Some plugins rely on real paths of symlinks for working correctly. For\n        example, certbot-apache uses real paths of configuration files to tell\n        a virtual host from another. On systems where TMP itself is a symbolic\n        link, (ex: OS X) such plugins will be confused. This function prevents\n        such a case.\n        \"\"\"\n        return filesystem.realpath(tempfile.mkdtemp(prefix))\n\n    temp_dir = expanded_tempdir(\"temp\")\n    config_dir = expanded_tempdir(\"config\")\n    work_dir = expanded_tempdir(\"work\")\n\n    filesystem.chmod(temp_dir, constants.CONFIG_DIRS_MODE)\n    filesystem.chmod(config_dir, constants.CONFIG_DIRS_MODE)\n    filesystem.chmod(work_dir, constants.CONFIG_DIRS_MODE)\n\n    test_dir_ref = importlib.resources.files(pkg).joinpath(\"testdata\").joinpath(test_dir)\n    with importlib.resources.as_file(test_dir_ref) as path:\n        shutil.copytree(\n            path, os.path.join(temp_dir, test_dir), symlinks=True)\n\n    return temp_dir, config_dir, work_dir\n"
  },
  {
    "path": "certbot/src/certbot/plugins/dns_common.py",
    "content": "\"\"\"Common code for DNS Authenticator Plugins.\"\"\"\nimport abc\nimport logging\nfrom time import sleep\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\n\nimport configobj\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import ops\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\n\nlogger = logging.getLogger(__name__)\n\n\n# As of writing this, the only one of our plugins that does not inherit from this class (either\n# directly or indirectly through certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator) is\n# certbot-dns-route53. If you are attempting to make changes to all of our DNS plugins, please keep\n# this difference in mind.\nclass DNSAuthenticator(common.Plugin, interfaces.Authenticator, metaclass=abc.ABCMeta):\n    \"\"\"Base class for DNS Authenticators\"\"\"\n\n    def __init__(self, config: configuration.NamespaceConfig, name: str) -> None:\n        super().__init__(config, name)\n\n        self._attempt_cleanup = False\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],  # pylint: disable=arguments-differ\n                             default_propagation_seconds: int = 10) -> None:\n        add('propagation-seconds',\n            default=default_propagation_seconds,\n            type=int,\n            help='The number of seconds to wait for DNS to propagate before asking the ACME server '\n                 'to verify the DNS record.')\n\n    def auth_hint(self, failed_achalls: list[achallenges.AnnotatedChallenge]) -> str:\n        \"\"\"See certbot.plugins.common.Plugin.auth_hint.\"\"\"\n        delay = self.conf('propagation-seconds')\n        return (\n            'The Certificate Authority failed to verify the DNS TXT records created by --{name}. '\n            'Ensure the above domains are hosted by this DNS provider, or try increasing '\n            '--{name}-propagation-seconds (currently {secs} second{suffix}).'\n            .format(name=self.name, secs=delay, suffix='s' if delay != 1 else '')\n        )\n\n    def get_chall_pref(self, unused_identifier: str) -> Iterable[type[challenges.Challenge]]:  # pylint: disable=missing-function-docstring\n        return [challenges.DNS01]\n\n    def prepare(self) -> None:  # pylint: disable=missing-function-docstring\n        pass\n\n    def more_info(self) -> str:  # pylint: disable=missing-function-docstring\n        raise NotImplementedError()\n\n    def perform(self, achalls: list[achallenges.AnnotatedChallenge]\n                ) -> list[challenges.ChallengeResponse]: # pylint: disable=missing-function-docstring\n        self._setup_credentials()\n\n        self._attempt_cleanup = True\n\n        responses = []\n        for achall in achalls:\n            domain = achall.identifier.value\n            validation_domain_name = achall.validation_domain_name(domain)\n            validation = achall.validation(achall.account_key)\n\n            self._perform(domain, validation_domain_name, validation)\n            responses.append(achall.response(achall.account_key))\n\n        # DNS updates take time to propagate and checking to see if the update has occurred is not\n        # reliable (the machine this code is running on might be able to see an update before\n        # the ACME server). So: we sleep for a short amount of time we believe to be long enough.\n        display_util.notify(\"Waiting %d seconds for DNS changes to propagate\" %\n                    self.conf('propagation-seconds'))\n        sleep(self.conf('propagation-seconds'))\n\n        return responses\n\n    def cleanup(self, achalls: list[achallenges.AnnotatedChallenge]) -> None:  # pylint: disable=missing-function-docstring\n        if self._attempt_cleanup:\n            for achall in achalls:\n                domain = achall.identifier.value\n                validation_domain_name = achall.validation_domain_name(domain)\n                validation = achall.validation(achall.account_key)\n\n                self._cleanup(domain, validation_domain_name, validation)\n\n    @abc.abstractmethod\n    def _setup_credentials(self) -> None:  # pragma: no cover\n        \"\"\"\n        Establish credentials, prompting if necessary.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def _perform(self, domain: str, validation_name: str,\n                 validation: str) -> None:  # pragma: no cover\n        \"\"\"\n        Performs a dns-01 challenge by creating a DNS TXT record.\n\n        :param str domain: The domain being validated.\n        :param str validation_domain_name: The validation record domain name.\n        :param str validation: The validation record content.\n        :raises errors.PluginError: If the challenge cannot be performed\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def _cleanup(self, domain: str, validation_name: str,\n                 validation: str) -> None:  # pragma: no cover\n        \"\"\"\n        Deletes the DNS TXT record which would have been created by `_perform_achall`.\n\n        Fails gracefully if no such record exists.\n\n        :param str domain: The domain being validated.\n        :param str validation_domain_name: The validation record domain name.\n        :param str validation: The validation record content.\n        \"\"\"\n        raise NotImplementedError()\n\n    def _configure(self, key: str, label: str) -> None:\n        \"\"\"\n        Ensure that a configuration value is available.\n\n        If necessary, prompts the user and stores the result.\n\n        :param str key: The configuration key.\n        :param str label: The user-friendly label for this piece of information.\n        \"\"\"\n\n        configured_value = self.conf(key)\n        if not configured_value:\n            new_value = self._prompt_for_data(label)\n\n            setattr(self.config, self.dest(key), new_value)\n\n    def _configure_file(self, key: str, label: str,\n                        validator: Optional[Callable[[str], None]] = None) -> None:\n        \"\"\"\n        Ensure that a configuration value is available for a path.\n\n        If necessary, prompts the user and stores the result.\n\n        :param str key: The configuration key.\n        :param str label: The user-friendly label for this piece of information.\n        \"\"\"\n\n        configured_value = self.conf(key)\n        if not configured_value:\n            new_value = self._prompt_for_file(label, validator)\n\n            setattr(self.config, self.dest(key), os.path.abspath(os.path.expanduser(new_value)))\n\n    def _configure_credentials(\n        self, key: str, label: str, required_variables: Optional[Mapping[str, str]] = None,\n        validator: Optional[Callable[['CredentialsConfiguration'], None]] = None\n    ) -> 'CredentialsConfiguration':\n        \"\"\"\n        As `_configure_file`, but for a credential configuration file.\n\n        If necessary, prompts the user and stores the result.\n\n        Always stores absolute paths to avoid issues during renewal.\n\n        :param str key: The configuration key.\n        :param str label: The user-friendly label for this piece of information.\n        :param dict required_variables: Map of variable which must be present to error to display.\n        :param callable validator: A method which will be called to validate the\n            `CredentialsConfiguration` resulting from the supplied input after it has been validated\n            to contain the `required_variables`. Should throw a `~certbot.errors.PluginError` to\n            indicate any issue.\n        \"\"\"\n\n        def __validator(filename: str) -> None:  # pylint: disable=unused-private-member\n            applied_configuration = CredentialsConfiguration(filename, self.dest)\n\n            if required_variables:\n                applied_configuration.require(required_variables)\n\n            if validator:\n                validator(applied_configuration)\n\n        self._configure_file(key, label, __validator)\n\n        credentials_configuration = CredentialsConfiguration(self.conf(key), self.dest)\n        if required_variables:\n            credentials_configuration.require(required_variables)\n\n        if validator:\n            validator(credentials_configuration)\n\n        return credentials_configuration\n\n    @staticmethod\n    def _prompt_for_data(label: str) -> str:\n        \"\"\"\n        Prompt the user for a piece of information.\n\n        :param str label: The user-friendly label for this piece of information.\n        :returns: The user's response (guaranteed non-empty).\n        :rtype: str\n        \"\"\"\n\n        def __validator(i: str) -> None:  # pylint: disable=unused-private-member\n            if not i:\n                raise errors.PluginError('Please enter your {0}.'.format(label))\n\n        code, response = ops.validated_input(\n            __validator,\n            'Input your {0}'.format(label),\n            force_interactive=True)\n\n        if code == display_util.OK:\n            return response\n        raise errors.PluginError('{0} required to proceed.'.format(label))\n\n    @staticmethod\n    def _prompt_for_file(label: str, validator: Optional[Callable[[str], None]] = None) -> str:\n        \"\"\"\n        Prompt the user for a path.\n\n        :param str label: The user-friendly label for the file.\n        :param callable validator: A method which will be called to validate the supplied input\n            after it has been validated to be a non-empty path to an existing file. Should throw a\n            `~certbot.errors.PluginError` to indicate any issue.\n        :returns: The user's response (guaranteed to exist).\n        :rtype: str\n        \"\"\"\n\n        def __validator(filename: str) -> None:  # pylint: disable=unused-private-member\n            if not filename:\n                raise errors.PluginError('Please enter a valid path to your {0}.'.format(label))\n\n            filename = os.path.expanduser(filename)\n\n            validate_file(filename)\n\n            if validator:\n                validator(filename)\n\n        code, response = ops.validated_directory(\n            __validator,\n            'Input the path to your {0}'.format(label),\n            force_interactive=True)\n\n        if code == display_util.OK:\n            return response\n        raise errors.PluginError('{0} required to proceed.'.format(label))\n\n\nclass CredentialsConfiguration:\n    \"\"\"Represents a user-supplied filed which stores API credentials.\"\"\"\n\n    def __init__(self, filename: str, mapper: Callable[[str], str] = lambda x: x) -> None:\n        \"\"\"\n        :param str filename: A path to the configuration file.\n        :param callable mapper: A transformation to apply to configuration key names\n        :raises errors.PluginError: If the file does not exist or is not a valid format.\n        \"\"\"\n        validate_file_permissions(filename)\n\n        try:\n            self.confobj = configobj.ConfigObj(filename)\n        except configobj.ConfigObjError as e:\n            logger.debug(\n                \"Error parsing credentials configuration '%s': %s\",\n                filename,\n                e,\n                exc_info=True\n            )\n            raise errors.PluginError(\n                \"Error parsing credentials configuration '{}': {}\".format(\n                    filename,\n                    e\n                )\n            )\n\n        self.mapper = mapper\n\n    def require(self, required_variables: Mapping[str, str]) -> None:\n        \"\"\"Ensures that the supplied set of variables are all present in the file.\n\n        :param dict required_variables: Map of variable which must be present to error to display.\n        :raises errors.PluginError: If one or more are missing.\n        \"\"\"\n        messages = []\n\n        for var in required_variables:\n            if not self._has(var):\n                messages.append('Property \"{0}\" not found (should be {1}).'\n                                .format(self.mapper(var), required_variables[var]))\n            elif not self._get(var):\n                messages.append('Property \"{0}\" not set (should be {1}).'\n                                .format(self.mapper(var), required_variables[var]))\n\n        if messages:\n            raise errors.PluginError(\n                'Missing {0} in credentials configuration file {1}:\\n * {2}'.format(\n                        'property' if len(messages) == 1 else 'properties',\n                        self.confobj.filename,\n                        '\\n * '.join(messages)\n                    )\n            )\n\n    def conf(self, var: str) -> Optional[str]:\n        \"\"\"Find a configuration value for variable `var`, as transformed by `mapper`.\n\n        :param str var: The variable to get.\n        :returns: The value of the variable, if it exists.\n        :rtype: str or None\n        \"\"\"\n\n        return self._get(var)\n\n    def _has(self, var: str) -> bool:\n        return self.mapper(var) in self.confobj\n\n    def _get(self, var: str) -> Optional[str]:\n        return self.confobj.get(self.mapper(var))\n\n\ndef validate_file(filename: str) -> None:\n    \"\"\"Ensure that the specified file exists.\"\"\"\n\n    if not os.path.exists(filename):\n        raise errors.PluginError('File not found: {0}'.format(filename))\n\n    if os.path.isdir(filename):\n        raise errors.PluginError('Path is a directory: {0}'.format(filename))\n\n\ndef validate_file_permissions(filename: str) -> None:\n    \"\"\"Ensure that the specified file exists and warn about unsafe permissions.\"\"\"\n\n    validate_file(filename)\n\n    if filesystem.has_world_permissions(filename):\n        logger.warning('Unsafe permissions on credentials configuration file: %s', filename)\n\n\ndef base_domain_name_guesses(domain: str) -> list[str]:\n    \"\"\"Return a list of progressively less-specific domain names.\n\n    One of these will probably be the domain name known to the DNS provider.\n\n    :Example:\n\n    >>> base_domain_name_guesses('foo.bar.baz.example.com')\n    ['foo.bar.baz.example.com', 'bar.baz.example.com', 'baz.example.com', 'example.com', 'com']\n\n    :param str domain: The domain for which to return guesses.\n    :returns: The a list of less specific domain names.\n    :rtype: list\n    \"\"\"\n\n    fragments = domain.split('.')\n    return ['.'.join(fragments[i:]) for i in range(0, len(fragments))]\n"
  },
  {
    "path": "certbot/src/certbot/plugins/dns_common_lexicon.py",
    "content": "\"\"\"Common code for DNS Authenticator Plugins built on Lexicon.\"\"\"\nimport abc\nimport logging\nimport sys\nfrom types import ModuleType\nfrom typing import Any\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\nimport warnings\n\nfrom requests.exceptions import HTTPError\nfrom requests.exceptions import RequestException\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot.plugins import dns_common\n\n# Lexicon is not declared as a dependency in Certbot itself,\n# but in the Certbot plugins backed by Lexicon.\n# So we catch import error here to allow this module to be\n# always importable, even if it does not make sense to use it\n# if Lexicon is not available, obviously.\ntry:\n    from lexicon.client import Client\n    from lexicon.config import ConfigResolver\n    from lexicon.interfaces import Provider\nexcept ImportError:  # pragma: no cover\n    Client = None  # type: ignore\n    ConfigResolver = None  # type: ignore\n    Provider = None  # type: ignore\n\nlogger = logging.getLogger(__name__)\n\n\nclass LexiconClient:  # pragma: no cover\n    \"\"\"\n    Encapsulates all communication with a DNS provider via Lexicon.\n\n    .. deprecated:: 2.7.0\n       Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.provider: Provider\n\n    def add_txt_record(self, domain: str, record_name: str, record_content: str) -> None:\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :raises errors.PluginError: if an error occurs communicating with the DNS Provider API\n        \"\"\"\n        self._find_domain_id(domain)\n\n        try:\n            self.provider.create_record(rtype='TXT', name=record_name, content=record_content)\n        except RequestException as e:\n            logger.debug('Encountered error adding TXT record: %s', e, exc_info=True)\n            raise errors.PluginError('Error adding TXT record: {0}'.format(e))\n\n    def del_txt_record(self, domain: str, record_name: str, record_content: str) -> None:\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :raises errors.PluginError: if an error occurs communicating with the DNS Provider  API\n        \"\"\"\n        try:\n            self._find_domain_id(domain)\n        except errors.PluginError as e:\n            logger.debug('Encountered error finding domain_id during deletion: %s', e,\n                         exc_info=True)\n            return\n\n        try:\n            self.provider.delete_record(rtype='TXT', name=record_name, content=record_content)\n        except RequestException as e:\n            logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True)\n\n    def _find_domain_id(self, domain: str) -> None:\n        \"\"\"\n        Find the domain_id for a given domain.\n\n        :param str domain: The domain for which to find the domain_id.\n        :raises errors.PluginError: if the domain_id cannot be found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(domain)\n\n        for domain_name in domain_name_guesses:\n            try:\n                if hasattr(self.provider, 'options'):\n                    # For Lexicon 2.x\n                    self.provider.options['domain'] = domain_name\n                else:\n                    # For Lexicon 3.x\n                    self.provider.domain = domain_name\n\n                self.provider.authenticate()\n\n                return  # If `authenticate` doesn't throw an exception, we've found the right name\n            except HTTPError as e:\n                result1 = self._handle_http_error(e, domain_name)\n\n                if result1:\n                    raise result1\n            except Exception as e:  # pylint: disable=broad-except\n                result2 = self._handle_general_error(e, domain_name)\n\n                if result2:\n                    raise result2  # pylint: disable=raising-bad-type\n\n        raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}'\n                                 .format(domain, domain_name_guesses))\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        return errors.PluginError('Error determining zone identifier for {0}: {1}.'\n                                  .format(domain_name, e))\n\n    def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:\n        if not str(e).startswith('No domain found'):\n            return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}'\n                                      .format(domain_name, e))\n        return None\n\n\ndef build_lexicon_config(lexicon_provider_name: str,\n                         lexicon_options: Mapping[str, Any], provider_options: Mapping[str, Any]\n                         ) -> Union[ConfigResolver, dict[str, Any]]:  # pragma: no cover\n    \"\"\"\n    Convenient function to build a Lexicon 2.x/3.x config object.\n\n    :param str lexicon_provider_name: the name of the lexicon provider to use\n    :param dict lexicon_options: options specific to lexicon\n    :param dict provider_options: options specific to provider\n    :return: configuration to apply to the provider\n    :rtype: ConfigurationResolver or dict\n\n    .. deprecated:: 2.7.0\n       Please use certbot.plugins.dns_common_lexicon.LexiconDNSAuthenticator instead.\n    \"\"\"\n    config_dict: dict[str, Any] = {'provider_name': lexicon_provider_name}\n    config_dict.update(lexicon_options)\n    if ConfigResolver is None:\n        # Lexicon 2.x\n        config_dict.update(provider_options)\n        return config_dict\n    else:\n        # Lexicon 3.x\n        provider_config: dict[str, Any] = {}\n        provider_config.update(provider_options)\n        config_dict[lexicon_provider_name] = provider_config\n        return ConfigResolver().with_dict(config_dict).with_env()\n\n\nclass LexiconDNSAuthenticator(dns_common.DNSAuthenticator):\n    \"\"\"\n    Base class for a DNS authenticator that uses Lexicon client\n    as backend to execute DNS record updates\n    \"\"\"\n\n    def __init__(self, config: configuration.NamespaceConfig, name: str):\n        super().__init__(config, name)\n        self._provider_options: list[tuple[str, str, str]] = []\n        self._credentials: dns_common.CredentialsConfiguration\n\n    @property\n    @abc.abstractmethod\n    def _provider_name(self) -> str:\n        \"\"\"\n        The name of the Lexicon provider to use\n        \"\"\"\n\n    @property\n    def _ttl(self) -> int:\n        \"\"\"\n        Time to live to apply to the DNS records created by this Authenticator\n        \"\"\"\n        return 60\n\n    def _add_provider_option(self, creds_var_name: str, creds_var_label: str,\n                             lexicon_provider_option_name: str) -> None:\n        self._provider_options.append(\n            (creds_var_name, creds_var_label, lexicon_provider_option_name))\n\n    def _build_lexicon_config(self, domain: str) -> ConfigResolver:\n        if not hasattr(self, '_credentials'):  # pragma: no cover\n            self._setup_credentials()\n\n        dict_config = {\n            'domain': domain,\n            # We bypass Lexicon subdomain resolution by setting the 'delegated' field in the config\n            # to the value of the 'domain' field itself. Here we consider that the domain passed to\n            # _build_lexicon_config() is already the exact subdomain of the actual DNS zone to use.\n            'delegated': domain,\n            'provider_name': self._provider_name,\n            'ttl': self._ttl,\n            self._provider_name: {item[2]: self._credentials.conf(item[0])\n                                  for item in self._provider_options}\n        }\n        return ConfigResolver().with_dict(dict_config).with_env()\n\n    def _setup_credentials(self) -> None:\n        self._credentials = self._configure_credentials(\n            key='credentials',\n            label=f'Credentials INI file for {self._provider_name} DNS authenticator',\n            required_variables={item[0]: item[1] for item in self._provider_options},\n        )\n\n    def _perform(self, domain: str, validation_name: str, validation: str) -> None:\n        resolved_domain = self._resolve_domain(domain)\n\n        try:\n            with Client(self._build_lexicon_config(resolved_domain)) as operations:\n                operations.create_record(rtype='TXT', name=validation_name, content=validation)\n        except RequestException as e:\n            logger.debug('Encountered error adding TXT record: %s', e, exc_info=True)\n            raise errors.PluginError('Error adding TXT record: {0}'.format(e))\n\n    def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:\n        try:\n            resolved_domain = self._resolve_domain(domain)\n        except errors.PluginError as e:\n            logger.debug('Encountered error finding domain_id during deletion: %s', e,\n                         exc_info=True)\n            return\n\n        try:\n            with Client(self._build_lexicon_config(resolved_domain)) as operations:\n                operations.delete_record(rtype='TXT', name=validation_name, content=validation)\n        except RequestException as e:\n            logger.debug('Encountered error deleting TXT record: %s', e, exc_info=True)\n\n    def _resolve_domain(self, domain: str) -> str:\n        domain_name_guesses = dns_common.base_domain_name_guesses(domain)\n\n        for domain_name in domain_name_guesses:\n            try:\n                # Using client as a context manager requires `dns-lexicon>=3.14` and we may want to\n                # provide better checks and error handling around this in the future.\n                with Client(self._build_lexicon_config(domain_name)):\n                    return domain_name\n            except HTTPError as e:\n                result1 = self._handle_http_error(e, domain_name)\n\n                if result1:\n                    raise result1\n            except Exception as e:  # pylint: disable=broad-except\n                result2 = self._handle_general_error(e, domain_name)\n\n                if result2:\n                    raise result2  # pylint: disable=raising-bad-type\n\n        raise errors.PluginError('Unable to determine zone identifier for {0} using zone names: {1}'\n                                 .format(domain, domain_name_guesses))\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        return errors.PluginError('Error determining zone identifier for {0}: {1}.'\n                                  .format(domain_name, e))\n\n    def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:\n        if not str(e).startswith('No domain found'):\n            return errors.PluginError('Unexpected error determining zone identifier for {0}: {1}'\n                                      .format(domain_name, e))\n        return None\n\n\n# This class takes a similar approach to the cryptography project to deprecate attributes\n# in public modules. See the _ModuleWithDeprecation class here:\n# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129\nclass _DeprecationModule:\n    \"\"\"\n    Internal class delegating to a module, and displaying warnings when attributes\n    related to deprecated attributes in the current module.\n    \"\"\"\n    def __init__(self, module: ModuleType):\n        self.__dict__['_module'] = module\n\n    def __getattr__(self, attr: str) -> Any:\n        if attr in ('LexiconClient', 'build_lexicon_config'):\n            warnings.warn(f'{attr} attribute in {__name__} module is deprecated '\n                          'and will be removed soon.',\n                          DeprecationWarning, stacklevel=2)\n        return getattr(self._module, attr)\n\n    def __setattr__(self, attr: str, value: Any) -> None:  # pragma: no cover\n        setattr(self._module, attr, value)\n\n    def __delattr__(self, attr: str) -> Any:  # pragma: no cover\n        delattr(self._module, attr)\n\n    def __dir__(self) -> list[str]:  # pragma: no cover\n        return ['_module'] + dir(self._module)\n\n\n# Patching ourselves to warn about deprecation and planned removal of some elements in the module.\nsys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__]))\n"
  },
  {
    "path": "certbot/src/certbot/plugins/dns_test_common.py",
    "content": "\"\"\"Base test class for DNS authenticators.\"\"\"\nfrom typing import Any\nfrom typing import Mapping\nfrom typing import Protocol\nfrom unittest import mock\n\nimport configobj\nimport josepy as jose\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot.compat import filesystem\nfrom certbot.plugins.dns_common import DNSAuthenticator\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\nDOMAIN = 'example.com'\nKEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass _AuthenticatorCallableTestCase(Protocol):\n    \"\"\"Protocol describing a TestCase able to call a real DNSAuthenticator instance.\"\"\"\n    auth: DNSAuthenticator\n\n    def assertTrue(self, *unused_args: Any) -> None:\n        \"\"\"\n        See\n        https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertTrue\n        \"\"\"\n\n    def assertEqual(self, *unused_args: Any) -> None:\n        \"\"\"\n        See\n        https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertEqual\n        \"\"\"\n\n    def assertRaises(self, *unused_args: Any) -> None:\n        \"\"\"\n        See\n        https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises\n        \"\"\"\n\n\nclass BaseAuthenticatorTest:\n    \"\"\"\n    A base test class to reduce duplication between test code for DNS Authenticator Plugins.\n\n    Assumes:\n     * That subclasses also subclass unittest.TestCase\n     * That the authenticator is stored as self.auth\n    \"\"\"\n\n    achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n        challb=acme_util.DNS01,\n        identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN),\n        account_key=KEY)\n\n    def test_more_info(self: _AuthenticatorCallableTestCase) -> None:\n        self.assertTrue(isinstance(self.auth.more_info(), str))  # pylint: disable=no-member\n\n    def test_get_chall_pref(self: _AuthenticatorCallableTestCase) -> None:\n        self.assertEqual(self.auth.get_chall_pref(\"example.org\"), [challenges.DNS01])  # pylint: disable=no-member\n\n    def test_parser_arguments(self: _AuthenticatorCallableTestCase) -> None:\n        m = mock.MagicMock()\n        self.auth.add_parser_arguments(m)  # pylint: disable=no-member\n\n        m.assert_any_call('propagation-seconds', type=int, default=mock.ANY, help=mock.ANY)\n\n\ndef write(values: Mapping[str, Any], path: str) -> None:\n    \"\"\"Write the specified values to a config file.\n\n    :param dict values: A map of values to write.\n    :param str path: Where to write the values.\n    \"\"\"\n\n    config = configobj.ConfigObj()\n\n    for key in values:\n        config[key] = values[key]\n\n    with open(path, \"wb\") as f:\n        config.write(outfile=f)\n\n    filesystem.chmod(path, 0o600)\n"
  },
  {
    "path": "certbot/src/certbot/plugins/dns_test_common_lexicon.py",
    "content": "\"\"\"Base test class for DNS authenticators built on Lexicon.\"\"\"\nimport contextlib\nimport sys\nfrom types import ModuleType\nfrom typing import Any\nfrom typing import cast\nfrom typing import Generator\nfrom typing import Protocol\nfrom unittest import mock\nfrom unittest.mock import MagicMock\nimport warnings\n\nimport josepy as jose\nfrom requests import Response\nfrom requests.exceptions import HTTPError\nfrom requests.exceptions import RequestException\n\nfrom certbot import errors\nfrom certbot.achallenges import AnnotatedChallenge\nfrom certbot.plugins import dns_test_common\n\nwith warnings.catch_warnings():\n    warnings.filterwarnings(\"ignore\", category=DeprecationWarning)\n    from certbot.plugins.dns_common_lexicon import LexiconClient\n\nfrom certbot.plugins.dns_test_common import _AuthenticatorCallableTestCase\nfrom certbot.tests import util as test_util\n\nDOMAIN = 'example.com'\nKEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\nDOMAIN_NOT_FOUND = Exception('No domain found')\nGENERIC_ERROR = RequestException\nLOGIN_ERROR = HTTPError('400 Client Error: ...', response=Response())\nUNKNOWN_LOGIN_ERROR = HTTPError('500 Surprise! Error: ...', response=Response())\n\n\nclass _AuthenticatorCallableLexiconTestCase(_AuthenticatorCallableTestCase, Protocol):\n    \"\"\"\n    Protocol describing a TestCase suitable to test challenges against\n    a mocked LexiconClient instance.\n    \"\"\"\n    mock_client: MagicMock\n    achall: AnnotatedChallenge\n\n\nclass _LexiconAwareTestCase(Protocol):\n    \"\"\"\n    Protocol describing a TestCase suitable to test a real LexiconClient instance.\n    \"\"\"\n    client: LexiconClient\n    provider_mock: MagicMock\n\n    record_prefix: str\n    record_name: str\n    record_content: str\n\n    DOMAIN_NOT_FOUND: Exception\n    GENERIC_ERROR: Exception\n    LOGIN_ERROR: Exception\n    UNKNOWN_LOGIN_ERROR: Exception\n\n    def assertRaises(self, *unused_args: Any) -> None:\n        \"\"\"\n        See\n        https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertRaises\n        \"\"\"\n\n\n# These classes are intended to be subclassed/mixed in, so not all members are defined.\n# pylint: disable=no-member\n\nclass BaseLexiconAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):  # pragma: no cover\n\n    @test_util.patch_display_util()\n    def test_perform(self: _AuthenticatorCallableLexiconTestCase,\n                     unused_mock_get_utility: Any) -> None:\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]\n        self.assertEqual(expected, self.mock_client.mock_calls)\n\n    def test_cleanup(self: _AuthenticatorCallableLexiconTestCase) -> None:\n        self.auth._attempt_cleanup = True  # _attempt_cleanup | pylint: disable=protected-access\n        self.auth.cleanup([self.achall])\n\n        expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]\n        self.assertEqual(expected, self.mock_client.mock_calls)\n\n\nclass BaseLexiconClientTest:  # pragma: no cover\n    DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND\n    GENERIC_ERROR = GENERIC_ERROR\n    LOGIN_ERROR = LOGIN_ERROR\n    UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR\n\n    record_prefix = \"_acme-challenge\"\n    record_name = record_prefix + \".\" + DOMAIN\n    record_content = \"bar\"\n\n    def test_add_txt_record(self: _LexiconAwareTestCase) -> None:\n        self.client.add_txt_record(DOMAIN, self.record_name, self.record_content)\n\n        self.provider_mock.create_record.assert_called_with(rtype='TXT',\n                                                            name=self.record_name,\n                                                            content=self.record_content)\n\n    def test_add_txt_record_try_twice_to_find_domain(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND, '']\n\n        self.client.add_txt_record(DOMAIN, self.record_name, self.record_content)\n\n        self.provider_mock.create_record.assert_called_with(rtype='TXT',\n                                                            name=self.record_name,\n                                                            content=self.record_content)\n\n    def test_add_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND,\n                                                       self.DOMAIN_NOT_FOUND,\n                                                       self.DOMAIN_NOT_FOUND,]\n\n        self.assertRaises(errors.PluginError,\n                          self.client.add_txt_record,\n                          DOMAIN, self.record_name, self.record_content)\n\n    def test_add_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR\n\n        self.assertRaises(errors.PluginError,\n                          self.client.add_txt_record,\n                          DOMAIN, self.record_name, self.record_content)\n\n    def test_add_txt_record_fail_to_authenticate_with_unknown_error(\n            self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR\n\n        self.assertRaises(errors.PluginError,\n                          self.client.add_txt_record,\n                          DOMAIN, self.record_name, self.record_content)\n\n    def test_add_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR\n\n        self.assertRaises(errors.PluginError,\n                          self.client.add_txt_record,\n                          DOMAIN, self.record_name, self.record_content)\n\n    def test_add_txt_record_error_adding_record(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.create_record.side_effect = self.GENERIC_ERROR\n\n        self.assertRaises(errors.PluginError,\n                          self.client.add_txt_record,\n                          DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record(self: _LexiconAwareTestCase) -> None:\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n        self.provider_mock.delete_record.assert_called_with(rtype='TXT',\n                                                            name=self.record_name,\n                                                            content=self.record_content)\n\n    def test_del_txt_record_fail_to_find_domain(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = [self.DOMAIN_NOT_FOUND,\n                                                       self.DOMAIN_NOT_FOUND,\n                                                       self.DOMAIN_NOT_FOUND, ]\n\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_fail_to_authenticate(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.LOGIN_ERROR\n\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_fail_to_authenticate_with_unknown_error(\n            self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.UNKNOWN_LOGIN_ERROR\n\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_error_finding_domain(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.authenticate.side_effect = self.GENERIC_ERROR\n\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_error_deleting_record(self: _LexiconAwareTestCase) -> None:\n        self.provider_mock.delete_record.side_effect = self.GENERIC_ERROR\n\n        self.client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n\nclass _BaseLexiconDNSAuthenticatorTestProto(_AuthenticatorCallableTestCase, Protocol):\n    \"\"\"Protocol for BaseLexiconDNSAuthenticatorTest instances\"\"\"\n    DOMAIN_NOT_FOUND: Exception\n    GENERIC_ERROR: Exception\n    LOGIN_ERROR: Exception\n    UNKNOWN_LOGIN_ERROR: Exception\n\n    achall: AnnotatedChallenge\n\n\nclass BaseLexiconDNSAuthenticatorTest(dns_test_common.BaseAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = DOMAIN_NOT_FOUND\n    GENERIC_ERROR = GENERIC_ERROR\n    LOGIN_ERROR = LOGIN_ERROR\n    UNKNOWN_LOGIN_ERROR = UNKNOWN_LOGIN_ERROR\n\n    def test_perform_succeed(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, mock_operations):\n                self.auth.perform([self.achall])\n\n        mock_client.assert_called()\n        config = mock_client.call_args[0][0]\n        self.assertEqual(DOMAIN, config.resolve('lexicon:domain'))\n\n        mock_operations.create_record.assert_called_with(\n            rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY)\n\n    def test_perform_with_one_domain_resolution_failure_succeed(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, mock_operations):\n                mock_client.return_value.__enter__.side_effect = [\n                    self.DOMAIN_NOT_FOUND,  # First resolution domain attempt\n                    mock_operations,  # Second resolution domain attempt\n                    mock_operations,  # Create record operation\n                ]\n                self.auth.perform([self.achall])\n\n    def test_perform_with_two_domain_resolution_failures_raise(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, _):\n                mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND\n                self.assertRaises(errors.PluginError,\n                                  self.auth.perform,\n                                  [self.achall])\n\n    def test_perform_with_domain_resolution_general_failure_raise(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, _):\n                mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR\n                self.assertRaises(errors.PluginError,\n                                  self.auth.perform,\n                                  [self.achall])\n\n    def test_perform_with_auth_failure_raise(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, _):\n                mock_client.side_effect = self.LOGIN_ERROR\n                self.assertRaises(errors.PluginError,\n                                  self.auth.perform,\n                                  [self.achall])\n\n    def test_perform_with_unknown_auth_failure_raise(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (mock_client, _):\n                mock_client.side_effect = self.UNKNOWN_LOGIN_ERROR\n                self.assertRaises(errors.PluginError,\n                                  self.auth.perform,\n                                  [self.achall])\n\n    def test_perform_with_create_record_failure_raise(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with test_util.patch_display_util():\n            with _patch_lexicon_client() as (_, mock_operations):\n                mock_operations.create_record.side_effect = self.GENERIC_ERROR\n                self.assertRaises(errors.PluginError,\n                                  self.auth.perform,\n                                  [self.achall])\n\n    def test_cleanup_success(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        self.auth._attempt_cleanup = True  # _attempt_cleanup | pylint: disable=protected-access\n        with _patch_lexicon_client() as (mock_client, mock_operations):\n            self.auth.cleanup([self.achall])\n\n        mock_client.assert_called()\n        config = mock_client.call_args[0][0]\n        self.assertEqual(DOMAIN, config.resolve('lexicon:domain'))\n\n        mock_operations.delete_record.assert_called_with(\n            rtype='TXT', name=f'_acme-challenge.{DOMAIN}', content=mock.ANY)\n\n    def test_cleanup_with_auth_failure_ignore(self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with _patch_lexicon_client() as (mock_client, _):\n            mock_client.side_effect = self.LOGIN_ERROR\n            self.auth.cleanup([self.achall])\n\n    def test_cleanup_with_unknown_auth_failure_ignore(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with _patch_lexicon_client() as (mock_client, _):\n            mock_client.side_effect = self.LOGIN_ERROR\n            self.auth.cleanup([self.achall])\n\n    def test_cleanup_with_domain_resolution_failure_ignore(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with _patch_lexicon_client() as (mock_client, _):\n            mock_client.return_value.__enter__.side_effect = self.DOMAIN_NOT_FOUND\n            self.auth.cleanup([self.achall])\n\n    def test_cleanup_with_domain_resolution_general_failure_ignore(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with _patch_lexicon_client() as (mock_client, _):\n            mock_client.return_value.__enter__.side_effect = self.GENERIC_ERROR\n            self.auth.cleanup([self.achall])\n\n    def test_cleanup_with_delete_record_failure_ignore(\n            self: _BaseLexiconDNSAuthenticatorTestProto) -> None:\n        with _patch_lexicon_client() as (_, mock_operations):\n            mock_operations.create_record.side_effect = self.GENERIC_ERROR\n            self.auth.cleanup([self.achall])\n\n\n@contextlib.contextmanager\ndef _patch_lexicon_client() -> Generator[tuple[MagicMock, MagicMock], None, None]:\n    with mock.patch('certbot.plugins.dns_common_lexicon.Client') as mock_client:\n        mock_operations = MagicMock()\n        mock_client.return_value.__enter__.return_value = mock_operations\n        yield mock_client, mock_operations\n\n\n# This class takes a similar approach to the cryptography project to deprecate attributes\n# in public modules. See the _ModuleWithDeprecation class here:\n# https://github.com/pyca/cryptography/blob/91105952739442a74582d3e62b3d2111365b0dc7/src/cryptography/utils.py#L129\nclass _DeprecationModule:\n    \"\"\"\n    Internal class delegating to a module, and displaying warnings when attributes\n    related to deprecated attributes in the current module.\n    \"\"\"\n    def __init__(self, module: ModuleType):\n        self.__dict__['_module'] = module\n\n    def __getattr__(self, attr: str) -> Any:\n        if attr in ('BaseLexiconAuthenticatorTest', 'BaseLexiconClientTest'):\n            warnings.warn(f'{attr} attribute in {__name__} module is deprecated '\n                          'and will be removed soon.',\n                          DeprecationWarning, stacklevel=2)\n        return getattr(self._module, attr)\n\n    def __setattr__(self, attr: str, value: Any) -> None:  # pragma: no cover\n        setattr(self._module, attr, value)\n\n    def __delattr__(self, attr: str) -> Any:  # pragma: no cover\n        delattr(self._module, attr)\n\n    def __dir__(self) -> list[str]:  # pragma: no cover\n        return ['_module'] + dir(self._module)\n\n\n# Patching ourselves to warn about deprecation and planned removal of some elements in the module.\nsys.modules[__name__] = cast(ModuleType, _DeprecationModule(sys.modules[__name__]))\n"
  },
  {
    "path": "certbot/src/certbot/plugins/enhancements.py",
    "content": "\"\"\"New interface style Certbot enhancements\"\"\"\nimport abc\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Generator\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot import configuration\nfrom certbot import interfaces\nfrom certbot._internal import constants\n\nENHANCEMENTS = [\"redirect\", \"ensure-http-header\", \"ocsp-stapling\"]\n\"\"\"List of possible :class:`certbot.interfaces.Installer`\nenhancements.\n\nList of expected options parameters:\n- redirect: None\n- ensure-http-header: name of header (i.e. Strict-Transport-Security)\n- ocsp-stapling: certificate chain file path\n\n\"\"\"\n\n\ndef enabled_enhancements(\n        config: configuration.NamespaceConfig) -> Generator[dict[str, Any], None, None]:\n    \"\"\"\n    Generator to yield the enabled new style enhancements.\n\n    :param config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n    \"\"\"\n    for enh in _INDEX:\n        if getattr(config, enh[\"cli_dest\"]):\n            yield enh\n\n\ndef are_requested(config: configuration.NamespaceConfig) -> bool:\n    \"\"\"\n    Checks if one or more of the requested enhancements are those of the new\n    enhancement interfaces.\n\n    :param config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n    \"\"\"\n    return any(enabled_enhancements(config))\n\n\ndef are_supported(config: configuration.NamespaceConfig,\n                  installer: Optional[interfaces.Installer]) -> bool:\n    \"\"\"\n    Checks that all of the requested enhancements are supported by the\n    installer.\n\n    :param config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :returns: If all the requested enhancements are supported by the installer\n    :rtype: bool\n    \"\"\"\n    for enh in enabled_enhancements(config):\n        if not isinstance(installer, enh[\"class\"]):\n            return False\n    return True\n\n\ndef enable(lineage: Optional[interfaces.RenewableCert], domains: Iterable[str],\n           installer: Optional[interfaces.Installer],\n           config: configuration.NamespaceConfig) -> None:\n    \"\"\"\n    Run enable method for each requested enhancement that is supported.\n\n    :param lineage: Certificate lineage object\n    :type lineage: certbot.interfaces.RenewableCert\n\n    :param domains: List of domains in certificate to enhance\n    :type domains: str\n\n    :param installer: Installer object\n    :type installer: interfaces.Installer\n\n    :param config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n    \"\"\"\n    if installer:\n        for enh in enabled_enhancements(config):\n            getattr(installer, enh[\"enable_function\"])(lineage, domains)\n\n\ndef populate_cli(add: Callable[..., None]) -> None:\n    \"\"\"\n    Populates the command line flags for certbot._internal.cli.HelpfulParser\n\n    :param add: Add function of certbot._internal.cli.HelpfulParser\n    :type add: func\n    \"\"\"\n    for enh in _INDEX:\n        add(enh[\"cli_groups\"], enh[\"cli_flag\"], action=enh[\"cli_action\"],\n            dest=enh[\"cli_dest\"], default=enh[\"cli_flag_default\"],\n            help=enh[\"cli_help\"])\n\n\nclass AutoHSTSEnhancement(object, metaclass=abc.ABCMeta):\n    \"\"\"\n    Enhancement interface that installer plugins can implement in order to\n    provide functionality that configures the software to have a\n    'Strict-Transport-Security' with initially low max-age value that will\n    increase over time.\n\n    The plugins implementing new style enhancements are responsible of handling\n    the saving of configuration checkpoints as well as calling possible restarts\n    of managed software themselves. For update_autohsts method, the installer may\n    have to call prepare() to finalize the plugin initialization.\n\n    Methods:\n        enable_autohsts is called when the header is initially installed using a\n        low max-age value.\n\n        update_autohsts is called every time when Certbot is run using 'renew'\n        verb. The max-age value should be increased over time using this method.\n\n        deploy_autohsts is called for every lineage that has had its certificate\n        renewed. A long HSTS max-age value should be set here, as we should be\n        confident that the user is able to automatically renew their certificates.\n\n\n    \"\"\"\n\n    @abc.abstractmethod\n    def update_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None:\n        \"\"\"\n        Gets called for each lineage every time Certbot is run with 'renew' verb.\n        Implementation of this method should increase the max-age value.\n\n        :param lineage: Certificate lineage object\n        :type lineage: certbot.interfaces.RenewableCert\n\n        .. note:: prepare() method inherited from `interfaces.Plugin` might need\n            to be called manually within implementation of this interface method\n            to finalize the plugin initialization.\n        \"\"\"\n\n    @abc.abstractmethod\n    def deploy_autohsts(self, lineage: interfaces.RenewableCert, *args: Any, **kwargs: Any) -> None:\n        \"\"\"\n        Gets called for a lineage when its certificate is successfully renewed.\n        Long max-age value should be set in implementation of this method.\n\n        :param lineage: Certificate lineage object\n        :type lineage: certbot.interfaces.RenewableCert\n        \"\"\"\n\n    @abc.abstractmethod\n    def enable_autohsts(self, lineage: Optional[interfaces.RenewableCert], domains: Iterable[str],\n                        *args: Any, **kwargs: Any) -> None:\n        \"\"\"\n        Enables the AutoHSTS enhancement, installing\n        Strict-Transport-Security header with a low initial value to be increased\n        over the subsequent runs of Certbot renew.\n\n        :param lineage: Certificate lineage object\n        :type lineage: certbot.interfaces.RenewableCert\n\n        :param domains: List of domains in certificate to enhance\n        :type domains: `list` of `str`\n        \"\"\"\n\n\n# This is used to configure internal new style enhancements in Certbot. These\n# enhancement interfaces need to be defined in this file. Please do not modify\n# this list from plugin code.\n_INDEX: list[dict[str, Any]] = [\n    {\n        \"name\": \"AutoHSTS\",\n        \"cli_help\": \"Gradually increasing max-age value for HTTP Strict Transport \"+\n                    \"Security security header\",\n        \"cli_flag\": \"--auto-hsts\",\n        \"cli_flag_default\": constants.CLI_DEFAULTS[\"auto_hsts\"],\n        \"cli_groups\": [\"security\", \"enhance\"],\n        \"cli_dest\": \"auto_hsts\",\n        \"cli_action\": \"store_true\",\n        \"class\": AutoHSTSEnhancement,\n        \"updater_function\": \"update_autohsts\",\n        \"deployer_function\": \"deploy_autohsts\",\n        \"enable_function\": \"enable_autohsts\"\n    }\n]\n"
  },
  {
    "path": "certbot/src/certbot/plugins/storage.py",
    "content": "\"\"\"Plugin storage class.\"\"\"\nimport json\nimport logging\nfrom typing import Any\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n\nclass PluginStorage:\n    \"\"\"Class implementing storage functionality for plugins\"\"\"\n\n    def __init__(self, config: configuration.NamespaceConfig, classkey: str) -> None:\n        \"\"\"Initializes PluginStorage object storing required configuration\n        options.\n\n        :param .configuration.NamespaceConfig config: Configuration object\n        :param str classkey: class name to use as root key in storage file\n\n        \"\"\"\n\n        self._config = config\n        self._classkey = classkey\n        self._initialized = False\n        self._data: dict\n        self._storagepath: str\n\n    def _initialize_storage(self) -> None:\n        \"\"\"Initializes PluginStorage data and reads current state from the disk\n        if the storage json exists.\"\"\"\n\n        self._storagepath = os.path.join(self._config.config_dir, \".pluginstorage.json\")\n        self._load()\n        self._initialized = True\n\n    def _load(self) -> None:\n        \"\"\"Reads PluginStorage content from the disk to a dict structure\n\n        :raises .errors.PluginStorageError: when unable to open or read the file\n        \"\"\"\n        data: dict[str, Any] = {}\n        filedata = \"\"\n        try:\n            with open(self._storagepath, 'r') as fh:\n                filedata = fh.read()\n        except OSError as e:\n            errmsg = \"Could not read PluginStorage data file: {0} : {1}\".format(\n                self._storagepath, str(e))\n            if os.path.isfile(self._storagepath):\n                # Only error out if file exists, but cannot be read\n                logger.error(errmsg)\n                raise errors.PluginStorageError(errmsg)\n        try:\n            data = json.loads(filedata)\n        except ValueError:\n            if not filedata:\n                logger.debug(\"Plugin storage file %s was empty, no values loaded\",\n                             self._storagepath)\n            else:\n                errmsg = \"PluginStorage file {0} is corrupted.\".format(\n                    self._storagepath)\n                logger.error(errmsg)\n                raise errors.PluginStorageError(errmsg)\n        self._data = data\n\n    def save(self) -> None:\n        \"\"\"Saves PluginStorage content to disk\n\n        :raises .errors.PluginStorageError: when unable to serialize the data\n            or write it to the filesystem\n        \"\"\"\n        if not self._initialized:\n            errmsg = \"Unable to save, no values have been added to PluginStorage.\"\n            logger.error(errmsg)\n            raise errors.PluginStorageError(errmsg)\n\n        try:\n            serialized = json.dumps(self._data)\n        except TypeError as e:\n            errmsg = \"Could not serialize PluginStorage data: {0}\".format(\n                str(e))\n            logger.error(errmsg)\n            raise errors.PluginStorageError(errmsg)\n        try:\n            with os.fdopen(filesystem.open(\n                    self._storagepath,\n                    os.O_WRONLY | os.O_CREAT | os.O_TRUNC,\n                    0o600), 'w') as fh:\n                fh.write(serialized)\n        except OSError as e:\n            errmsg = \"Could not write PluginStorage data to file {0} : {1}\".format(\n                self._storagepath, str(e))\n            logger.error(errmsg)\n            raise errors.PluginStorageError(errmsg)\n\n    def put(self, key: str, value: Any) -> None:\n        \"\"\"Put configuration value to PluginStorage\n\n        :param str key: Key to store the value to\n        :param value: Data to store\n        \"\"\"\n        if not self._initialized:\n            self._initialize_storage()\n\n        if self._classkey not in self._data:\n            self._data[self._classkey] = {}\n        self._data[self._classkey][key] = value\n\n    def fetch(self, key: str) -> Any:\n        \"\"\"Get configuration value from PluginStorage\n\n        :param str key: Key to get value from the storage\n\n        :raises KeyError: If the key doesn't exist in the storage\n        \"\"\"\n        if not self._initialized:\n            self._initialize_storage()\n\n        return self._data[self._classkey][key]\n"
  },
  {
    "path": "certbot/src/certbot/plugins/util.py",
    "content": "\"\"\"Plugin utilities.\"\"\"\nimport logging\n\nfrom certbot import util\nfrom certbot.compat import os\nfrom certbot.compat.misc import STANDARD_BINARY_DIRS\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_prefixes(path: str) -> list[str]:\n    \"\"\"Retrieves all possible path prefixes of a path, in descending order\n    of length. For instance:\n\n      * (Linux) `/a/b/c` returns `['/a/b/c', '/a/b', '/a', '/']`\n      * (Windows) `C:\\\\a\\\\b\\\\c` returns `['C:\\\\a\\\\b\\\\c', 'C:\\\\a\\\\b', 'C:\\\\a', 'C:']`\n\n    :param str path: the path to break into prefixes\n\n    :returns: all possible path prefixes of given path in descending order\n    :rtype: `list` of `str`\n    \"\"\"\n    prefix = os.path.normpath(path)\n    prefixes: list[str] = []\n    while prefix:\n        prefixes.append(prefix)\n        prefix, _ = os.path.split(prefix)\n        # break once we hit the root path\n        if prefix == prefixes[-1]:\n            break\n    return prefixes\n\n\ndef path_surgery(cmd: str) -> bool:\n    \"\"\"Attempt to perform PATH surgery to find cmd\n\n    Mitigates https://github.com/certbot/certbot/issues/1833\n\n    :param str cmd: the command that is being searched for in the PATH\n\n    :returns: True if the operation succeeded, False otherwise\n    \"\"\"\n    path = os.environ[\"PATH\"]\n    added = []\n    for d in STANDARD_BINARY_DIRS:\n        if d not in path:\n            path += os.pathsep + d\n            added.append(d)\n\n    if any(added):\n        logger.debug(\"Can't find %s, attempting PATH mitigation by adding %s\",\n                     cmd, os.pathsep.join(added))\n        os.environ[\"PATH\"] = path\n\n    if util.exe_exists(cmd):\n        return True\n    expanded = \" expanded\" if any(added) else \"\"\n    logger.debug(\"Failed to find executable %s in%s PATH: %s\", cmd,\n                 expanded, path)\n    return False\n"
  },
  {
    "path": "certbot/src/certbot/py.typed",
    "content": ""
  },
  {
    "path": "certbot/src/certbot/reverter.py",
    "content": "\"\"\"Reverter class saves configuration checkpoints and allows for recovery.\"\"\"\nimport csv\nimport glob\nimport logging\nimport shutil\nimport time\nimport traceback\nfrom typing import Iterable\nfrom typing import TextIO\n\nfrom certbot import configuration\nfrom certbot import errors\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n\nclass Reverter:\n    \"\"\"Reverter Class - save and revert configuration checkpoints.\n\n    This class can be used by the plugins, especially Installers, to\n    undo changes made to the user's system. Modifications to files and\n    commands to do undo actions taken by the plugin should be registered\n    with this class before the action is taken.\n\n    Once a change has been registered with this class, there are three\n    states the change can be in. First, the change can be a temporary\n    change. This should be used for changes that will soon be reverted,\n    such as config changes for the purpose of solving a challenge.\n    Changes are added to this state through calls to\n    :func:`~add_to_temp_checkpoint` and reverted when\n    :func:`~revert_temporary_config` or :func:`~recovery_routine` is\n    called.\n\n    The second state a change can be in is in progress. These changes\n    are not temporary, however, they also have not been finalized in a\n    checkpoint. A change must become in progress before it can be\n    finalized. Changes are added to this state through calls to\n    :func:`~add_to_checkpoint` and reverted when\n    :func:`~recovery_routine` is called.\n\n    The last state a change can be in is finalized in a checkpoint. A\n    change is put into this state by first becoming an in progress\n    change and then calling :func:`~finalize_checkpoint`. Changes\n    in this state can be reverted through calls to\n    :func:`~rollback_checkpoints`.\n\n    As a final note, creating new files and registering undo commands\n    are handled specially and use the methods\n    :func:`~register_file_creation` and :func:`~register_undo_command`\n    respectively. Both of these methods can be used to create either\n    temporary or in progress changes.\n\n    .. note:: Consider moving everything over to CSV format.\n\n    :param config: Configuration.\n    :type config: :class:`certbot.configuration.NamespaceConfig`\n\n    \"\"\"\n    def __init__(self, config: configuration.NamespaceConfig) -> None:\n        self.config = config\n\n        util.make_or_verify_dir(\n            config.backup_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions)\n\n    def revert_temporary_config(self) -> None:\n        \"\"\"Reload users original configuration files after a temporary save.\n\n        This function should reinstall the users original configuration files\n        for all saves with temporary=True\n\n        :raises .ReverterError: when unable to revert config\n\n        \"\"\"\n        if os.path.isdir(self.config.temp_checkpoint_dir):\n            try:\n                self._recover_checkpoint(self.config.temp_checkpoint_dir)\n            except errors.ReverterError:\n                # We have a partial or incomplete recovery\n                logger.critical(\n                    \"Incomplete or failed recovery for %s\",\n                    self.config.temp_checkpoint_dir,\n                )\n                raise errors.ReverterError(\"Unable to revert temporary config\")\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        \"\"\"Revert 'rollback' number of configuration checkpoints.\n\n        :param int rollback: Number of checkpoints to reverse. A str num will be\n           cast to an integer. So \"2\" is also acceptable.\n\n        :raises .ReverterError:\n            if there is a problem with the input or if the function is\n            unable to correctly revert the configuration checkpoints\n\n        \"\"\"\n        try:\n            rollback = int(rollback)\n        except ValueError:\n            logger.error(\"Rollback argument must be a positive integer\")\n            raise errors.ReverterError(\"Invalid Input\")\n        # Sanity check input\n        if rollback < 0:\n            logger.error(\"Rollback argument must be a positive integer\")\n            raise errors.ReverterError(\"Invalid Input\")\n\n        backups = os.listdir(self.config.backup_dir)\n        backups.sort()\n\n        if not backups:\n            logger.warning(\n                \"Certbot hasn't modified your configuration, so rollback \"\n                \"isn't available.\")\n        elif len(backups) < rollback:\n            logger.warning(\"Unable to rollback %d checkpoints, only %d exist\",\n                           rollback, len(backups))\n\n        while rollback > 0 and backups:\n            cp_dir = os.path.join(self.config.backup_dir, backups.pop())\n            try:\n                self._recover_checkpoint(cp_dir)\n            except errors.ReverterError:\n                logger.critical(\"Failed to load checkpoint during rollback\")\n                raise errors.ReverterError(\n                    \"Unable to load checkpoint during rollback\")\n            rollback -= 1\n\n    def add_to_temp_checkpoint(self, save_files: set[str], save_notes: str) -> None:\n        \"\"\"Add files to temporary checkpoint.\n\n        :param set save_files: set of filepaths to save\n        :param str save_notes: notes about changes during the save\n\n        \"\"\"\n        self._add_to_checkpoint_dir(\n            self.config.temp_checkpoint_dir, save_files, save_notes)\n\n    def add_to_checkpoint(self, save_files: set[str], save_notes: str) -> None:\n        \"\"\"Add files to a permanent checkpoint.\n\n        :param set save_files: set of filepaths to save\n        :param str save_notes: notes about changes during the save\n\n        \"\"\"\n        # Check to make sure we are not overwriting a temp file\n        self._check_tempfile_saves(save_files)\n        self._add_to_checkpoint_dir(\n            self.config.in_progress_dir, save_files, save_notes)\n\n    def _add_to_checkpoint_dir(self, cp_dir: str, save_files: set[str], save_notes: str) -> None:\n        \"\"\"Add save files to checkpoint directory.\n\n        :param str cp_dir: Checkpoint directory filepath\n        :param set save_files: set of files to save\n        :param str save_notes: notes about changes made during the save\n\n        :raises IOError: if unable to open cp_dir + FILEPATHS file\n        :raises .ReverterError: if unable to add checkpoint\n\n        \"\"\"\n        util.make_or_verify_dir(\n            cp_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions)\n\n        op_fd, existing_filepaths = self._read_and_append(\n            os.path.join(cp_dir, \"FILEPATHS\"))\n\n        idx = len(existing_filepaths)\n\n        for filename in save_files:\n            # No need to copy/index already existing files\n            # The oldest copy already exists in the directory...\n            if filename not in existing_filepaths:\n                # Tag files with index so multiple files can\n                # have the same filename\n                logger.debug(\"Creating backup of %s\", filename)\n                try:\n                    shutil.copy2(filename, os.path.join(\n                        cp_dir, os.path.basename(filename) + \"_\" + str(idx)))\n                    op_fd.write('{0}\\n'.format(filename))\n                # https://stackoverflow.com/questions/4726260/effective-use-of-python-shutil-copy2\n                except OSError:\n                    op_fd.close()\n                    logger.error(\n                        \"Unable to add file %s to checkpoint %s\",\n                        filename, cp_dir)\n                    raise errors.ReverterError(\n                        \"Unable to add file {0} to checkpoint \"\n                        \"{1}\".format(filename, cp_dir))\n                idx += 1\n        op_fd.close()\n\n        with open(os.path.join(cp_dir, \"CHANGES_SINCE\"), \"a\") as notes_fd:\n            notes_fd.write(save_notes)\n\n    def _read_and_append(self, filepath: str) -> tuple[TextIO, list[str]]:\n        \"\"\"Reads the file lines and returns a file obj.\n\n        Read the file returning the lines, and a pointer to the end of the file.\n\n        \"\"\"\n        # pylint: disable=consider-using-with\n        # Open up filepath differently depending on if it already exists\n        if os.path.isfile(filepath):\n            op_fd = open(filepath, \"r+\")\n            lines = op_fd.read().splitlines()\n        else:\n            lines = []\n            op_fd = open(filepath, \"w\")\n\n        return op_fd, lines\n\n    def _recover_checkpoint(self, cp_dir: str) -> None:\n        \"\"\"Recover a specific checkpoint.\n\n        Recover a specific checkpoint provided by cp_dir\n        Note: this function does not reload augeas.\n\n        :param str cp_dir: checkpoint directory file path\n\n        :raises errors.ReverterError: If unable to recover checkpoint\n\n        \"\"\"\n        # Undo all commands\n        if os.path.isfile(os.path.join(cp_dir, \"COMMANDS\")):\n            self._run_undo_commands(os.path.join(cp_dir, \"COMMANDS\"))\n        # Revert all changed files\n        if os.path.isfile(os.path.join(cp_dir, \"FILEPATHS\")):\n            try:\n                with open(os.path.join(cp_dir, \"FILEPATHS\")) as paths_fd:\n                    filepaths = paths_fd.read().splitlines()\n                    for idx, path in enumerate(filepaths):\n                        shutil.copy2(os.path.join(\n                            cp_dir,\n                            os.path.basename(path) + \"_\" + str(idx)), path)\n            except OSError:\n                # This file is required in all checkpoints.\n                logger.error(\"Unable to recover files from %s\", cp_dir)\n                raise errors.ReverterError(f\"Unable to recover files from {cp_dir}\")\n\n        # Remove any newly added files if they exist\n        self._remove_contained_files(os.path.join(cp_dir, \"NEW_FILES\"))\n\n        try:\n            shutil.rmtree(cp_dir)\n        except OSError:\n            logger.error(\"Unable to remove directory: %s\", cp_dir)\n            raise errors.ReverterError(\n                \"Unable to remove directory: %s\" % cp_dir)\n\n    def _run_undo_commands(self, filepath: str) -> None:\n        \"\"\"Run all commands in a file.\"\"\"\n        # NOTE: csv module uses native strings. That is unicode on Python 3\n        # It is strongly advised to set newline = '' on Python 3 with CSV,\n        # and it fixes problems on Windows.\n        kwargs = {'newline': ''}\n        with open(filepath, 'r', **kwargs) as csvfile:  # type: ignore\n            csvreader = csv.reader(csvfile)\n            for command in reversed(list(csvreader)):\n                try:\n                    util.run_script(command)\n                except errors.SubprocessError:\n                    logger.error(\n                        \"Unable to run undo command: %s\", \" \".join(command))\n\n    def _check_tempfile_saves(self, save_files: set[str]) -> None:\n        \"\"\"Verify save isn't overwriting any temporary files.\n\n        :param set save_files: Set of files about to be saved.\n\n        :raises certbot.errors.ReverterError:\n            when save is attempting to overwrite a temporary file.\n\n        \"\"\"\n        protected_files = []\n\n        # Get temp modified files\n        temp_path = os.path.join(self.config.temp_checkpoint_dir, \"FILEPATHS\")\n        if os.path.isfile(temp_path):\n            with open(temp_path, \"r\") as protected_fd:\n                protected_files.extend(protected_fd.read().splitlines())\n\n        # Get temp new files\n        new_path = os.path.join(self.config.temp_checkpoint_dir, \"NEW_FILES\")\n        if os.path.isfile(new_path):\n            with open(new_path, \"r\") as protected_fd:\n                protected_files.extend(protected_fd.read().splitlines())\n\n        # Verify no save_file is in protected_files\n        for filename in protected_files:\n            if filename in save_files:\n                raise errors.ReverterError(f\"Attempting to overwrite challenge file - {filename}\")\n\n    def register_file_creation(self, temporary: bool, *files: str) -> None:\n        r\"\"\"Register the creation of all files during certbot execution.\n\n        Call this method before writing to the file to make sure that the\n        file will be cleaned up if the program exits unexpectedly.\n        (Before a save occurs)\n\n        :param bool temporary: If the file creation registry is for\n            a temp or permanent save.\n        :param \\*files: file paths (str) to be registered\n\n        :raises certbot.errors.ReverterError: If\n            call does not contain necessary parameters or if the file creation\n            is unable to be registered.\n\n        \"\"\"\n        # Make sure some files are provided... as this is an error\n        # Made this mistake in my initial implementation of apache.dvsni.py\n        if not files:\n            raise errors.ReverterError(\"Forgot to provide files to registration call\")\n\n        cp_dir = self._get_cp_dir(temporary)\n\n        # Append all new files (that aren't already registered)\n        new_fd = None\n        try:\n            new_fd, ex_files = self._read_and_append(os.path.join(cp_dir, \"NEW_FILES\"))\n\n            for path in files:\n                if path not in ex_files:\n                    new_fd.write(\"{0}\\n\".format(path))\n        except OSError:\n            logger.error(\"Unable to register file creation(s) - %s\", files)\n            raise errors.ReverterError(\n                \"Unable to register file creation(s) - {0}\".format(files))\n        finally:\n            if new_fd is not None:\n                new_fd.close()\n\n    def register_undo_command(self, temporary: bool, command: Iterable[str]) -> None:\n        \"\"\"Register a command to be run to undo actions taken.\n\n        .. warning:: This function does not enforce order of operations in terms\n            of file modification vs. command registration.  All undo commands\n            are run first before all normal files are reverted to their previous\n            state.  If you need to maintain strict order, you may create\n            checkpoints before and after the the command registration. This\n            function may be improved in the future based on demand.\n\n        :param bool temporary: Whether the command should be saved in the\n            IN_PROGRESS or TEMPORARY checkpoints.\n        :param command: Command to be run.\n        :type command: list of str\n\n        \"\"\"\n        commands_fp = os.path.join(self._get_cp_dir(temporary), \"COMMANDS\")\n        # It is strongly advised to set newline = '' on Python 3 with CSV,\n        # and it fixes problems on Windows.\n        kwargs = {'newline': ''}\n        try:\n            mode = \"a\" if os.path.isfile(commands_fp) else \"w\"\n            with open(commands_fp, mode, **kwargs) as f:  # type: ignore\n                csvwriter = csv.writer(f)\n                csvwriter.writerow(command)\n        except OSError:\n            logger.error(\"Unable to register undo command\")\n            raise errors.ReverterError(\n                \"Unable to register undo command.\")\n\n    def _get_cp_dir(self, temporary: bool) -> str:\n        \"\"\"Return the proper reverter directory.\"\"\"\n        if temporary:\n            cp_dir = self.config.temp_checkpoint_dir\n        else:\n            cp_dir = self.config.in_progress_dir\n\n        util.make_or_verify_dir(\n            cp_dir, constants.CONFIG_DIRS_MODE, self.config.strict_permissions)\n\n        return cp_dir\n\n    def recovery_routine(self) -> None:\n        \"\"\"Revert configuration to most recent finalized checkpoint.\n\n        Remove all changes (temporary and permanent) that have not been\n        finalized. This is useful to protect against crashes and other\n        execution interruptions.\n\n        :raises .errors.ReverterError: If unable to recover the configuration\n\n        \"\"\"\n        # First, any changes found in NamespaceConfig.temp_checkpoint_dir are removed,\n        # then IN_PROGRESS changes are removed The order is important.\n        # IN_PROGRESS is unable to add files that are already added by a TEMP\n        # change.  Thus TEMP must be rolled back first because that will be the\n        # 'latest' occurrence of the file.\n        self.revert_temporary_config()\n        if os.path.isdir(self.config.in_progress_dir):\n            try:\n                self._recover_checkpoint(self.config.in_progress_dir)\n            except errors.ReverterError:\n                # We have a partial or incomplete recovery\n                logger.critical(\"Incomplete or failed recovery for IN_PROGRESS \"\n                             \"checkpoint - %s\",\n                             self.config.in_progress_dir)\n                raise errors.ReverterError(\n                    \"Incomplete or failed recovery for IN_PROGRESS checkpoint \"\n                    \"- %s\" % self.config.in_progress_dir)\n\n    def _remove_contained_files(self, file_list: str) -> bool:\n        \"\"\"Erase all files contained within file_list.\n\n        :param str file_list: file containing list of file paths to be deleted\n\n        :returns: Success\n        :rtype: bool\n\n        :raises certbot.errors.ReverterError: If\n            all files within file_list cannot be removed\n\n        \"\"\"\n        # Check to see that file exists to differentiate can't find file_list\n        # and can't remove filepaths within file_list errors.\n        if not os.path.isfile(file_list):\n            return False\n        try:\n            with open(file_list, \"r\") as list_fd:\n                filepaths = list_fd.read().splitlines()\n                for path in filepaths:\n                    # Files are registered before they are added... so\n                    # check to see if file exists first\n                    if os.path.lexists(path):\n                        os.remove(path)\n                    else:\n                        logger.warning(\n                            \"File: %s - Could not be found to be deleted\\n\"\n                            \" - Certbot probably shut down unexpectedly\",\n                            path)\n        except OSError:\n            logger.critical(\n                \"Unable to remove filepaths contained within %s\", file_list)\n            raise errors.ReverterError(\n                \"Unable to remove filepaths contained within \"\n                \"{0}\".format(file_list))\n\n        return True\n\n    def finalize_checkpoint(self, title: str) -> None:\n        \"\"\"Finalize the checkpoint.\n\n        Timestamps and permanently saves all changes made through the use\n        of :func:`~add_to_checkpoint` and :func:`~register_file_creation`\n\n        :param str title: Title describing checkpoint\n\n        :raises certbot.errors.ReverterError: when the\n            checkpoint is not able to be finalized.\n\n        \"\"\"\n        # Check to make sure an \"in progress\" directory exists\n        if not os.path.isdir(self.config.in_progress_dir):\n            return\n\n        changes_since_path = os.path.join(self.config.in_progress_dir, \"CHANGES_SINCE\")\n        changes_since_tmp_path = os.path.join(self.config.in_progress_dir, \"CHANGES_SINCE.tmp\")\n\n        if not os.path.exists(changes_since_path):\n            logger.info(\"Rollback checkpoint is empty (no changes made?)\")\n            with open(changes_since_path, 'w') as f:\n                f.write(\"No changes\\n\")\n\n        # Add title to self.config.in_progress_dir CHANGES_SINCE\n        try:\n            with open(changes_since_tmp_path, \"w\") as changes_tmp:\n                changes_tmp.write(\"-- %s --\\n\" % title)\n                with open(changes_since_path, \"r\") as changes_orig:\n                    changes_tmp.write(changes_orig.read())\n\n        # Move self.config.in_progress_dir to Backups directory\n            shutil.move(changes_since_tmp_path, changes_since_path)\n        except OSError:\n            logger.error(\"Unable to finalize checkpoint - adding title\")\n            logger.debug(\"Exception was:\\n%s\", traceback.format_exc())\n            raise errors.ReverterError(\"Unable to add title\")\n\n        # rename the directory as a timestamp\n        self._timestamp_progress_dir()\n\n    def _checkpoint_timestamp(self) -> str:\n        \"Determine the timestamp of the checkpoint, enforcing monotonicity.\"\n        timestamp = str(time.time())\n        others = glob.glob(os.path.join(self.config.backup_dir, \"[0-9]*\"))\n        others = [os.path.basename(d) for d in others]\n        others.append(timestamp)\n        others.sort()\n        if others[-1] != timestamp:\n            timetravel = str(float(others[-1]) + 1)\n            logger.warning(\"Current timestamp %s does not correspond to newest reverter \"\n                \"checkpoint; your clock probably jumped. Time travelling to %s\",\n                timestamp, timetravel)\n            timestamp = timetravel\n        elif len(others) > 1 and others[-2] == timestamp:\n            # It is possible if the checkpoints are made extremely quickly\n            # that will result in a name collision.\n            logger.debug(\"Race condition with timestamp %s, incrementing by 0.01\", timestamp)\n            timetravel = str(float(others[-1]) + 0.01)\n            timestamp = timetravel\n        return timestamp\n\n    def _timestamp_progress_dir(self) -> None:\n        \"\"\"Timestamp the checkpoint.\"\"\"\n        # It is possible save checkpoints faster than 1 per second resulting in\n        # collisions in the naming convention.\n\n        for _ in range(2):\n            timestamp = self._checkpoint_timestamp()\n            final_dir = os.path.join(self.config.backup_dir, timestamp)\n            try:\n                filesystem.replace(self.config.in_progress_dir, final_dir)\n                return\n            except OSError:\n                logger.warning(\"Unexpected race condition, retrying (%s)\", timestamp)\n\n        # After 10 attempts... something is probably wrong here...\n        logger.error(\n            \"Unable to finalize checkpoint, %s -> %s\",\n            self.config.in_progress_dir, final_dir)\n        raise errors.ReverterError(\n            \"Unable to finalize checkpoint renaming\")\n"
  },
  {
    "path": "certbot/src/certbot/ssl-dhparams.pem",
    "content": "-----BEGIN DH PARAMETERS-----\nMIIBCAKCAQEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz\n+8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a\n87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7\nYdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi\n7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD\nssbzSibBsu/6iGtCOGEoXJf//////////wIBAg==\n-----END DH PARAMETERS-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/__init__.py",
    "content": "\"\"\"Utilities for running Certbot tests\"\"\"\n"
  },
  {
    "path": "certbot/src/certbot/tests/acme_util.py",
    "content": "\"\"\"ACME utilities for testing.\"\"\"\nimport datetime\nfrom typing import Any\nfrom typing import Iterable\n\nimport josepy as jose\n\nfrom acme import challenges\nfrom acme import messages\nfrom certbot._internal import auth_handler\nfrom certbot.tests import util\n\nJWK = jose.JWK.load(util.load_vector('rsa512_key.pem'))\nKEY = util.load_jose_rsa_private_key_pem('rsa512_key.pem')\n\n# Challenges\nHTTP01 = challenges.HTTP01(\n    token=b\"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA\")\nDNS01 = challenges.DNS01(token=b\"17817c66b60ce2e4012dfad92657527a\")\nDNS01_2 = challenges.DNS01(token=b\"cafecafecafecafecafecafe0feedbac\")\n\nCHALLENGES = [HTTP01, DNS01]\n\n\ndef chall_to_challb(chall: challenges.Challenge, status: messages.Status) -> messages.ChallengeBody:\n    \"\"\"Return ChallengeBody from Challenge.\"\"\"\n    kwargs = {\n        \"chall\": chall,\n        \"uri\": chall.typ + \"_uri\",\n        \"status\": status,\n    }\n\n    if status == messages.STATUS_VALID:\n        kwargs.update({\"validated\": datetime.datetime.now()})\n\n    return messages.ChallengeBody(**kwargs)\n\n\n# Pending ChallengeBody objects\nHTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)\nDNS01_P = chall_to_challb(DNS01, messages.STATUS_PENDING)\nDNS01_P_2 = chall_to_challb(DNS01_2, messages.STATUS_PENDING)\n\nCHALLENGES_P = [HTTP01_P, DNS01_P]\n\n\n# AnnotatedChallenge objects\nHTTP01_A = auth_handler.challb_to_achall(HTTP01_P, JWK, messages.Identifier(\n    typ=messages.IDENTIFIER_FQDN, value=\"example.com\"))\nDNS01_A = auth_handler.challb_to_achall(DNS01_P, JWK, messages.Identifier(\n    typ=messages.IDENTIFIER_FQDN, value=\"example.org\"))\nDNS01_A_2 = auth_handler.challb_to_achall(DNS01_P_2, JWK, messages.Identifier(\n    typ=messages.IDENTIFIER_FQDN, value=\"esimerkki.example.org\"))\n\nACHALLENGES = [HTTP01_A, DNS01_A]\n\n\ndef gen_authzr(authz_status: messages.Status, domain: str, challs: Iterable[challenges.Challenge],\n               statuses: Iterable[messages.Status]) -> messages.AuthorizationResource:\n    \"\"\"Generate an authorization resource.\n\n    :param authz_status: Status object\n    :type authz_status: :class:`acme.messages.Status`\n    :param list challs: Challenge objects\n    :param list statuses: status of each challenge object\n\n    \"\"\"\n    challbs = tuple(\n        chall_to_challb(chall, status)\n        for chall, status in zip(challs, statuses)\n    )\n    authz_kwargs: dict[str, Any] = {\n        \"identifier\": messages.Identifier(\n            typ=messages.IDENTIFIER_FQDN, value=domain),\n        \"challenges\": challbs,\n    }\n    if authz_status == messages.STATUS_VALID:\n        authz_kwargs.update({\n            \"status\": authz_status,\n            \"expires\": datetime.datetime.now() + datetime.timedelta(days=31),\n        })\n    else:\n        authz_kwargs.update({\n            \"status\": authz_status,\n        })\n\n    return messages.AuthorizationResource(\n        uri=\"https://trusted.ca/new-authz-resource\",\n        body=messages.Authorization(**authz_kwargs)\n    )\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/README",
    "content": "The following command has been used to generate test keys:\n\n\tfor x in 512 2048; do openssl genrsa -out rsa${k}_key.pem $k; done\n\nFor the elliptic curve private keys, this command was used:\n\n\tfor k in \"prime256v1\" \"secp384r1\" \"secp521r1\" do\n\t    openssl genpkey -algorithm ${k} -out ec_${k}_key.pem\n    done\n\nand for the CSR PEM (Certificate Signing Request):\n\n\t\topenssl req -new -out csr-Xsans_X.pem -key rsa512_key.pem [-config csr-Xsans_X.conf | -subj '/CN=example.com'] [-outform DER > csr_X.der]\n\nand for the certificate:\n\n  openssl req -new -out cert_X.pem -key rsaX_key.pem -subj '/CN=example.com' -x509 [-outform DER > cert_X.der]\n\n`csr-mixed.pem` was generated with pyca/cryptography using the following snippet:\n\n\tfrom cryptography import x509\n\tfrom cryptography.hazmat.primitives import hashes, serialization\n\tk = serialization.load_pem_private_key(\n\t    open(\"./acme/acme/_internal/tests/testdata/rsa2048_key.pem\", \"rb\").read(), None\n\t)\n\tcsr = (\n\t    x509.CertificateSigningRequestBuilder().add_extension(\n\t        x509.SubjectAlternativeName([x509.DNSName('a.example.com'), x509.IPAddress(ipaddress.ipaddr('192.0.2.111'))]),\n\t        critical=False\n\t    ).subject_name(\n\t        x509.Name([])\n\t    ).sign(\n\t        k, hashes.SHA256()\n\t    )\n\t)\n\topen(\"./acme/acme/_internal/tests/testdata/csr-mixed.pem\", \"wb\").write(\n\t    csr.public_bytes(serialization.Encoding.PEM)\n\t)\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert-5sans_512.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICkTCCAjugAwIBAgIJAJNbfABWQ8bbMA0GCSqGSIb3DQEBCwUAMHkxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNp\nc2NvMScwJQYDVQQKDB5FbGVjdHJvbmljIEZyb250aWVyIEZvdW5kYXRpb24xFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE2MDYwOTIzMDEzNloXDTE2MDcwOTIzMDEz\nNloweTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcM\nDVNhbiBGcmFuY2lzY28xJzAlBgNVBAoMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91\nbmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANL\nADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE\n30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4GlMIGiMB0GA1UdDgQWBBQmz8jt\nS9eUsuQlA1gkjwTAdNWXijAfBgNVHSMEGDAWgBQmz8jtS9eUsuQlA1gkjwTAdNWX\nijAMBgNVHRMEBTADAQH/MFIGA1UdEQRLMEmCDWEuZXhhbXBsZS5jb22CDWIuZXhh\nbXBsZS5jb22CDWMuZXhhbXBsZS5jb22CDWQuZXhhbXBsZS5jb22CC2V4YW1wbGUu\nY29tMA0GCSqGSIb3DQEBCwUAA0EAVXmZxB+IJdgFvY2InOYeytTD1QmouDZRtj/T\nH/HIpSdsfO7qr4d/ZprI2IhLRxp2S4BiU5Qc5HUkeADcpNd06A==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert-nosans_nistp256.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIBoDCCAUYCCQDCnzfUZ7TQdDAKBggqhkjOPQQDAjBYMQswCQYDVQQGEwJVUzER\nMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwD\nRUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xODA1MTUxNzIyMzlaFw0xODA2\nMTQxNzIyMzlaMFgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAG\nA1UEBwwJQW5uIEFyYm9yMQwwCgYDVQQKDANFRkYxFDASBgNVBAMMC2V4YW1wbGUu\nY29tMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPPl0JauSZukvAUWv4l5VNLAY\nQXhuPXYQBf4dVET3s0E5q9ZCbSe+pNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tzAK\nBggqhkjOPQQDAgNIADBFAiEAv8S2GXmWJqZ+j3DBfm72E1YK+HkOf+TOUHsbVR+O\nZ1oCIFWNt1SPdIgRp4QAyzVk2pcTF8jDNajEMLWETDtxgRvM\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert-san_512.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx\nETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM\nIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG\nA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix\nKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR\n7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c\n+pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt\ncGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF\nnTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7\nRDjyGMKy5ZgM2w==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_2048.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV\nBAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD\nRUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC\nQ0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G\nyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0\napV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7\nr3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6\n0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F\nOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f\nAgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME\nGDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh\nvds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv\nxzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D\nHEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X\nYXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ\n5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_512.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx\nETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM\nIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4\nYW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG\nA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix\nKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR\n7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c\n+pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll\nvr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn\nB/o=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_512_bad.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICYzCCAg2gAwIBAgIJAPvqv4TcAtuFMA0GCSqGSIb3DQEBCwUAMIGMMQswCQYD\nVQQGEwJDQTEQMA4GA1UECAwHT250YXJpbzEQMA4GA1UEBwwHVG9yb250bzEMMAoG\nA1UECgwDRUZGMRYwFAYDVQQLDA1UZWNoIFByb2plY3RzMQ4wDAYDVQQDDAVZb21u\nYTEjMCEGCSqGSIb3DQEJARYUeW9tbmEubmFzc2VyQGVmZi5vcmcwHhcNMTcwMzI0\nMjIzMjUxWhcNNDgwMTI0MjIzMjUxWjCBjDELMAkGA1UEBhMCQ0ExEDAOBgNVBAgM\nB09udGFyaW8xEDAOBgNVBAcMB1Rvcm9udG8xDDAKBgNVBAoMA0VGRjEWMBQGA1UE\nCwwNVGVjaCBQcm9qZWN0czEOMAwGA1UEAwwFWW9tbmExIzAhBgkqhkiG9w0BCQEW\nFHlvbW5hLm5hc3NlckBlZmYub3JnMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1\nc7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvO\njm0c+pVE6K+EdE/twuUCAwEAAaNQME4wHQYDVR0OBBYEFCbPyO1L15Sy5CUDWCSP\nBMB01ZeKMB8GA1UdIwQYMBaAFCbPyO1L15Sy5CUDWCSPBMB01ZeKMAwGA1UdEwQF\nMAMBAf8wDQYJKoZIhvcNAQELBQADQQAeWDdcrJOolFHr3m8TrlDJ/Ca4SfJya2jb\nK1wahbX83sC42834HbDOQASGBhoLYDhC1cMPbKDDjMbR9rjYuf7T\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_fullchain_2048.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV\nBAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD\nRUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC\nQ0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G\nyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0\napV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7\nr3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6\n0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F\nOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f\nAgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME\nGDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh\nvds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv\nxzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D\nHEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X\nYXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ\n5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIJAIYLtIQHBBG0MA0GCSqGSIb3DQEBCwUAMDoxCzAJBgNV\nBAYTAkNBMQswCQYDVQQIDAJPTjEQMA4GA1UEBwwHVG9yb250bzEMMAoGA1UECgwD\nRUZGMB4XDTE3MDUyOTA3NDIwMVoXDTQ4MDMzMDA3NDIwMVowOjELMAkGA1UEBhMC\nQ0ExCzAJBgNVBAgMAk9OMRAwDgYDVQQHDAdUb3JvbnRvMQwwCgYDVQQKDANFRkYw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDm1WIecnHjL4FsJvxDP27G\nyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop+D7s+oh0\napV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3DaNokGn7\nr3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf5QU5pFx6\n0a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDVMJ3mIB8F\nOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf3nCJFk1f\nAgMBAAGjUzBRMB0GA1UdDgQWBBSdJ++M23AW3LkFD7LKhsH7gL6/2jAfBgNVHSME\nGDAWgBSdJ++M23AW3LkFD7LKhsH7gL6/2jAPBgNVHRMBAf8EBTADAQH/MA0GCSqG\nSIb3DQEBCwUAA4IBAQCV5kSt1HTFzUPdBvxT455YrLd3jIsRt1pRNuGjVaUYIRxh\nvds8NN1Z8h/8Cdzz8NVkIdCuYb2lFaDjs3zNVUQxCyVcH7xVyPwFI85NR27+HPRv\nxzz2rwzST+NKYst6ZBg086BKjqFtxs16lpU/TD6tOJqg86TBbfP6gib/ocGeER2D\nHEEik69FjmUCziT6uXyYW5y1PxD15UWO3RWoTpao0vGtTPceTeeuO05PVeCUlx8X\nYXg9zoVWBba0GF+qQJ67zT5nvfc2KJcgnWRIRr/90YXzBf+FdFVuC4xFHINBI1OJ\n5XBLJOv61Zu+Du/nmlBVcb8KL/Vd2oZyfoH+0oCN\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_intermediate_1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIIYbnTKswm95swDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgNzcwNjgzMCAXDTIwMDYxOTIxMDY1NVoYDzIwNTAw\nNjE5MjAwNjU1WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1\nOWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk\nCuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS\nHuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8\nksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd\nANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052\nhzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2\nlqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR\nKqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFBg/Nkzipd7/vMs/f9VV11uPo2bS\nMA0GCSqGSIb3DQEBCwUAA4IBAQBISrMR9Fchj2u1FhxAr7eQsdM1Lus3B/eEJmcR\nKDO/tNzns5zsrcJL42wzDVQA61X+aVzZBSfb1oMbwHCpWBvj88avL/mJ1OgrC+VZ\nv7IDdKOwvXmFf0VVE5LLCPY+gfR65LVRkb0W9ZrZ9Oj0ke9A9ryPSNTYBvxE+Qar\nV5Jqe0YdG7IQBd4xFFnpWZyEPnQBW2Sl5oudt6LgJaQosMJdRLp4lL805G/lKPEt\nCKjWoaP6J4V1Ty+zwuWVdIpSUuIoWkZ7BeIIqVkTg7ieNj4eN2zddDxbTIquFD2K\n7rZ1691krTWTe8bkLx6wGaAfvLOk97Ywmdsot3nTVRbaVTXq\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_intermediate_2.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIILrJTDiWZDFkwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgMGNjNmYwMCAXDTIwMDYxOTIxMDY1NFoYDzIwNTAw\nNjE5MjAwNjU0WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDI1\nOWJjMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALFeijP7HyxcUMrk\nCuKgEqyaRmgjGj3JvPnm7RPJl0j6rgWe767eweP2dMJ8LnYHWZVDwmkysq1rb/kS\nHuWoqUW1gpqwo4+ARMAb0fhx79Ze7eNp5M44PRN9SenK3TZQYrNFzC5t0NzNQaq8\nksv0R4kxi2VqfPuV6GCEYHI2t5z7249U+4PHEPSLEjq0TvFYp8aLVJ2cRLsspdXd\nANQwbKCWz00j03CcK79LXR6ncNw1WqEoYxjzmagiAKBf9kJVawxeJF91A+kPH052\nhzGxhmMOU/VeWcl+zGeRR2cybyMF1qZ/tDX0n+J4q7FrcOjzJeZH8uxwOZ4N7Xn2\nlqavdTkCAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMCE8MSR\nKqrGnv0Vd4iv8FCVtVmhMB8GA1UdIwQYMBaAFE4E9PDqHbaTzFi8XEeTc/s/xEal\nMA0GCSqGSIb3DQEBCwUAA4IBAQCIEnLk9ZgIzwjew3ktKmZ/lDKl00EcIJ/or/GE\nIM3exvEzUYJotoCKdw/6d1mjfsJ1AkJUrY5WASEhXgQ9wP5/z08xwmCSVowSnHhg\niKeQf6iNIqpxg7LGqOeSc32QXzeBMTsvQcqupZ7J6ptomiPopeAfYIMQWni9Ym/I\nP1ZjydR2petIFioPuFtByu1UhvIcF4LCJ7UWwhVNcrivf5VGaRUVac4IxGZ4Urw+\nW1xgT8pt5Rk1yCoqXsenllXRxdakbDDG9F0ZBWQNmvArYZJt1mdq1dBHT/JO/wKI\naGF5PXa6k3YwhyvK4zTDcwPKmOBikL2FgzlohLYfETpTznW0\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cert_leaf.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDWTCCAkGgAwIBAgIIYWk/LgSO+zowDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAyNTliYzEwHhcNMjAwNjIxMDEyMTM3\nWhcNMjUwNjIxMDEyMTM3WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOABmyVITyczj6/R/1EWH6FGEfzJgOFi\nzUC1HweYouEnC3Pu4SV/qFyKC8FsVQAY5FYWV4rrexnMgAeCL7XBTFyvIYQ7FoCH\n5SeyUr3T1j/BAaFZtdb419d+BXLBOYigIu36pyRrH/3z0oxvv0brG7zXty/AOHTK\n/k5aE7e3iuUSPJqNoGIQi4NwtNw5Fwcpe4lOHRxNNp9kfjxKYzLgj9xaT6cw6r2T\n9QJ7wnU9C0/cs9h98xz+IhApyoVWlF4SE87dpG99NQ8M92od5xv1jSIF34AMQzww\nfAW3FIKr4onaMP6XHtiXnJFn4pSw+BryxPqsKz864Ritvokz/nJoylUCAwEAAaOB\nmDCBlTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF\nBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKG2yAaJtpPVStXakLLD5Jc7fcaN\nMB8GA1UdIwQYMBaAFMCE8MSRKqrGnv0Vd4iv8FCVtVmhMBYGA1UdEQQPMA2CC2V4\nYW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBde/j8lgRqzksphFVwfx310TaD\n4foS+15cq0OnTM/iVt1JtjyyGaRamgCcFXkm31IP8c9vnaReYGY/QawshK1NSLii\nW6mN/RqguQNoovp0xw3SkDb+lyDo0VuIyMrbwlbhP4P5uue+Shb1ZTy7QmfUeYuK\nOTjFI3j0oBiSKins3ryEFHIMvuXcaQ4F2uw6UOHBgwv5XbVV5B9l62jGbZnxrpc7\nSB9mzU8I5CJoK1uetwuADwlhDX8ITQ778unR7yIZOojyIMKWd9z9KtKDVSPK9I+d\nu0oYF4xG97nTks6Cqt2OUJizSY3ojgwK4U8w1795FCVUlq5Z58gpWf1BhA8a\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/cli.ini",
    "content": "agree-dev-preview = True\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-6sans_512.conf",
    "content": "[req]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n\n[req_distinguished_name]\nC=US\nC_default = US\nST=Michigan\nST_default=Michigan\nL=Ann Arbor\nL_default=Ann Arbor\nO=EFF\nO_default=EFF\nOU=University of Michigan\nOU_default=University of Michigan\nCN=example.com\nCN_default=example.com\n\n\n[ v3_req ]\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = example.com\nDNS.2 = example.org\nDNS.3 = example.net\nDNS.4 = example.info\nDNS.5 = subdomain.example.com\nDNS.6 = other.subdomain.example.com"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-6sans_512.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw\nEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy\nc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f\np580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoIGGMIGDBgkqhkiG\n9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL\nZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t\nghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQA+\nsU6T30n3SsdnHlj0Va8eECOWK7Lf8nUfxxgjPMQ7BoU8gbAnGfDmOlwDronTRqf1\nMe+nlYJU4TX1OiX10DYu\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-nonames_512.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIH/MIGqAgEAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwXDANBgkqhkiG9w0BAQEF\nAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+\n6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoAAwDQYJKoZIhvcNAQELBQAD\nQQBt9XLSZ9DGfWcGGaBUTCiSY7lWBegpNlCeo8pK3ydWmKpjcza+j7lF5paph2LH\nlKWVQ8+xwYMscGWK0NApHGco\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-nosans_512.conf",
    "content": "[req]\ndistinguished_name = req_distinguished_name\n\n[req_distinguished_name]\nC=US\nC_default = US\nST=Michigan\nST_default=Michigan\nL=Ann Arbor\nL_default=Ann Arbor\nO=EFF\nO_default=EFF\nOU=University of Michigan\nOU_default=University of Michigan\nCN=example.com\nCN_default=example.com"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-nosans_512.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBMzCB3gIBADB5MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ\nBgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMR8wHQYDVQQLDBZVbml2ZXJz\naXR5IHBmIE1pY2hpZ2FuMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3\nDQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db0Cx1GxS4dQEEEOUfA2VF3d+n\nnzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLlAgMBAAGgADANBgkqhkiG9w0B\nAQsFAANBAG06jIPvSC6wiGLy7sUTaEX4UCE6Cztp3vh/uXN7Q++CGn6KiXNs/BRW\neFlcFPbvxbVG/ZZFR5aPs+Oy6RhqOjg=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-nosans_nistp256.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBFDCBugIBADBYMQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQ\nBgNVBAcMCUFubiBBcmJvcjEMMAoGA1UECgwDRUZGMRQwEgYDVQQDDAtleGFtcGxl\nLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDz5dCWrkmbpLwFFr+JeVTSw\nGEF4bj12EAX+HVRE97NBOavWQm0nvqTVG5KPRfkxZLnO11Y0D7H5A24dCPZw9Leg\nADAKBggqhkjOPQQDAgNJADBGAiEAuoZHrYA5sy2DRTdLAxJTBNHKFFKbtaGt+QaJ\nA62qa8sCIQCUkSgSAiNaEnJ7r5fKphdjeORHqhpl6flYkLE3lGmGdg==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr-san_512.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw\nEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy\nc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG\n9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f\np580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN\nAQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t\nMA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy\ntmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/csr_512.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh\nMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtFeGFt\ncGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQCsdXO0Ue0f3a5wUkP838db\n0Cx1GxS4dQEEEOUfA2VF3d+nnzSu/b7pBYTfRxaB2YlLzo5tHPqVROivhHRP7cLl\nAgMBAAGgADANBgkqhkiG9w0BAQsFAANBAAceUlq4La8qaiK0DeDP3M19BIVzMmz2\noemG2fOvPiwNCB90ctSWQ6bMpUMV85ShcFi31C5vlntPfztehhq6YuE=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ec_prime256v1_key.pem",
    "content": "-----BEGIN EC PARAMETERS-----\nBggqhkjOPQMBBw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIDqQPQl69kuh+DrecC8SFPt21f0F/HHDP3T4/Lf0zIVFoAoGCCqGSM49\nAwEHoUQDQgAEHou50Ee9u+8Vial6VbUHExlzsiCHtORlW0X0pKo5RspIKB0QyKwo\ndUXvBbv95I9yCO5+MlGkKjwLHtIEze0Hww==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ec_secp384r1_key.pem",
    "content": "-----BEGIN EC PARAMETERS-----\nBgUrgQQAIg==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAgvZGw5C7Mp26N0cXA+vIg5K/J5MJw+MVGYfGF4ZutuCLeYMrWT68R\nA0h6hJvDtMSgBwYFK4EEACKhZANiAAR1uQYZeU5Kml5o53Q8/PCdwUbqdgCSkV0C\nJ5a6bhDRMp20fdp2T/mbkdxuVEl81lqfKPZhsd4CZsLaVIU3RUoGgIT1R3QKawpJ\nSuXq37yWFX2hqlgt+lsBufZ8RD5QnZc=\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ec_secp521r1_key.pem",
    "content": "-----BEGIN EC PARAMETERS-----\nBgUrgQQAIw==\n-----END EC PARAMETERS-----\n-----BEGIN EC PRIVATE KEY-----\nMIHcAgEBBEIACWWVKm1qAIejZ6qmqk9D69wQW5FAe3Er0IxWAMkonTEhu8EH5Q2i\n2vT2bESm730zhGTe2Pn11b85H6UI9hxhCHygBwYFK4EEACOhgYkDgYYABAEQi1WF\nm3suHjPyWACyOJYGUn1Kx6rfBo0PjC7X2TU9jr8umLkIpaaF5UsBuMBmdz1IHL0U\nk0gQtoOQ0Qu8N74GuAGzGR0S3RYIv6gfYVz3dS1K4n4b307Lx62bnvtlNxcIvt3w\nhmS5OdvQ1Kdxh6oqbSVhhbQmJcgab78Txx3R2QeCxw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/nistp256_key.pem",
    "content": "-----BEGIN EC PRIVATE KEY-----\nMHcCAQEEIOvXH384CyNNv2lfxvjc7hg2f7ScYoLvlk/VpINLJlGBoAoGCCqGSM49\nAwEHoUQDQgAEPPl0JauSZukvAUWv4l5VNLAYQXhuPXYQBf4dVET3s0E5q9ZCbSe+\npNUbko9F+TFkuc7XVjQPsfkDbh0I9nD0tw==\n-----END EC PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ocsp_certificate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIGYDCCBEigAwIBAgIKcjrC4hZcebbtODANBgkqhkiG9w0BAQsFADBRMQswCQYD\nVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNVBAMM\nGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDUxMjE1NTgyMVoXDTE5\nMTEwODIyNTkwMFowADCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK9P\nb+YhJPypm4ui+AZUHPrJ6IsB9R/6Wvgec2G/GuW/UNQFktIhU10HOHAbiJeYLqNZ\n1Cia8JD6NXXGbprOjIbZWvjulYTaLSlClcK0H7HZrcgrK60OeIGEtur27ga68RML\nhs1FG7TNyWVysifOtwW9Oo1mZQQtxViiE2Yb+Q4QqIxitnbrnFmKrVJSUHVXi8/I\nBK1yLrJiRBZMIw0wvAWcWEG2Gpp9PAbemlb11Zx8sm/RSGh7u60rmETbB2Pu941s\nXJCSQRtq5yKdtjIJTIgbe12SPkknqTqa3aUh7hgho0IymlDSeeocL60SUiUAsPEr\nQRWleodOR1ChXz5mFokCAwEAAaOCAokwggKFMAkGA1UdEwQCMAAwHwYDVR0jBBgw\nFoAUd9nQBpFm2N0ZJo1JrNowL2p7YrEwHQYDVR0OBBYEFExS23I6sLCeO6KIxzoc\ntr9s+HmiMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwIAYDVR0gBBkwFzALBglghEIBGgEAAgcwCAYGZ4EMAQIBMEIGA1UdHwQ7\nMDkwN6A1oDOGMWh0dHA6Ly9jcmwudGVzdDQuYnV5cGFzcy5uby9jcmwvQlBDbGFz\nczJUNENBNS5jcmwwIQYDVR0RAQH/BBcwFYITYnV5cGFzcy5wYWNhbGlzLm5ldDB4\nBggrBgEFBQcBAQRsMGowKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnRlc3Q0LmJ1\neXBhc3MuY29tMD0GCCsGAQUFBzAChjFodHRwOi8vY3J0LnRlc3Q0LmJ1eXBhc3Mu\nbm8vY3J0L0JQQ2xhc3MyVDRDQTUuY2VyMIIBBAYKKwYBBAHWeQIEAgSB9QSB8gDw\nAHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsTLGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMA\nRzBFAiEA1oWB4c6q7+tqGA4HhLNACOemr9c2aIUuWxeQE7/PlSYCIEolZ7pWVs1J\nVyQW/AqeuXGB7qScwUgLh9C1uOJoeRe6AHYAsMyD5aX5fWuvfAnMKEkEhyrH6IsT\nLGNQt8b9JuFsbHcAAAFqrMQ/cQAABAMARzBFAiAoLaNvIwMDifsDAXJBsAKHlYx7\nQPLXL8onYKm8f+Sf1wIhAMepo2GX84UR7WtooqzkBZLG+PaBy1zMuUAG6mwnroF9\nMA0GCSqGSIb3DQEBCwUAA4ICAQAPWLdjNS5lLL5SEtghYebtDmNj2968NYSDvb1L\n1/uFwg3LCVRR1Xb3z1Hc/sc1W0IFXU0zOqEQiuP8jkVP7UqkaWuK5Eu0eP0zPI83\nWBZM0+eBwxwzIMK/Q7fYKTu1+vg/FlH0WhtV43DQSik66366zvPi2Tfag9IPvRei\nDOjbSOBF0o4er2oCrtI0lK5YrHOdWtD7xwQIuA606P9ucuufMf+JcmduRJsVZ2Zu\n3K32SMDdAnyjvQWZNbt1ex3G8vuFQEi690UBhPcha/SO8QvLS89wcaLJnyMIWdv7\n54cbw+fa1nLKM7qph6Mk1yb0qpomPqLmKw4T6WX36c0vDlFSpexJLGgWDFqLUxPN\nqV7cJz4mi1qaYfdWXRrnyU4bl55pHTTgEzbohV7apsmytkCe1uFNrpcTh8jzAhGN\nPQqarX9UoESR56B/ufbBGlBWi0pkV49BFks6Ue0GVKo7djoxuV6+SsmYSE+6MNPv\nIUsm54TSnwxjA8WyG7pl14g1hkGFQ4NRYJMiVqK3DMABaPxVmT7NRxUQQiM0mmM7\nEKNzLBeWHJF5ecdDR1MiIF3ayn+RiZb0r8aSQBMLwN1YwUZw+hSYz1eCd7bHN1gC\n1ksxP61f8LBz0SwDoyOTr8wY++wqF26KfoYuKQ3LjLeHvuUtL3EMnAhiyuej8ZOZ\n22spng==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ocsp_issuer_certificate.pem",
    "content": "22spng==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIGMzCCBBugAwIBAgIJMvsa+ZFQCj8nMA0GCSqGSIb3DQEBCwUAMFQxCzAJBgNV\nBAYTAk5PMR0wGwYDVQQKDBRCdXlwYXNzIEFTLTk4MzE2MzMyNzEmMCQGA1UEAwwd\nQnV5cGFzcyBDbGFzcyAyIFRlc3Q0IFJvb3QgQ0EwHhcNMTcwMjEzMTY1MjQ2WhcN\nMjcwMjEzMTY1MjQ2WjBRMQswCQYDVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBB\nUy05ODMxNjMzMjcxIzAhBgNVBAMMGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAi/vpgO2sbUQZsoxWd6us\nQvT/59kvw5ehoJABBXFs1J1AV1/K2hjhDXit/sNGKjzDvkfE9PJqXMnhKpPFkUzC\nz/NmDK++d6aRflnDvJrxlPVpp0QGbe3qOErByFjWiHoobuVItlpRO/BaBdlgGvmQ\nLeZFBXs/ZrLNFUKBcE+DZIyJH7vy2EB5dNNVn2mx0n+371InpKsYUaHNlxPpp+uj\nTOL+e4OjWTBwDaI7rVzpavozb8SPzFxjpxLLVH/j+8VPwoe3lmxr8ATyI178iRdA\nuxYfaKURSfu7PWjnDNTnq26E3pwW3E5zUbsADgUMh/PzoJAcszL1eHGUQaAGBP85\nPlLmHr+nsPMHXOUyl7Ts6KGkZlvjnVshKwUxYAqjAC7/BY0iI0xc406NK9heeVDk\nNiFA8/To6mQ09vO/TBxQtkfNk2yuxiixa101peSg4/+E4VhwYv6MJxS/oVqBd2d3\nwemYW/JUVeJg9wXGq1e/c09/UjGwUGwU9s5LNFEgj4v1tcvWnONzWNXkyMrs5g4e\nU8L/DQ3XgNrcA9zrfFq0cQhSJonj/VI/jbBYyB2yEuQAIjAN6eDIOoLmHGIIvZtE\n0LL5jaZC3W518jB1OF7QSvaFtaFl0VqDy6LMXL50elMVC+hr9KpDnN0t8gaSiPyZ\nwEC9SMdQ7SLVOUK1Xdh3dh0CAwEAAaOCAQkwggEFMA8GA1UdEwEB/wQFMAMBAf8w\nHwYDVR0jBBgwFoAU0aT+MaGsc75ZynH0up0oH+tVHh4wHQYDVR0OBBYEFHfZ0AaR\nZtjdGSaNSazaMC9qe2KxMA4GA1UdDwEB/wQEAwIBBjAgBgNVHSAEGTAXMAsGCWCE\nQgEaAQACBzAIBgZngQwBAgEwRQYDVR0fBD4wPDA6oDigNoY0aHR0cDovL2NybC50\nZXN0NC5idXlwYXNzLm5vL2NybC9CUENsYXNzMlQ0Um9vdENBLmNybDA5BggrBgEF\nBQcBAQQtMCswKQYIKwYBBQUHMAGGHWh0dHA6Ly9vY3NwLnRlc3Q0LmJ1eXBhc3Mu\nY29tMA0GCSqGSIb3DQEBCwUAA4ICAQBOgxedV31NCpZQRc8yFxoqQNgBnY1UeH/h\n/s/9fGQzyGnTWZldEi5MGJKF6ulcYnklitlg/jic9au3xSoqP/i2smUHByX2wMrC\nmDpLCwio2x2p/0Wscj5asqzJE2cCWqob2iHxo36nsr3Jdd2GIlzhZ0wm8rMZxsQG\nFgbgHYIer79S+PIdHoZuUnCJhsJ+1PRUmm2t7vcmZpu8l4CeL0XJX98l2L8kbBds\nMGo1EazGAEirZnSfQKCARhUcEdavsKl067+irsGGcK4+L78Vl9S1/QPfKG30L5fv\nnM1X1qAdhsbjwVdrhLkjpzabT0icsW6W17HLh8UBYdA7k4GclA6h+mNrXAt7JAeZ\nPzMFq0I7vVJNEdolZHTVCqT0sdJiTj+phS1ztK86Wb1R/5d5B1VSb789zSdJfrwV\nppXgPtZq5x3GQi6ooteWyuWj3cBcNu9TU1D8u1F0XI5gw4Y0VpxlDxysUgFQJlo4\nVYmMpgr442o/35UgwzkIC7x/6dkvMZvM4jYB5JZJXjynR35XawXB/hzybermJ8BB\nDsY0MCOwxhpsTbyEC4wfxZ08B4JtORkToOt4OWuejovsr68Ht6ytOPj7dquoPPNM\n9eGNSp94nEIiZ2n75ZMg0gIQArXU9OCV6B2TXxB7w2YB0y0teDgVhoM3IY/ltqJ/\nPJrUUjM8OQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/ocsp_responder_certificate.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEpjCCAo6gAwIBAgINARMIGYlEsD1LTt6D7zANBgkqhkiG9w0BAQsFADBRMQsw\nCQYDVQQGEwJOTzEdMBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIzAhBgNV\nBAMMGkJ1eXBhc3MgQ2xhc3MgMiBUZXN0NCBDQSA1MB4XDTE5MDQwNTEwMDAwMFoX\nDTE5MDcwNDEwMDAwMFowSTELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3Mg\nQVMtOTgzMTYzMzI3MRswGQYDVQQDDBJCdXlwYXNzIFRlc3Q0IE9DU1AwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDKGF+kYNd1fbhYT7Vf9xouZlx+4w45\nY5EowPoaSKFo4uUDDxkj4PwmMiH4w9Q2bGrCbZRrDrvlNVY/kwzLu4CIk6Ip0dgm\nVZGNFB3Xo9nai7rI5pn/YVvVnDIQXh1LRbekzLVyHvhRgMpRb19xN/iYsxaOJDph\n8eAgbTKf6eitvfbvn/zXHj4KGKycuULI4+mwlfV3uioT4ulbT7PTVJetgi/XXFDO\nxMjbqx6I1ZMmzKJ6LNaFlfx6GdZsaLRDCidHzGp8Fm4ZdV+UPvMZcVDQO6rvQ3wU\niGyCqgfE5e0aFvfeLoBPBtaoT0Ht1CvGdTfVet6PXrF6gh40fdEH5Ob5AgMBAAGj\ngYQwgYEwCQYDVR0TBAIwADAfBgNVHSMEGDAWgBR32dAGkWbY3RkmjUms2jAvanti\nsTAdBgNVHQ4EFgQU3VlR+sSIVpmXklieP7IlpVUcXIowDgYDVR0PAQH/BAQDAgeA\nMBMGA1UdJQQMMAoGCCsGAQUFBwMJMA8GCSsGAQUFBzABBQQCBQAwDQYJKoZIhvcN\nAQELBQADggIBAFBRLVsBadNFAoFi0HOrfxYsiqggZGJLlgxGyi/0NBIgduG4kcpM\nTHvplwBwMQEqyp5511pSEbLPAFj8EqC5c46hXZXmT49xlfRvr2Bo+qtTPV9szuWr\n8muEIejwRrkATpqWPZWR2zVTXfB90mU2oGuRvxUVmnW4v+FrCChJo7+9yTocZJKx\np4vxYfPMeggomdGAAUz94+0ppSjOLDzs3MA8uOcR0zJ2Y7UHb7PBf/HiM3GO2uKB\nsRgdDaGIf/PNpav0xJ/abGNNNwvXzHiMgqqImsuv/JoncPQWbClNurhXpdN7xt9C\nHcLX2AdggabcogjWm4guBFuFTsL1i0l8Bsu/6iPJ7ddCeANfYzf7h6AcQq12uFl3\n070F29DtPh8D3FPWgRZZsxoANFjXErxfj4a4+DR+jhhkb9YM/wI0vCOM7W6PKxVn\nZK5kHGOQTcQMj7RCX52gEf27M33zC7HVam+kKhGvwq7D9Bs5hZclzcbjpR4eIxT7\ntzuiy5VpPh1DRLPrphPUB4xsA1dy6zbkg8OqddG6NxD++ja/iZyzSB3SeWyO02qA\nQoK2FzDasxpZ9rT3ioAcms3wVNe4lcd4OP8gHZONuat/gvxk6OZvAld6cnIrQZYB\nTbu89ZWvhsyI3p4YC/15pUvA95j9Y0te+G+CF22Eoyb+rtz6mMletnUB\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/os-release",
    "content": "NAME=\"SystemdOS\"\nVERSION=\"42.42.42 LTS, Unreal\"\nID=systemdos\nID_LIKE=\"something nonexistent debian\"\nVERSION_ID=\"42\"\nHOME_URL=\"http://www.example.com/\"\nSUPPORT_URL=\"http://help.example.com/\"\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/rsa2048_key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDm1WIecnHjL4Fs\nJvxDP27GyeqnXKc41HsRP9cv4z+NDjE94mDgva5ndieiA9xZ0Sh7LXtZcGDcpGop\n+D7s+oh0apV6idIJ9eEPUegYlGxOFJQnZ8re6hD7MaAlNZEVhZrwJvrGy6rTFpi3\nDaNokGn7r3s2nrQ9aziljkWRp1PnTBnRNgOdi3c1IB2f4+2PdykjihxlnYUuI4Wf\n5QU5pFx60a2mdTVDC+bKAP22IvuQnnkHgJYYS/oMxFCT9QR4xQRPOx7U2RWVrFDV\nMJ3mIB8FOW6JXfQSmaZZr46xclbEIr4QQ6RcPWvcJ1cCV1idFjEmufi52sV7r1Bf\n3nCJFk1fAgMBAAECggEAJkhbVntagfgd+cbZbXm2sIdKQGlwXk92/Zxd3tZMcuNY\nrU+/C2bJ5uTEm+0R/V9f3FXlsCagGde2t7ExFnJScSRAGCuFRxudMMI/wNvUvnpR\nO9vN3HxrRo2rZqBkqHIZCR0d2Bxs/0cvGqTLZgsVWKV4xM07TThcE7DtvsNGegRn\nWFxfsRcRypkIvZoba1HagvCituRBEa07R7mQp8kRhP9ZeRq3bZws9qBmqzj1cylG\nq8QA4Foq7sK8P78bpIhrcOFBDAr+Vr1ZGY6u01J0w13MUtl6iIx4VCjQKt4NkzsK\ndj2q+GAMwhReR2ZS42o8LiyGpwusj+dKIFfFekgK2QKBgQD4wwmRDgvt85brQTNF\nTkhui0eToz5oXt8mVDb58nwkpojFQOv87ZyNsEqm7S0t/3RtEViVio2aymTMsrz4\n21vRq46dvhINQ3DoMok6xIchEOEgMeonOilkURWtrMjD/Kn297Asv7zOqI5BCNiP\n3FFcRqf+CaqbhnOgMkcI5z6b7QKBgQDtjM1otFFHyS7ctyLRuMeFyxWUSbWHvi8U\nxjUW256c6wpQ2DBLSVB61VQjfrSjkZ5DJVFGnbw42HxSDafL11mzTbY1vDbgtgLK\nYiuVHG7OYZJTLaZoM68BseX4xHN8FztnvvP1ttuk5oFb+vD8q6ODZSEawRd3PvtX\nD7RtNouc+wKBgQDiwBWGTUF+gt18T5BGilbnvLlf0Btg06mgrH74UpnqZoqhEs6J\nXKWpWZqSkfruxL4BdSBEH2l4QSiklgA+7uTBOBnlm42k3WaboQUJtn5eG5651AXV\n/+Qe9vJFvwu56iObZKcIAzY9QdN5YHDWoULgU99pZrJG1cWrrmilqvOc+QKBgQCB\niOslslY0N+926eJxzDn4qkJtJzh2+e1AfcjLWx0F4mEwroK/Ow5IvPVxmZE1NJ3B\nbaMBR9gwg1RfhhS+4gKG9NRsPuMJ7BZfd+LeH7AImEorU1RPtAc1fGW0HqP+wchi\nDU2I6pqhNBTMLG2myo2Sg93mce6y1sRFuEmh2EGPawKBgQC3uUEdjQekXaxXfYHi\n1Dk3Ht1a9t8XxwoCVRqicE7lqlwDtS2y9lHAeUP7JNy8ZGNjx8srRZpkYVMztugo\nEcw26UA7FbNqJP5OPkGjfiFqtOq70h9vlfLdiAPmoqyOx//RkgiNXt9m5xcDzzdB\n7EtBK59KSiQkB8fHtooy7Ipiiw==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/rsa512_key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79\nvukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn\nelAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc\nmQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp\nZu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj\n8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq\n6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive/cert1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF\nADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5\nMDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+\nslm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN\nNYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h\nA5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx\nUpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP\nr4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC\nAhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8\nL54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE\nbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy\neXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0\nc2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB\n8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw\nOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl\ncnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy\ndGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl\nIFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0\nb3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy\nKzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve\njzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2\nEse3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU\n+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf\nrAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive/chain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV\nBAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw\nNDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i\n8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8\ntnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj\n7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8\nBMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD\nHOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj\nUDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7\neE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA\nA4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB\nvR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl\nzBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo\nvRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L\noeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW\nrFo4Uv1EnkKJm3vJFe50eJGhEKlx\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive/fullchain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF\nADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5\nMDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+\nslm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN\nNYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h\nA5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx\nUpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP\nr4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC\nAhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8\nL54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE\nbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy\neXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0\nc2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB\n8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw\nOi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl\ncnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy\ndGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl\nIFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0\nb3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy\nKzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve\njzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2\nEse3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU\n+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf\nrAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV\nBAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw\nNDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i\n8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8\ntnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj\n7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8\nBMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD\nHOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj\nUDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7\neE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA\nA4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB\nvR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl\nzBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo\nvRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L\noeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW\nrFo4Uv1EnkKJm3vJFe50eJGhEKlx\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive/privkey1.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV\nWitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI\ni/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg\nFsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+\n9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+\ntJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A\n1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi\nmefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast\n8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB\nyqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF\nRJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE\nbKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+\nlBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs\nWkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT\nrzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO\ne4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po\nta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy\nhj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4\nGYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R\nWaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96\nKdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z\nMlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE\nLR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu\nA19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB\nvzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY\nSkn+ML18qyUoEPnmbpfHxCs=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive-ec/cert1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw\nWhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs\nZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU\nCRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh\no4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX\nmPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE\nJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww\nGoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl\nk0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd\n9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV\nifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq\ncP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6\nDa+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj\no/MXO8lcxkrem5zU5QWP\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive-ec/chain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx\nMDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy\nNmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq\nmJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB\nqKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5\nCIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH\nnm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY\nMRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx\nPzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE\nbIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT\nMA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq\nuCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P\nfJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV\nEdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW\nfcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG\n9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive-ec/fullchain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0\nWhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs\nZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5\n9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/\no4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO\nlKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE\nJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww\nGoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn\n2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5\n58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G\nptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF\nmv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU\n+Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX\n9U7y3nVI8WEbPGf+HDeu\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx\nMDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy\nNmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq\nmJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB\nqKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5\nCIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH\nnm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY\nMRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx\nPzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE\nbIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT\nMA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq\nuCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P\nfJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV\nEdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW\nfcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG\n9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-archive-ec/privkey1.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi\n0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj\nzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-renewal-ancient.conf",
    "content": "cert = MAGICDIR/live/sample-renewal-ancient/cert.pem\nprivkey = MAGICDIR/live/sample-renewal-ancient/privkey.pem\nchain = MAGICDIR/live/sample-renewal-ancient/chain.pem\nfullchain = MAGICDIR/live/sample-renewal-ancient/fullchain.pem\nrenew_before_expiry = 1 year\n\n# Options and defaults used in the renewal process\n[renewalparams]\nno_self_upgrade = False\napache_enmod = a2enmod\nno_verify_ssl = False\nifaces = None\napache_dismod = a2dismod\nregister_unsafely_without_email = False\napache_handle_modules = True\nuir = None\ninstaller = None\nnginx_ctl = nginx\nconfig_dir = MAGICDIR\ntext_mode = False\nfunc = <function obtain_cert at 0x7f093a163c08>\nstaging = True\nprepare = False\nwork_dir = /var/lib/letsencrypt\ntos = False\ninit = False\nhttp01_port = 80\nduplicate = False\nnoninteractive_mode = True\nkey_path = None\nnginx = False\nnginx_server_root = /etc/nginx\nfullchain_path = /home/ubuntu/letsencrypt/chain.pem\nemail = None\ncsr = None\nagree_dev_preview = None\nredirect = None\nverb = certonly\nverbose_count = -3\nconfig_file = None\nrenew_by_default = False\nhsts = False\napache_handle_sites = True\nauthenticator = webroot\ndomains = isnot.org,\nrsa_key_size = 2048\napache_challenge_location = /etc/apache2\ncheckpoints = 1\nmanual_test_mode = False\napache = False\ncert_path = /home/ubuntu/letsencrypt/cert.pem\nwebroot_path = /var/www/\nreinstall = False\nexpand = False\nstrict_permissions = False\napache_server_root = /etc/apache2\naccount = None\ndry_run = False\nmanual_public_ip_logging_ok = False\nchain_path = /home/ubuntu/letsencrypt/chain.pem\nbreak_my_certs = False\nstandalone = True\nmanual = False\nserver = https://acme-staging.api.letsencrypt.org/directory\nwebroot = True\nos_packages_only = False\napache_init_script = None\nuser_agent = None\napache_le_vhost_ext = -le-ssl.conf\ndebug = False\nlogs_dir = /var/log/letsencrypt\napache_vhost_root = /etc/apache2/sites-available\nconfigurator = None\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-renewal-deprecated-option.conf",
    "content": "# renew_before_expiry = 30 days\nversion = 1.11.0\narchive_dir = MAGICDIR/live/sample-renewal-deprecated-option\ncert = MAGICDIR/live/sample-renewal-deprecated-option/cert.pem\nprivkey = MAGICDIR/live/sample-renewal-deprecated-option/privkey.pem\nchain = MAGICDIR/live/sample-renewal-deprecated-option/chain.pem\nfullchain = MAGICDIR/live/sample-renewal-deprecated-option/fullchain.pem\n\n# Options used in the renewal process\n[renewalparams]\naccount = ffffffffffffffffffffffffffffffff\nauthenticator = nginx\ninstaller = nginx\nmanual_public_ip_logging_ok = None\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-renewal-ec.conf",
    "content": "# add some stuff here\n# assets/integration_tests\n\ncert = MAGICDIR/live/sample-renewal-ec/cert.pem\nprivkey = MAGICDIR/live/sample-renewal-ec/privkey.pem\nchain = MAGICDIR/live/sample-renewal-ec/chain.pem\nfullchain = MAGICDIR/live/sample-renewal-ec/fullchain.pem\nrenew_before_expiry = 4 years\n\n# Options and defaults used in the renewal process\n[renewalparams]\nno_self_upgrade = False\napache_enmod = a2enmod\nno_verify_ssl = False\nifaces = None\napache_dismod = a2dismod\nregister_unsafely_without_email = False\napache_handle_modules = True\nuir = None\ninstaller = None\nnginx_ctl = nginx\nconfig_dir = MAGICDIR\ntext_mode = False\nfunc = <function obtain_cert at 0x7f093a163c08>\nstaging = True\nprepare = False\nwork_dir = /var/lib/letsencrypt\ntos = False\ninit = False\nhttp01_port = 80\nduplicate = False\nnoninteractive_mode = True\nkey_path = None\nnginx = False\nnginx_server_root = /etc/nginx\nfullchain_path = /home/ubuntu/letsencrypt/chain.pem\nemail = None\ncsr = None\nagree_dev_preview = None\nredirect = None\nverb = certonly\nverbose_count = -3\nconfig_file = None\nrenew_by_default = False\nhsts = False\napache_handle_sites = True\nauthenticator = standalone\ndomains = isnot.org,\nkey_type = ecdsa\nelliptic_curve = secp256r1\napache_challenge_location = /etc/apache2\ncheckpoints = 1\nmanual_test_mode = False\napache = False\ncert_path = /home/ubuntu/letsencrypt/cert.pem\nwebroot_path = None\nreinstall = False\nexpand = False\nstrict_permissions = False\napache_server_root = /etc/apache2\naccount = None\ndry_run = False\nmanual_public_ip_logging_ok = False\nchain_path = /home/ubuntu/letsencrypt/chain.pem\nbreak_my_certs = False\nstandalone = True\nmanual = False\nserver = https://acme-staging-v02.api.letsencrypt.org/directory\nwebroot = False\nos_packages_only = False\napache_init_script = None\nuser_agent = None\napache_le_vhost_ext = -le-ssl.conf\ndebug = False\nlogs_dir = /var/log/letsencrypt\napache_vhost_root = /etc/apache2/sites-available\nconfigurator = None\nmust_staple = True\n[[webroot_map]]\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/sample-renewal.conf",
    "content": "cert = MAGICDIR/live/sample-renewal/cert.pem\nprivkey = MAGICDIR/live/sample-renewal/privkey.pem\nchain = MAGICDIR/live/sample-renewal/chain.pem\nfullchain = MAGICDIR/live/sample-renewal/fullchain.pem\nrenew_before_expiry = 4 years\n\n# Options and defaults used in the renewal process\n[renewalparams]\nno_self_upgrade = False\napache_enmod = a2enmod\nno_verify_ssl = False\nifaces = None\napache_dismod = a2dismod\nregister_unsafely_without_email = False\napache_handle_modules = True\nuir = None\ninstaller = None\nnginx_ctl = nginx\nconfig_dir = MAGICDIR\ntext_mode = False\nfunc = <function obtain_cert at 0x7f093a163c08>\nstaging = True\nprepare = False\nwork_dir = /var/lib/letsencrypt\ntos = False\ninit = False\nhttp01_port = 80\nduplicate = False\nnoninteractive_mode = True\nkey_path = None\nnginx = False\nnginx_server_root = /etc/nginx\nfullchain_path = /home/ubuntu/letsencrypt/chain.pem\nemail = None\ncsr = None\nagree_dev_preview = None\nredirect = None\nverb = certonly\nverbose_count = -3\nconfig_file = None\nrenew_by_default = False\nhsts = False\napache_handle_sites = True\nauthenticator = standalone\ndomains = isnot.org,\nrsa_key_size = 2048\nelliptic_curve = secp256r1\napache_challenge_location = /etc/apache2\ncheckpoints = 1\nmanual_test_mode = False\napache = False\ncert_path = /home/ubuntu/letsencrypt/cert.pem\nwebroot_path = None\nreinstall = False\nexpand = False\nstrict_permissions = False\napache_server_root = /etc/apache2\naccount = None\ndry_run = False\nmanual_public_ip_logging_ok = False\nchain_path = /home/ubuntu/letsencrypt/chain.pem\nbreak_my_certs = False\nstandalone = True\nmanual = False\nserver = https://acme-staging-v02.api.letsencrypt.org/directory\nwebroot = False\nos_packages_only = False\napache_init_script = None\nuser_agent = None\napache_le_vhost_ext = -le-ssl.conf\ndebug = False\nlogs_dir = /var/log/letsencrypt\napache_vhost_root = /etc/apache2/sites-available\nconfigurator = None\nmust_staple = True\n[[webroot_map]]\n"
  },
  {
    "path": "certbot/src/certbot/tests/testdata/webrootconftest.ini",
    "content": "webroot\nwebroot-path = /tmp\ndomains = eg.com, eg2.com\n"
  },
  {
    "path": "certbot/src/certbot/tests/util.py",
    "content": "\"\"\"Test utilities.\"\"\"\nimport atexit\nfrom contextlib import ExitStack\nimport copy\nfrom importlib import reload as reload_module\nimport importlib.resources\nimport io\nimport logging\nimport multiprocessing\nfrom multiprocessing import synchronize\nimport shutil\nimport sys\nimport tempfile\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import IO\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\nimport unittest\nfrom unittest import mock\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey\nimport josepy as jose\n\nfrom certbot import configuration\nfrom certbot import util\nfrom certbot._internal import constants\nfrom certbot._internal import lock\nfrom certbot._internal import storage\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\n\n\nclass DummyInstaller(common.Installer):\n    \"\"\"Dummy installer plugin for test purpose.\"\"\"\n    def get_all_names(self) -> Iterable[str]:\n        return []\n\n    def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str,\n                    fullchain_path: str) -> None:\n        pass\n\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[list[str], str]] = None) -> None:\n        pass\n\n    def supported_enhancements(self) -> list[str]:\n        return []\n\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        pass\n\n    def config_test(self) -> None:\n        pass\n\n    def restart(self) -> None:\n        pass\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        pass\n\n    def prepare(self) -> None:\n        pass\n\n    def more_info(self) -> str:\n        return \"\"\n\n\ndef vector_path(*names: str) -> str:\n    \"\"\"Path to a test vector.\"\"\"\n    _file_manager = ExitStack()\n    atexit.register(_file_manager.close)\n    vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)\n    path = _file_manager.enter_context(importlib.resources.as_file(vector_ref))\n    return str(path)\n\n\ndef load_vector(*names: str) -> bytes:\n    \"\"\"Load contents of a test vector.\"\"\"\n    vector_ref = importlib.resources.files(__package__).joinpath('testdata', *names)\n    data = vector_ref.read_bytes()\n    # Try at most to convert CRLF to LF when data is text\n    try:\n        return data.decode().replace('\\r\\n', '\\n').encode()\n    except ValueError:\n        # Failed to process the file with standard encoding.\n        # Most likely not a text file, return its bytes untouched.\n        return data\n\n\ndef _guess_loader(filename: str, loader_pem: Callable, loader_der: Callable) -> Callable:\n    _, ext = os.path.splitext(filename)\n    if ext.lower() == '.pem':\n        return loader_pem\n    elif ext.lower() == '.der':\n        return loader_der\n    raise ValueError(\"Loader could not be recognized based on extension\")  # pragma: no cover\n\n\ndef load_cert(*names: str) -> x509.Certificate:\n    \"\"\"Load certificate.\"\"\"\n    loader = _guess_loader(\n        names[-1], x509.load_pem_x509_certificate, x509.load_der_x509_certificate\n    )\n    return loader(load_vector(*names))\n\n\ndef load_jose_rsa_private_key_pem(*names: str) -> jose.ComparableRSAKey:\n    \"\"\"Load RSA private key wrapped in jose.ComparableRSAKey\"\"\"\n    return jose.ComparableRSAKey(load_rsa_private_key_pem(*names))\n\n\ndef load_rsa_private_key_pem(*names: str) -> RSAPrivateKey:\n    \"\"\"Load RSA private key.\"\"\"\n    _, ext = os.path.splitext(names[-1])\n    assert ext.lower() == '.pem'\n    key = serialization.load_pem_private_key(\n        load_vector(*names), password=None, backend=default_backend())\n    assert isinstance(key, RSAPrivateKey)\n    return key\n\n\ndef make_lineage(config_dir: str, testfile: str, ec: bool = True) -> str:\n    \"\"\"Creates a lineage defined by testfile.\n\n    This creates the archive, live, and renewal directories if\n    necessary and creates a simple lineage.\n\n    :param str config_dir: path to the configuration directory\n    :param str testfile: configuration file to base the lineage on\n    :param bool ec: True if we generate the lineage with an ECDSA key\n\n    :returns: path to the renewal conf file for the created lineage\n    :rtype: str\n\n    \"\"\"\n    lineage_name = testfile[:-len('.conf')]\n\n    conf_dir = os.path.join(\n        config_dir, constants.RENEWAL_CONFIGS_DIR)\n    archive_dir = os.path.join(\n        config_dir, constants.ARCHIVE_DIR, lineage_name)\n    live_dir = os.path.join(\n        config_dir, constants.LIVE_DIR, lineage_name)\n\n    for directory in (archive_dir, conf_dir, live_dir,):\n        if not os.path.exists(directory):\n            filesystem.makedirs(directory)\n\n    sample_archive = vector_path('sample-archive{}'.format('-ec' if ec else ''))\n    for kind in os.listdir(sample_archive):\n        shutil.copyfile(os.path.join(sample_archive, kind),\n                        os.path.join(archive_dir, kind))\n\n    for kind in storage.ALL_FOUR:\n        os.symlink(os.path.join(archive_dir, '{0}1.pem'.format(kind)),\n                   os.path.join(live_dir, '{0}.pem'.format(kind)))\n\n    conf_path = os.path.join(config_dir, conf_dir, testfile)\n    with open(vector_path(testfile)) as src:\n        with open(conf_path, 'w') as dst:\n            dst.writelines(\n                line.replace('MAGICDIR', config_dir) for line in src)\n\n    return conf_path\n\n\ndef patch_display_util() -> mock.MagicMock:\n    \"\"\"Patch certbot.display.util to use a special mock display utility.\n\n    The mock display utility works like a regular mock object, except it also\n    also asserts that methods are called with valid arguments.\n\n    The mock created by this patch mocks out Certbot internals. That is, the\n    mock object will be called by the certbot.display.util functions and the\n    mock returned by that call will be used as the display utility. This was\n    done to simplify the transition from zope.component and mocking\n    certbot.display.util functions directly in test code should be preferred\n    over using this function in the future.\n\n    See https://github.com/certbot/certbot/issues/8948\n\n    :returns: patch on the function used internally by certbot.display.util to\n        get a display utility instance\n    :rtype: mock.MagicMock\n\n    \"\"\"\n    return cast(mock.MagicMock, mock.patch('certbot._internal.display.obj.get_display',\n                                           new_callable=_create_display_util_mock))\n\n\ndef patch_display_util_with_stdout(\n        stdout: Optional[IO] = None) -> mock.MagicMock:\n    \"\"\"Patch certbot.display.util to use a special mock display utility.\n\n    The mock display utility works like a regular mock object, except it also\n    asserts that methods are called with valid arguments.\n\n    The mock created by this patch mocks out Certbot internals. That is, the\n    mock object will be called by the certbot.display.util functions and the\n    mock returned by that call will be used as the display utility. This was\n    done to simplify the transition from zope.component and mocking\n    certbot.display.util functions directly in test code should be preferred\n    over using this function in the future.\n\n    See https://github.com/certbot/certbot/issues/8948\n\n    The `message` argument passed to the display utility methods is passed to\n    stdout's write method.\n\n    :param object stdout: object to write standard output to; it is\n        expected to have a `write` method\n    :returns: patch on the function used internally by certbot.display.util to\n        get a display utility instance\n    :rtype: mock.MagicMock\n\n    \"\"\"\n    stdout = stdout if stdout else io.StringIO()\n\n    return cast(mock.MagicMock, mock.patch('certbot._internal.display.obj.get_display',\n                                           new=_create_display_util_mock_with_stdout(stdout)))\n\n\nclass FreezableMock:\n    \"\"\"Mock object with the ability to freeze attributes.\n\n    This class works like a regular mock.MagicMock object, except\n    attributes and behavior set before the object is frozen cannot\n    be changed during tests.\n\n    If a func argument is provided to the constructor, this function\n    is called first when an instance of FreezableMock is called,\n    followed by the usual behavior defined by MagicMock. The return\n    value of func is ignored.\n\n    \"\"\"\n    def __init__(self, frozen: bool = False, func: Optional[Callable[..., Any]] = None,\n                 return_value: Any = mock.sentinel.DEFAULT) -> None:\n        self._frozen_set = set() if frozen else {'freeze', }\n        self._func = func\n        self._mock = mock.MagicMock()\n        if return_value != mock.sentinel.DEFAULT:\n            self.return_value = return_value\n        self._frozen = frozen\n\n    def freeze(self) -> None:\n        \"\"\"Freeze object preventing further changes.\"\"\"\n        self._frozen = True\n\n    def __call__(self, *args: Any, **kwargs: Any) -> mock.MagicMock:\n        if self._func is not None:\n            self._func(*args, **kwargs)\n        return self._mock(*args, **kwargs)\n\n    def __getattribute__(self, name: str) -> Any:\n        if name == '_frozen':\n            try:\n                return object.__getattribute__(self, name)\n            except AttributeError:\n                return False\n        elif name in ('return_value', 'side_effect',):\n            return getattr(object.__getattribute__(self, '_mock'), name)\n        elif name == '_frozen_set' or name in self._frozen_set:\n            return object.__getattribute__(self, name)\n        else:\n            return getattr(object.__getattribute__(self, '_mock'), name)\n\n    def __setattr__(self, name: str, value: Any) -> None:\n        \"\"\" Before it is frozen, attributes are set on the FreezableMock\n        instance and added to the _frozen_set. Attributes in the _frozen_set\n        cannot be changed after the FreezableMock is frozen. In this case,\n        they are set on the underlying _mock.\n\n        In cases of return_value and side_effect, these attributes are always\n        passed through to the instance's _mock and added to the _frozen_set\n        before the object is frozen.\n\n        \"\"\"\n        if self._frozen:\n            if name in self._frozen_set:\n                raise AttributeError('Cannot change frozen attribute ' + name)\n            return setattr(self._mock, name, value)\n\n        if name != '_frozen_set':\n            self._frozen_set.add(name)\n\n        if name in ('return_value', 'side_effect'):\n            return setattr(self._mock, name, value)\n\n        return object.__setattr__(self, name, value)\n\n\ndef _create_display_util_mock() -> FreezableMock:\n    display = FreezableMock()\n    # Use pylint code for disable to keep on single line under line length limit\n    method_list = [func for func in dir(display_obj.FileDisplay)\n                   if callable(getattr(display_obj.FileDisplay, func))\n                   and not func.startswith(\"__\")]\n    for method in method_list:\n        if method != 'notification':\n            frozen_mock = FreezableMock(frozen=True, func=_assert_valid_call)\n            setattr(display, method, frozen_mock)\n    display.freeze()\n    return FreezableMock(frozen=True, return_value=display)\n\n\ndef _create_display_util_mock_with_stdout(stdout: IO) -> FreezableMock:\n    def _write_msg(message: str, *unused_args: Any, **unused_kwargs: Any) -> None:\n        \"\"\"Write to message to stdout.\n        \"\"\"\n        if message:\n            stdout.write(message)\n\n    def mock_method(*args: Any, **kwargs: Any) -> None:\n        \"\"\"\n        Mock function for display utility methods.\n        \"\"\"\n        _assert_valid_call(args, kwargs)\n        _write_msg(*args, **kwargs)\n\n    display = FreezableMock()\n    # Use pylint code for disable to keep on single line under line length limit\n    method_list = [func for func in dir(display_obj.FileDisplay)\n                   if callable(getattr(display_obj.FileDisplay, func))\n                   and not func.startswith(\"__\")]\n    for method in method_list:\n        if method == 'notification':\n            frozen_mock = FreezableMock(frozen=True,\n                                        func=_write_msg)\n        else:\n            frozen_mock = FreezableMock(frozen=True,\n                                        func=mock_method)\n        setattr(display, method, frozen_mock)\n    display.freeze()\n    return FreezableMock(frozen=True, return_value=display)\n\n\ndef _assert_valid_call(*args: Any, **kwargs: Any) -> None:\n    assert_args = [args[0] if args else kwargs['message']]\n\n    assert_kwargs = {\n        'default': kwargs.get('default', None),\n        'cli_flag': kwargs.get('cli_flag', None),\n        'force_interactive': kwargs.get('force_interactive', False),\n    }\n\n    display_util.assert_valid_call(*assert_args, **assert_kwargs)\n\n\nclass TempDirTestCase(unittest.TestCase):\n    \"\"\"Base test class which sets up and tears down a temporary directory\"\"\"\n\n    def setUp(self) -> None:\n        \"\"\"Execute before test\"\"\"\n        self.tempdir = tempfile.mkdtemp()\n\n    def tearDown(self) -> None:\n        \"\"\"Execute after test\"\"\"\n        # Cleanup opened resources after a test. This is usually done through atexit handlers in\n        # Certbot, but during tests, atexit will not run registered functions before tearDown is\n        # called and instead will run them right before the entire test process exits.\n        # It is a problem on Windows, that does not accept to clean resources before closing them.\n        logging.shutdown()\n        # Remove logging handlers that have been closed so they won't be\n        # accidentally used in future tests.\n        logging.getLogger().handlers = []\n        util._release_locks()  # pylint: disable=protected-access\n\n        shutil.rmtree(self.tempdir)\n\n\nclass ConfigTestCase(TempDirTestCase):\n    \"\"\"Test class which sets up a NamespaceConfig object.\"\"\"\n    def setUp(self) -> None:\n        super().setUp()\n        self.config = configuration.NamespaceConfig(\n            # We make a copy here so any mutable values from CLI_DEFAULTS do not get modified.\n            mock.MagicMock(**copy.deepcopy(constants.CLI_DEFAULTS)),\n        )\n        self.config.set_argument_sources({})\n        self.config.namespace.verb = \"certonly\"\n        self.config.namespace.config_dir = os.path.join(self.tempdir, 'config')\n        self.config.namespace.work_dir = os.path.join(self.tempdir, 'work')\n        self.config.namespace.logs_dir = os.path.join(self.tempdir, 'logs')\n        self.config.namespace.cert_path = constants.CLI_DEFAULTS['auth_cert_path']\n        self.config.namespace.fullchain_path = constants.CLI_DEFAULTS['auth_chain_path']\n        self.config.namespace.chain_path = constants.CLI_DEFAULTS['auth_chain_path']\n        self.config.namespace.server = \"https://example.com\"\n\n\ndef _handle_lock(event_in: synchronize.Event, event_out: synchronize.Event, path: str) -> None:\n    \"\"\"\n    Acquire a file lock on given path, then wait to release it. This worker is coordinated\n    using events to signal when the lock should be acquired and released.\n    :param multiprocessing.Event event_in: event object to signal when to release the lock\n    :param multiprocessing.Event event_out: event object to signal when the lock is acquired\n    :param path: the path to lock\n    \"\"\"\n    if os.path.isdir(path):\n        my_lock = lock.lock_dir(path)\n    else:\n        my_lock = lock.LockFile(path)\n    try:\n        event_out.set()\n        assert event_in.wait(timeout=20), 'Timeout while waiting to release the lock.'\n    finally:\n        my_lock.release()\n\n\ndef lock_and_call(callback: Callable[[], Any], path_to_lock: str) -> None:\n    \"\"\"\n    Grab a lock on path_to_lock from a foreign process then execute the callback.\n    :param callable callback: object to call after acquiring the lock\n    :param str path_to_lock: path to file or directory to lock\n    \"\"\"\n    # Reload certbot.util module to reset internal _LOCKS dictionary.\n    reload_module(util)\n\n    emit_event = multiprocessing.Event()\n    receive_event = multiprocessing.Event()\n    process = multiprocessing.Process(target=_handle_lock,\n                                      args=(emit_event, receive_event, path_to_lock))\n    process.start()\n\n    # Wait confirmation that lock is acquired\n    assert receive_event.wait(timeout=20), 'Timeout while waiting to acquire the lock.'\n    # Execute the callback\n    callback()\n    # Trigger unlock from foreign process\n    emit_event.set()\n\n    # Wait for process termination\n    process.join(timeout=10)\n    assert process.exitcode == 0\n\n\ndef skip_on_windows(reason: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:\n    \"\"\"Decorator to skip permanently a test on Windows. A reason is required.\"\"\"\n    def wrapper(function: Callable[..., Any]) -> Callable[..., Any]:\n        \"\"\"Wrapped version\"\"\"\n        return unittest.skipIf(sys.platform == 'win32', reason)(function)\n    return wrapper\n\n\ndef temp_join(path: str) -> str:\n    \"\"\"\n    Return the given path joined to the tempdir path for the current platform\n    Eg.: 'cert' => /tmp/cert (Linux) or 'C:\\\\Users\\\\currentuser\\\\AppData\\\\Temp\\\\cert' (Windows)\n    \"\"\"\n    return os.path.join(tempfile.gettempdir(), path)\n"
  },
  {
    "path": "certbot/src/certbot/util.py",
    "content": "\"\"\"Utilities for all Certbot.\"\"\"\nimport argparse\nimport atexit\nimport errno\nimport itertools\nimport logging\nimport platform\nimport re\nimport socket\nimport subprocess\nimport sys\nfrom typing import Any\nfrom typing import Callable\nfrom typing import IO\nfrom typing import NamedTuple\nfrom typing import Optional\nfrom typing import Union\n\nimport configargparse\n\nfrom certbot import errors\nfrom certbot._internal import constants\nfrom certbot._internal import lock\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\n\n_USE_DISTRO = sys.platform.startswith('linux')\nif _USE_DISTRO:\n    import distro\n\nlogger = logging.getLogger(__name__)\n\n\nclass Key(NamedTuple):\n    \"\"\"Container for an optional file path and contents for a PEM-formated private key.\"\"\"\n    file: Optional[str]\n    pem: bytes\n\n\nclass CSR(NamedTuple):\n    \"\"\"Container for an optional file path and contents for a PEM or DER-formatted CSR.\"\"\"\n    file: Optional[str]\n    data: bytes\n    # Note: form is the type of data, \"pem\" or \"der\"\n    form: str\n\n\nclass LooseVersion:\n    \"\"\"A version with loose rules, i.e. any given string is a valid version number.\n\n    but regular comparison is not supported. Instead, the `try_risky_comparison` method is\n    provided, which may return an error if two LooseVersions are 'incomparible'.\n    For example when integer and string version components are present in the same position.\n\n    Differences with old distutils.version.LooseVersion:\n    (https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/version.py#L269)\n    Most version comparisons should give the same result. However, if a version has multiple\n    trailing zeroes, not all of them are used in the comparison. This ensure that, for example,\n    \"2.0\" and \"2.0.0\" are equal.\n    \"\"\"\n\n    def __init__(self, version_string: str) -> None:\n        \"\"\"Parses a version string into its components.\n\n        :param str version_string: version string\n        \"\"\"\n        components: list[Union[int, str]]\n        components = [x for x in _VERSION_COMPONENT_RE.split(version_string)\n                              if x and x != '.']\n        for i, obj in enumerate(components):\n            try:\n                components[i] = int(obj)\n            except ValueError:\n                pass\n\n        self.version_components = components\n\n    def try_risky_comparison(self, other: 'LooseVersion') -> int:\n        \"\"\"Compares the LooseVersion to another value.\n\n        If the other value is another LooseVersion, the version components are compared. Otherwise,\n        an exception is raised.\n\n        Comparison is performed element-wise. If the version components being compared are of\n        different types, the two versions are considered incompatible. Otherwise, if either of the\n        components is not equal to the other, less or greater is returned based on the comparison's\n        result. In case the two versions are of different lengths, some elements in the longer\n        version have not yet been compared. If these are all equal to zero, the two versions are\n        equal. Otherwise, the longer version is greater.\n\n        If the two versions are incompatible, an exception is raised. Otherwise, the returned\n        integer indicates the result of the comparison. If self == other, 0 is returned.\n        If self > other, 1 is returned. If self < other -1 is returned.\n\n        Examples:\n        Equality:\n        - LooseVersion('1.0').try_risky_comparison(LooseVersion('1.0')) -> 0\n        - LooseVersion('2.0.0a').try_risky_comparison(LooseVersion('2.0.0a')) -> 0\n        Inequality:\n        - LooseVersion('2.0.0').try_risky_comparison(LooseVersion('1.0')) -> 1\n        - LooseVersion('1.0.1').try_risky_comparison(LooseVersion('2.0a')) -> -1\n        Incomparability:\n        - LooseVersion('1a').try_risky_comparison(LooseVersion('1.0')) -> ValueError\n        \"\"\"\n        try:\n            for self_vc, other_vc in itertools.zip_longest(self.version_components,\n                                                           other.version_components,\n                                                           fillvalue=0):\n                # ensure mypy ignores types here and catch any TypeErrors\n                if self_vc < other_vc:  # type: ignore\n                    return -1\n                elif self_vc > other_vc:  # type: ignore\n                    return 1\n            return 0\n        except TypeError:\n            raise ValueError(\"Cannot meaningfully compare LooseVersion {} with LooseVersion {} \"\n                             \"due to comparison of version components with different types.\"\n                             .format(self.version_components, other.version_components))\n\n\n# ANSI SGR escape codes\n# Formats text as bold or with increased intensity\nANSI_SGR_BOLD = '\\033[1m'\n# Colors text red\nANSI_SGR_RED = \"\\033[31m\"\n# Resets output format\nANSI_SGR_RESET = \"\\033[0m\"\n\n\nPERM_ERR_FMT = os.linesep.join((\n    \"The following error was encountered:\", \"{0}\",\n    \"Either run as root, or set --config-dir, \"\n    \"--work-dir, and --logs-dir to writeable paths.\"))\n\n\n# Stores importing process ID to be used by atexit_register()\n_INITIAL_PID = os.getpid()\n# Maps paths to locked directories to their lock object. All locks in\n# the dict are attempted to be cleaned up at program exit. If the\n# program exits before the lock is cleaned up, it is automatically\n# released, but the file isn't deleted.\n_LOCKS: dict[str, lock.LockFile] = {}\n_VERSION_COMPONENT_RE = re.compile(r'(\\d+ | [a-z]+ | \\.)', re.VERBOSE)\n\ndef env_no_snap_for_external_calls() -> dict[str, str]:\n    \"\"\"\n    When Certbot is run inside a Snap, certain environment variables\n    are modified. But Certbot sometimes calls out to external programs,\n    since it uses classic confinement. When we do that, we must modify\n    the env to remove our modifications so it will use the system's\n    libraries, since they may be incompatible with the versions of\n    libraries included in the Snap. For example, apachectl, Nginx, and\n    anything run from inside a hook should call this function and pass\n    the results into the ``env`` argument of ``subprocess.Popen``.\n\n    :returns: A modified copy of os.environ ready to pass to Popen\n    :rtype: dict\n\n    \"\"\"\n    env = os.environ.copy()\n    # Avoid accidentally modifying env\n    if 'SNAP' not in env or 'CERTBOT_SNAPPED' not in env:\n        return env\n\n    # These environment variables being set when running external programs can cause issues if these\n    # programs also use OpenSSL. See https://github.com/certbot/certbot/issues/10190.\n    env.pop('OPENSSL_FORCE_FIPS_MODE', None)\n    env.pop('OPENSSL_MODULES', None)\n\n    for path_name in ('PATH', 'LD_LIBRARY_PATH'):\n        if path_name in env:\n            env[path_name] = ':'.join(x for x in env[path_name].split(':') if env['SNAP'] not in x)\n    return env\n\n\ndef run_script(params: list[str], log: Callable[[str], None]=logger.error) -> tuple[str, str]:\n    \"\"\"Run the script with the given params.\n\n    :param list params: List of parameters to pass to subprocess.run\n    :param callable log: Logger method to use for errors\n\n    \"\"\"\n    try:\n        proc = subprocess.run(params,\n                              check=False,\n                              stdout=subprocess.PIPE,\n                              stderr=subprocess.PIPE,\n                              universal_newlines=True,\n                              env=env_no_snap_for_external_calls())\n\n    except (OSError, ValueError):\n        msg = \"Unable to run the command: %s\" % \" \".join(params)\n        log(msg)\n        raise errors.SubprocessError(msg)\n\n    if proc.returncode != 0:\n        msg = \"Error while running %s.\\n%s\\n%s\" % (\n            \" \".join(params), proc.stdout, proc.stderr)\n        # Enter recovery routine...\n        log(msg)\n        raise errors.SubprocessError(msg)\n\n    return proc.stdout, proc.stderr\n\n\ndef exe_exists(exe: str) -> bool:\n    \"\"\"Determine whether path/name refers to an executable.\n\n    :param str exe: Executable path or name\n\n    :returns: If exe is a valid executable\n    :rtype: bool\n\n    \"\"\"\n    path, _ = os.path.split(exe)\n    if path:\n        return filesystem.is_executable(exe)\n    for path in os.environ[\"PATH\"].split(os.pathsep):\n        if filesystem.is_executable(os.path.join(path, exe)):\n            return True\n\n    return False\n\n\ndef lock_dir_until_exit(dir_path: str) -> None:\n    \"\"\"Lock the directory at dir_path until program exit.\n\n    :param str dir_path: path to directory\n\n    :raises errors.LockError: if the lock is held by another process\n\n    \"\"\"\n    if not _LOCKS:  # this is the first lock to be released at exit\n        atexit_register(_release_locks)\n\n    if dir_path not in _LOCKS:\n        _LOCKS[dir_path] = lock.lock_dir(dir_path)\n\n\ndef _release_locks() -> None:\n    for dir_lock in _LOCKS.values():\n        try:\n            dir_lock.release()\n        except:  # pylint: disable=bare-except\n            msg = 'Exception occurred releasing lock: {0!r}'.format(dir_lock)\n            logger.debug(msg, exc_info=True)\n    _LOCKS.clear()\n\n\ndef set_up_core_dir(directory: str, mode: int, strict: bool) -> None:\n    \"\"\"Ensure directory exists with proper permissions and is locked.\n\n    :param str directory: Path to a directory.\n    :param int mode: Directory mode.\n    :param bool strict: require directory to be owned by current user\n\n    :raises .errors.LockError: if the directory cannot be locked\n    :raises .errors.Error: if the directory cannot be made or verified\n\n    \"\"\"\n    try:\n        make_or_verify_dir(directory, mode, strict)\n        lock_dir_until_exit(directory)\n    except OSError as error:\n        logger.debug(\"Exception was:\", exc_info=True)\n        raise errors.Error(PERM_ERR_FMT.format(error))\n\n\ndef make_or_verify_dir(directory: str, mode: int = 0o755, strict: bool = False) -> None:\n    \"\"\"Make sure directory exists with proper permissions.\n\n    :param str directory: Path to a directory.\n    :param int mode: Directory mode.\n    :param bool strict: require directory to be owned by current user\n\n    :raises .errors.Error: if a directory already exists,\n        but has wrong permissions or owner\n\n    :raises OSError: if invalid or inaccessible file names and\n        paths, or other arguments that have the correct type,\n        but are not accepted by the operating system.\n\n    \"\"\"\n    try:\n        filesystem.makedirs(directory, mode)\n    except OSError as exception:\n        if exception.errno == errno.EEXIST:\n            if strict and not filesystem.check_permissions(directory, mode):\n                raise errors.Error(\n                    \"%s exists, but it should be owned by current user with\"\n                    \" permissions %s\" % (directory, oct(mode)))\n        else:\n            raise\n\n\ndef safe_open(path: str, mode: str = \"w\", chmod: Optional[int] = None) -> IO:\n    \"\"\"Safely open a file.\n\n    :param str path: Path to a file.\n    :param str mode: Same os `mode` for `open`.\n    :param int chmod: Same as `mode` for `filesystem.open`, uses Python defaults\n        if ``None``.\n\n    \"\"\"\n    open_args: Union[tuple[()], tuple[int]] = ()\n    if chmod is not None:\n        open_args = (chmod,)\n    fdopen_args: Union[tuple[()], tuple[int]] = ()\n    fd = filesystem.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args)\n    return os.fdopen(fd, mode, *fdopen_args)\n\n\ndef _unique_file(path: str, filename_pat: Callable[[int], str], count: int,\n                 chmod: int, mode: str) -> tuple[IO, str]:\n    while True:\n        current_path = os.path.join(path, filename_pat(count))\n        try:\n            return safe_open(current_path, chmod=chmod, mode=mode), os.path.abspath(current_path)\n        except OSError as err:\n            # \"File exists,\" is okay, try a different name.\n            if err.errno != errno.EEXIST:\n                raise\n        count += 1\n\n\ndef unique_file(path: str, chmod: int = 0o777, mode: str = \"w\") -> tuple[IO, str]:\n    \"\"\"Safely finds a unique file.\n\n    :param str path: path/filename.ext\n    :param int chmod: File mode\n    :param str mode: Open mode\n\n    :returns: tuple of file object and file name\n\n    \"\"\"\n    path, tail = os.path.split(path)\n    return _unique_file(\n        path, filename_pat=(lambda count: \"%04d_%s\" % (count, tail)),\n        count=0, chmod=chmod, mode=mode)\n\n\ndef unique_lineage_name(path: str, filename: str, chmod: int = 0o644,\n                        mode: str = \"w\") -> tuple[IO, str]:\n    \"\"\"Safely finds a unique file using lineage convention.\n\n    :param str path: directory path\n    :param str filename: proposed filename\n    :param int chmod: file mode\n    :param str mode: open mode\n\n    :returns: tuple of file object and file name (which may be modified\n        from the requested one by appending digits to ensure uniqueness)\n\n    :raises OSError: if writing files fails for an unanticipated reason,\n        such as a full disk or a lack of permission to write to\n        specified location.\n\n    \"\"\"\n    preferred_path = os.path.join(path, \"%s.conf\" % (filename))\n    try:\n        return safe_open(preferred_path, chmod=chmod), preferred_path\n    except OSError as err:\n        if err.errno != errno.EEXIST:\n            raise\n    return _unique_file(\n        path, filename_pat=(lambda count: \"%s-%04d.conf\" % (filename, count)),\n        count=1, chmod=chmod, mode=mode)\n\n\ndef safely_remove(path: str) -> None:\n    \"\"\"Remove a file that may not exist.\"\"\"\n    try:\n        os.remove(path)\n    except OSError as err:\n        if err.errno != errno.ENOENT:\n            raise\n\n\ndef get_filtered_names(all_names: set[str]) -> set[str]:\n    \"\"\"Removes names that aren't considered valid by Let's Encrypt.\n\n    :param set all_names: all names found in the configuration\n\n    :returns: all found names that are considered valid by LE\n    :rtype: set\n\n    \"\"\"\n    filtered_names = set()\n    for name in all_names:\n        try:\n            filtered_names.add(enforce_le_validity(name))\n        except errors.ConfigurationError:\n            logger.debug('Not suggesting name \"%s\"', name, exc_info=True)\n    return filtered_names\n\ndef get_os_info() -> tuple[str, str]:\n    \"\"\"\n    Get OS name and version\n\n    :returns: (os_name, os_version)\n    :rtype: `tuple` of `str`\n    \"\"\"\n\n    return get_python_os_info(pretty=False)\n\ndef get_os_info_ua() -> str:\n    \"\"\"\n    Get OS name and version string for User Agent\n\n    :returns: os_ua\n    :rtype: `str`\n\n    \"\"\"\n    # distro.name returns an empty string if one cannot be determined. see\n    # https://github.com/python-distro/distro/blob/3bd19e61fcb7f8d2bf3d45d9e40d69c92e05d241/src/distro/distro.py#L883\n    os_info = \"\"\n    if _USE_DISTRO:\n        os_info = distro.name(pretty=True)\n\n    if not _USE_DISTRO or not os_info:\n        return \" \".join(get_python_os_info(pretty=True))\n    return os_info\n\ndef get_systemd_os_like() -> list[str]:\n    \"\"\"\n    Get a list of strings that indicate the distribution likeness to\n    other distributions.\n\n    :returns: List of distribution acronyms\n    :rtype: `list` of `str`\n    \"\"\"\n\n    if _USE_DISTRO:\n        return distro.like().split(\" \")\n    return []\n\ndef get_var_from_file(varname: str, filepath: str = \"/etc/os-release\") -> str:\n    \"\"\"\n    Get single value from a file formatted like systemd /etc/os-release\n\n    :param str varname: Name of variable to fetch\n    :param str filepath: File path of os-release file\n    :returns: requested value\n    :rtype: `str`\n    \"\"\"\n\n    var_string = varname+\"=\"\n    if not os.path.isfile(filepath):\n        return \"\"\n    with open(filepath, 'r') as fh:\n        contents = fh.readlines()\n\n    for line in contents:\n        if line.strip().startswith(var_string):\n            # Return the value of var, normalized\n            return _normalize_string(line.strip()[len(var_string):])\n    return \"\"\n\ndef _normalize_string(orig: str) -> str:\n    \"\"\"\n    Helper function for get_var_from_file() to remove quotes\n    and whitespaces\n    \"\"\"\n    return orig.replace('\"', '').replace(\"'\", \"\").strip()\n\ndef get_python_os_info(pretty: bool = False) -> tuple[str, str]:\n    \"\"\"\n    Get Operating System type/distribution and major version\n    using python platform module\n\n    :param bool pretty: If the returned OS name should be in longer (pretty) form\n\n    :returns: (os_name, os_version)\n    :rtype: `tuple` of `str`\n    \"\"\"\n    info = platform.system_alias(\n        platform.system(),\n        platform.release(),\n        platform.version()\n    )\n    os_type, os_ver, _ = info\n    os_type = os_type.lower()\n    if os_type.startswith('linux') and _USE_DISTRO:\n        distro_name, distro_version = distro.name() if pretty else distro.id(), distro.version()\n        # On arch, these values are reportedly empty strings so handle it\n        # defensively\n        # so handle it defensively\n        if distro_name:\n            os_type = distro_name\n        if distro_version:\n            os_ver = distro_version\n    elif os_type.startswith('darwin'):\n        try:\n            proc = subprocess.run(\n                [\"/usr/bin/sw_vers\", \"-productVersion\"],\n                stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                check=False, universal_newlines=True,\n                env=env_no_snap_for_external_calls(),\n            )\n        except OSError:\n            proc = subprocess.run(\n                [\"sw_vers\", \"-productVersion\"],\n                stdout=subprocess.PIPE, stderr=subprocess.PIPE,\n                check=False, universal_newlines=True,\n                env=env_no_snap_for_external_calls(),\n            )\n        os_ver = proc.stdout.rstrip('\\n')\n    elif os_type.startswith('freebsd'):\n        # eg \"9.3-RC3-p1\"\n        os_ver = os_ver.partition(\"-\")[0]\n        os_ver = os_ver.partition(\".\")[0]\n    elif platform.win32_ver()[1]:\n        os_ver = platform.win32_ver()[1]\n    else:\n        # Cases known to fall here: Cygwin python\n        os_ver = ''\n    return os_type, os_ver\n\n# Just make sure we don't get pwned... Make sure that it also doesn't\n# start with a period or have two consecutive periods <- this needs to\n# be done in addition to the regex\nEMAIL_REGEX = re.compile(\"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$\")\n\n\ndef safe_email(email: str) -> bool:\n    \"\"\"Scrub email address before using it.\"\"\"\n    if EMAIL_REGEX.match(email) is not None:\n        return not email.startswith(\".\") and \"..\" not in email\n    logger.error(\"Invalid email address: %s.\", email)\n    return False\n\n\nclass DeprecatedArgumentAction(argparse.Action):\n    \"\"\"Action to log a warning when an argument is used.\"\"\"\n    def __call__(self, unused1: Any, unused2: Any, unused3: Any,\n                 option_string: Optional[str] = None) -> None:\n        logger.warning(\"Use of %s is deprecated.\", option_string)\n\n\ndef add_deprecated_argument(add_argument: Callable[..., None], argument_name: str,\n                            nargs: Union[str, int]) -> None:\n    \"\"\"Adds a deprecated argument with the name argument_name.\n\n    Deprecated arguments are not shown in the help. If they are used on\n    the command line, a warning is shown stating that the argument is\n    deprecated and no other action is taken.\n\n    :param callable add_argument: Function that adds arguments to an\n        argument parser/group.\n    :param str argument_name: Name of deprecated argument.\n    :param nargs: Value for nargs when adding the argument to argparse.\n\n    \"\"\"\n    if DeprecatedArgumentAction not in configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE:\n        # In version 0.12.0 ACTION_TYPES_THAT_DONT_NEED_A_VALUE was\n        # changed from a set to a tuple.\n        if isinstance(configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE, set):\n            configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(\n                DeprecatedArgumentAction)\n        else:\n            configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE += (\n                DeprecatedArgumentAction,)\n    add_argument(argument_name, action=DeprecatedArgumentAction,\n                 help=argparse.SUPPRESS, nargs=nargs)\n\n\ndef enforce_le_validity(domain: str) -> str:\n    \"\"\"Checks that Let's Encrypt will consider domain to be valid.\n\n    :param str domain: FQDN to check\n    :type domain: `str`\n    :returns: The domain cast to `str`, with ASCII-only contents\n    :rtype: str\n    :raises ConfigurationError: for invalid domains and cases where Let's\n                                Encrypt currently will not issue certificates\n\n    \"\"\"\n\n    domain = enforce_domain_sanity(domain)\n    if not re.match(\"^[A-Za-z0-9.-]*$\", domain):\n        raise errors.ConfigurationError(\n            \"{0} contains an invalid character. \"\n            \"Valid characters are A-Z, a-z, 0-9, ., and -.\".format(domain))\n\n    labels = domain.split(\".\")\n    if len(labels) < 2:\n        raise errors.ConfigurationError(\n            \"{0} needs at least two labels\".format(domain))\n    for label in labels:\n        if label.startswith(\"-\"):\n            raise errors.ConfigurationError(\n                'label \"{0}\" in domain \"{1}\" cannot start with \"-\"'.format(\n                    label, domain))\n        if label.endswith(\"-\"):\n            raise errors.ConfigurationError(\n                'label \"{0}\" in domain \"{1}\" cannot end with \"-\"'.format(\n                    label, domain))\n    return domain\n\n\ndef enforce_domain_sanity(domain: Union[str, bytes]) -> str:\n    \"\"\"Method which validates domain value and errors out if\n    the requirements are not met.\n\n    :param domain: Domain to check\n    :type domain: `str` or `bytes`\n    :raises ConfigurationError: for invalid domains and cases where Let's\n                                Encrypt currently will not issue certificates\n\n    :returns: The domain cast to `str`, with ASCII-only contents\n    :rtype: str\n    \"\"\"\n    # Unicode\n    try:\n        if isinstance(domain, bytes):\n            domain = domain.decode('utf-8')\n        domain.encode('ascii')\n    except UnicodeError:\n        raise errors.ConfigurationError(\"Non-ASCII domain names not supported. \"\n            \"To issue for an Internationalized Domain Name, use Punycode.\")\n\n    domain = domain.lower()\n\n    # Remove trailing dot\n    domain = domain[:-1] if domain.endswith('.') else domain\n\n    # Separately check for odd \"domains\" like \"http://example.com\" to fail\n    # fast and provide a clear error message\n    for scheme in [\"http\", \"https\"]:  # Other schemes seem unlikely\n        if domain.startswith(\"{0}://\".format(scheme)):\n            raise errors.ConfigurationError(\n                \"Requested name {0} appears to be a URL, not a FQDN. \"\n                \"Try again without the leading \\\"{1}://\\\".\".format(\n                    domain, scheme\n                )\n            )\n\n    if is_ipaddress(domain):\n        raise errors.ConfigurationError(\n            \"Requested name {0} is an IP address. The Let's Encrypt \"\n            \"certificate authority will not issue certificates for a \"\n            \"bare IP address.\".format(domain))\n\n    # FQDN checks according to RFC 2181: domain name should be less than 255\n    # octets (inclusive). And each label is 1 - 63 octets (inclusive).\n    # https://tools.ietf.org/html/rfc2181#section-11\n    msg = \"Requested domain {0} is not a FQDN because\".format(domain)\n    if len(domain) > 255:\n        raise errors.ConfigurationError(\"{0} it is too long.\".format(msg))\n    labels = domain.split('.')\n    for l in labels:\n        if not l:\n            raise errors.ConfigurationError(\"{0} it contains an empty label.\".format(msg))\n        if len(l) > 63:\n            raise errors.ConfigurationError(\"{0} label {1} is too long.\".format(msg, l))\n\n    return domain\n\n\ndef is_ipaddress(address: str) -> bool:\n    \"\"\"Is given address string form of IP(v4 or v6) address?\n\n    :param address: address to check\n    :type address: `str`\n\n    :returns: True if address is valid IP address, otherwise return False.\n    :rtype: bool\n\n    \"\"\"\n    try:\n        socket.inet_pton(socket.AF_INET, address)\n        # If this line runs it was ip address (ipv4)\n        return True\n    except OSError:\n        # It wasn't an IPv4 address, so try ipv6\n        try:\n            socket.inet_pton(socket.AF_INET6, address)\n            return True\n        except OSError:\n            return False\n\n\ndef is_wildcard_domain(domain: Union[str, bytes]) -> bool:\n    \"\"\"\"Is domain a wildcard domain?\n\n    :param domain: domain to check\n    :type domain: `bytes` or `str`\n\n    :returns: True if domain is a wildcard, otherwise, False\n    :rtype: bool\n\n    \"\"\"\n    if isinstance(domain, str):\n        return domain.startswith(\"*.\")\n    return domain.startswith(b\"*.\")\n\n\ndef is_staging(srv: str) -> bool:\n    \"\"\"\n    Determine whether a given ACME server is a known test / staging server.\n\n    :param str srv: the URI for the ACME server\n    :returns: True iff srv is a known test / staging server\n    :rtype bool:\n    \"\"\"\n    return srv == constants.STAGING_URI or \"staging\" in srv\n\n\ndef atexit_register(func: Callable, *args: Any, **kwargs: Any) -> None:\n    \"\"\"Sets func to be called before the program exits.\n\n    Special care is taken to ensure func is only called when the process\n    that first imports this module exits rather than any child processes.\n\n    :param function func: function to be called in case of an error\n\n    \"\"\"\n    atexit.register(_atexit_call, func, *args, **kwargs)\n\n\ndef parse_loose_version(version_string: str) -> list[Union[int, str]]:\n    \"\"\"Parses a version string into its components.\n    This code and the returned tuple is based on the now deprecated\n    distutils.version.LooseVersion class from the Python standard library.\n    Two LooseVersion classes and two lists as returned by this function should\n    compare in the same way. See\n    https://github.com/python/cpython/blob/v3.10.0/Lib/distutils/version.py#L205-L347.\n    :param str version_string: version string\n    :returns: list of parsed version string components\n    :rtype: list\n    \"\"\"\n    loose_version = LooseVersion(version_string)\n    return loose_version.version_components\n\n\ndef _atexit_call(func: Callable, *args: Any, **kwargs: Any) -> None:\n    if _INITIAL_PID == os.getpid():\n        func(*args, **kwargs)\n"
  },
  {
    "path": "certbot-apache/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-apache/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include src/certbot_apache/_internal/augeas_lens *.aug\nrecursive-include src/certbot_apache/_internal/tls_configs *.conf\nrecursive-include src/certbot_apache/_internal/tests/testdata *\ninclude src/certbot_apache/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-apache/README.rst",
    "content": "Apache plugin for Certbot\n"
  },
  {
    "path": "certbot-apache/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-apache\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Apache plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndev = [\n    \"apacheconfig>=0.3.2\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\napache = \"certbot_apache._internal.entrypoint:ENTRYPOINT\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-apache/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    # We specify the minimum acme and certbot version as the current plugin\n    # version for simplicity. See\n    # https://github.com/certbot/certbot/issues/8761 for more info.\n    f'acme>={version}',\n    f'certbot>={version}',\n    'python-augeas',\n]\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/__init__.py",
    "content": "\"\"\"Certbot Apache plugin.\"\"\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/__init__.py",
    "content": "\"\"\"Certbot Apache plugin.\"\"\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/apache_util.py",
    "content": "\"\"\" Utility functions for certbot-apache plugin \"\"\"\nimport atexit\nimport binascii\nimport fnmatch\nimport importlib.resources\nimport logging\nimport re\nimport subprocess\nfrom contextlib import ExitStack\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import os\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_mod_deps(mod_name: str) -> list[str]:\n    \"\"\"Get known module dependencies.\n\n    .. note:: This does not need to be accurate in order for the client to\n        run.  This simply keeps things clean if the user decides to revert\n        changes.\n    .. warning:: If all deps are not included, it may cause incorrect parsing\n        behavior, due to enable_mod's shortcut for updating the parser's\n        currently defined modules (`.ApacheParser.add_mod`)\n        This would only present a major problem in extremely atypical\n        configs that use ifmod for the missing deps.\n\n    \"\"\"\n    deps = {\n        \"ssl\": [\"setenvif\", \"mime\"]\n    }\n    return deps.get(mod_name, [])\n\n\ndef get_file_path(vhost_path: str) -> Optional[str]:\n    \"\"\"Get file path from augeas_vhost_path.\n\n    Takes in Augeas path and returns the file name\n\n    :param str vhost_path: Augeas virtual host path\n\n    :returns: filename of vhost\n    :rtype: str\n\n    \"\"\"\n    if not vhost_path or not vhost_path.startswith(\"/files/\"):\n        return None\n\n    return _split_aug_path(vhost_path)[0]\n\n\ndef get_internal_aug_path(vhost_path: str) -> str:\n    \"\"\"Get the Augeas path for a vhost with the file path removed.\n\n    :param str vhost_path: Augeas virtual host path\n\n    :returns: Augeas path to vhost relative to the containing file\n    :rtype: str\n\n    \"\"\"\n    return _split_aug_path(vhost_path)[1]\n\n\ndef _split_aug_path(vhost_path: str) -> tuple[str, str]:\n    \"\"\"Splits an Augeas path into a file path and an internal path.\n\n    After removing \"/files\", this function splits vhost_path into the\n    file path and the remaining Augeas path.\n\n    :param str vhost_path: Augeas virtual host path\n\n    :returns: file path and internal Augeas path\n    :rtype: `tuple` of `str`\n\n    \"\"\"\n    # Strip off /files\n    file_path = vhost_path[6:]\n    internal_path: list[str] = []\n\n    # Remove components from the end of file_path until it becomes valid\n    while not os.path.exists(file_path):\n        file_path, _, internal_path_part = file_path.rpartition(\"/\")\n        internal_path.append(internal_path_part)\n\n    return file_path, \"/\".join(reversed(internal_path))\n\n\ndef parse_define_file(filepath: str, varname: str) -> dict[str, str]:\n    \"\"\" Parses Defines from a variable in configuration file\n\n    :param str filepath: Path of file to parse\n    :param str varname: Name of the variable\n\n    :returns: Dict of Define:Value pairs\n    :rtype: `dict`\n\n    \"\"\"\n    return_vars: dict[str, str] = {}\n    # Get list of words in the variable\n    a_opts = util.get_var_from_file(varname, filepath).split()\n    for i, v in enumerate(a_opts):\n        # Handle Define statements and make sure it has an argument\n        if v == \"-D\" and len(a_opts) >= i+2:\n            var_parts = a_opts[i+1].partition(\"=\")\n            return_vars[var_parts[0]] = var_parts[2]\n        elif len(v) > 2 and v.startswith(\"-D\"):\n            # Found var with no whitespace separator\n            var_parts = v[2:].partition(\"=\")\n            return_vars[var_parts[0]] = var_parts[2]\n    return return_vars\n\n\ndef unique_id() -> str:\n    \"\"\" Returns an unique id to be used as a VirtualHost identifier\"\"\"\n    return binascii.hexlify(os.urandom(16)).decode(\"utf-8\")\n\n\ndef included_in_paths(filepath: str, paths: Iterable[str]) -> bool:\n    \"\"\"\n    Returns true if the filepath is included in the list of paths\n    that may contain full paths or wildcard paths that need to be\n    expanded.\n\n    :param str filepath: Filepath to check\n    :param list paths: List of paths to check against\n\n    :returns: True if included\n    :rtype: bool\n    \"\"\"\n    return any(fnmatch.fnmatch(filepath, path) for path in paths)\n\n\ndef parse_defines(define_cmd: list[str]) -> dict[str, str]:\n    \"\"\"\n    Gets Defines from httpd process and returns a dictionary of\n    the defined variables.\n\n    :param list define_cmd: httpd command to dump defines\n\n    :returns: dictionary of defined variables\n    :rtype: dict\n    \"\"\"\n\n    variables: dict[str, str] = {}\n    matches = parse_from_subprocess(define_cmd, r\"Define: ([^ \\n]*)\")\n    try:\n        matches.remove(\"DUMP_RUN_CFG\")\n    except ValueError:\n        return {}\n\n    for match in matches:\n        # Value could also contain = so split only once\n        parts = match.split('=', 1)\n        value = parts[1] if len(parts) == 2 else ''\n        variables[parts[0]] = value\n\n    return variables\n\n\ndef parse_includes(inc_cmd: list[str]) -> list[str]:\n    \"\"\"\n    Gets Include directives from httpd process and returns a list of\n    their values.\n\n    :param list inc_cmd: httpd command to dump includes\n\n    :returns: list of found Include directive values\n    :rtype: list of str\n    \"\"\"\n\n    return parse_from_subprocess(inc_cmd, r\"\\(.*\\) (.*)\")\n\n\ndef parse_modules(mod_cmd: list[str]) -> list[str]:\n    \"\"\"\n    Get loaded modules from httpd process, and return the list\n    of loaded module names.\n\n    :param list mod_cmd: httpd command to dump loaded modules\n\n    :returns: list of found LoadModule module names\n    :rtype: list of str\n    \"\"\"\n\n    return parse_from_subprocess(mod_cmd, r\"(.*)_module\")\n\n\ndef parse_from_subprocess(command: list[str], regexp: str) -> list[str]:\n    \"\"\"Get values from stdout of subprocess command\n\n    :param list command: Command to run\n    :param str regexp: Regexp for parsing\n\n    :returns: list parsed from command output\n    :rtype: list\n\n    \"\"\"\n    stdout = _get_runtime_cfg(command)\n    return re.compile(regexp).findall(stdout)\n\n\ndef _get_runtime_cfg(command: list[str]) -> str:\n    \"\"\"\n    Get runtime configuration info.\n\n    :param command: Command to run\n\n    :returns: stdout from command\n\n    \"\"\"\n    try:\n        proc = subprocess.run(\n            command,\n            stdout=subprocess.PIPE,\n            stderr=subprocess.PIPE,\n            universal_newlines=True,\n            check=False,\n            env=util.env_no_snap_for_external_calls())\n        stdout, stderr = proc.stdout, proc.stderr\n\n    except (OSError, ValueError):\n        logger.error(\n            \"Error running command %s for runtime parameters!%s\",\n            command, os.linesep)\n        raise errors.MisconfigurationError(\n            \"Error accessing loaded Apache parameters: {0}\".format(\n                command))\n    # Small errors that do not impede\n    if proc.returncode != 0:\n        logger.warning(\"Error in checking parameter list: %s\", stderr)\n        raise errors.MisconfigurationError(\n            \"Apache is unable to check whether or not the module is \"\n            \"loaded because Apache is misconfigured.\")\n\n    return stdout\n\n\ndef find_ssl_apache_conf(prefix: str) -> str:\n    \"\"\"\n    Find a TLS Apache config file in the dedicated storage.\n    :param str prefix: prefix of the TLS Apache config file to find\n    :return: the path the TLS Apache config file\n    :rtype: str\n    \"\"\"\n    file_manager = ExitStack()\n    atexit.register(file_manager.close)\n    ref = (importlib.resources.files(\"certbot_apache\").joinpath(\"_internal\")\n           .joinpath(\"tls_configs\").joinpath(\"{0}-options-ssl-apache.conf\".format(prefix)))\n    return str(file_manager.enter_context(importlib.resources.as_file(ref)))\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/apacheparser.py",
    "content": "\"\"\" apacheconfig implementation of the ParserNode interfaces \"\"\"\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import interfaces\nfrom certbot_apache._internal import parsernode_util as util\nfrom certbot_apache._internal.interfaces import ParserNode\n\n\nclass ApacheParserNode(interfaces.ParserNode):\n    \"\"\" apacheconfig implementation of ParserNode interface.\n\n        Expects metadata `ac_ast` to be passed in, where `ac_ast` is the AST provided\n        by parsing the equivalent configuration text using the apacheconfig library.\n    \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        # pylint: disable=unused-variable\n        ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs)\n        super().__init__(**kwargs)\n        self.ancestor = ancestor\n        self.filepath = filepath\n        self.dirty = dirty\n        self.metadata = metadata\n        self._raw: Any = self.metadata[\"ac_ast\"]\n\n    def save(self, msg: str) -> None:\n        pass  # pragma: no cover\n\n    def find_ancestors(self, name: str) -> list[\"ApacheParserNode\"]:  # pylint: disable=unused-variable\n        \"\"\"Find ancestor BlockNodes with a given name\"\"\"\n        return [ApacheBlockNode(name=assertions.PASS,\n                                parameters=assertions.PASS,\n                                ancestor=self,\n                                filepath=assertions.PASS,\n                                metadata=self.metadata)]\n\n\nclass ApacheCommentNode(ApacheParserNode):\n    \"\"\" apacheconfig implementation of CommentNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        comment, kwargs = util.commentnode_kwargs(kwargs)\n        super().__init__(**kwargs)\n        self.comment = comment\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.comment == other.comment and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata and\n                    self.filepath == other.filepath)\n        return False  # pragma: no cover\n\n\nclass ApacheDirectiveNode(ApacheParserNode):\n    \"\"\" apacheconfig implementation of DirectiveNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)\n        super().__init__(**kwargs)\n        self.name = name\n        self.parameters = parameters\n        self.enabled = enabled\n        self.include: Optional[str] = None\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.name == other.name and\n                    self.filepath == other.filepath and\n                    self.parameters == other.parameters and\n                    self.enabled == other.enabled and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata)\n        return False  # pragma: no cover\n\n    def set_parameters(self, _parameters: Iterable[str]) -> None:\n        \"\"\"Sets the parameters for DirectiveNode\"\"\"\n        return  # pragma: no cover\n\n\nclass ApacheBlockNode(ApacheDirectiveNode):\n    \"\"\" apacheconfig implementation of BlockNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.children: tuple[ApacheParserNode, ...] = ()\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.name == other.name and\n                    self.filepath == other.filepath and\n                    self.parameters == other.parameters and\n                    self.children == other.children and\n                    self.enabled == other.enabled and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata)\n        return False  # pragma: no cover\n\n    # pylint: disable=unused-argument\n    def add_child_block(self, name: str, parameters: Optional[list[str]] = None,\n                        position: Optional[int] = None) -> \"ApacheBlockNode\":  # pragma: no cover\n        \"\"\"Adds a new BlockNode to the sequence of children\"\"\"\n        new_block = ApacheBlockNode(name=assertions.PASS,\n                                    parameters=assertions.PASS,\n                                    ancestor=self,\n                                    filepath=assertions.PASS,\n                                    metadata=self.metadata)\n        self.children += (new_block,)\n        return new_block\n\n    # pylint: disable=unused-argument\n    def add_child_directive(self, name: str, parameters: Optional[list[str]] = None,\n                            position: Optional[int] = None\n    ) -> ApacheDirectiveNode:  # pragma: no cover\n        \"\"\"Adds a new DirectiveNode to the sequence of children\"\"\"\n        new_dir = ApacheDirectiveNode(name=assertions.PASS,\n                                      parameters=assertions.PASS,\n                                      ancestor=self,\n                                      filepath=assertions.PASS,\n                                      metadata=self.metadata)\n        self.children += (new_dir,)\n        return new_dir\n\n    # pylint: disable=unused-argument\n    def add_child_comment(\n        self, name: str, parameters: Optional[int] = None, position: Optional[int] = None\n    ) -> ApacheCommentNode:  # pragma: no cover\n\n        \"\"\"Adds a new CommentNode to the sequence of children\"\"\"\n        new_comment = ApacheCommentNode(comment=assertions.PASS,\n                                        ancestor=self,\n                                        filepath=assertions.PASS,\n                                        metadata=self.metadata)\n        self.children += (new_comment,)\n        return new_comment\n\n    def find_blocks(self, name: str, exclude: bool = True) -> list[\"ApacheBlockNode\"]:  # pylint: disable=unused-argument\n        \"\"\"Recursive search of BlockNodes from the sequence of children\"\"\"\n        return [ApacheBlockNode(name=assertions.PASS,\n                                parameters=assertions.PASS,\n                                ancestor=self,\n                                filepath=assertions.PASS,\n                                metadata=self.metadata)]\n\n    def find_directives(self, name: str, exclude: bool = True) -> list[ApacheDirectiveNode]:  # pylint: disable=unused-argument\n        \"\"\"Recursive search of DirectiveNodes from the sequence of children\"\"\"\n        return [ApacheDirectiveNode(name=assertions.PASS,\n                                    parameters=assertions.PASS,\n                                    ancestor=self,\n                                    filepath=assertions.PASS,\n                                    metadata=self.metadata)]\n\n    # pylint: disable=unused-argument\n    def find_comments(self, comment: str, exact: bool = False) -> list[ApacheCommentNode]:\n        \"\"\"Recursive search of DirectiveNodes from the sequence of children\"\"\"\n        return [ApacheCommentNode(comment=assertions.PASS,  # pragma: no cover\n                                  ancestor=self,\n                                  filepath=assertions.PASS,\n                                  metadata=self.metadata)]\n\n    def delete_child(self, child: ParserNode) -> None:\n        \"\"\"Deletes a ParserNode from the sequence of children\"\"\"\n        return  # pragma: no cover\n\n    def unsaved_files(self) -> list[str]:\n        \"\"\"Returns a list of unsaved filepaths\"\"\"\n        return [assertions.PASS]  # pragma: no cover\n\n    def parsed_paths(self) -> list[str]:\n        \"\"\"Returns a list of parsed configuration file paths\"\"\"\n        return [assertions.PASS]\n\n\ninterfaces.CommentNode.register(ApacheCommentNode)\ninterfaces.DirectiveNode.register(ApacheDirectiveNode)\ninterfaces.BlockNode.register(ApacheBlockNode)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/assertions.py",
    "content": "\"\"\"Dual parser node assertions\"\"\"\nimport fnmatch\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot_apache._internal import interfaces\nfrom certbot_apache._internal.interfaces import CommentNode\nfrom certbot_apache._internal.interfaces import DirectiveNode\nfrom certbot_apache._internal.interfaces import ParserNode\nfrom certbot_apache._internal.obj import VirtualHost\n\nPASS = \"CERTBOT_PASS_ASSERT\"\n\n\ndef assertEqual(first: ParserNode, second: ParserNode) -> None:\n    \"\"\" Equality assertion \"\"\"\n\n    if isinstance(first, interfaces.CommentNode):\n        assertEqualComment(first, second)\n    elif isinstance(first, interfaces.DirectiveNode):\n        assertEqualDirective(first, second)\n\n    # Do an extra interface implementation assertion, as the contents were\n    # already checked for BlockNode in the assertEqualDirective\n    if isinstance(first, interfaces.BlockNode):\n        assert isinstance(second, interfaces.BlockNode)\n\n    # Skip tests if filepath includes the pass value. This is done\n    # because filepath is variable of the base ParserNode interface, and\n    # unless the implementation is actually done, we cannot assume getting\n    # correct results from boolean assertion for dirty\n    if not isPass(first.filepath) and not isPass(second.filepath):\n        assert first.dirty == second.dirty\n        # We might want to disable this later if testing with two separate\n        # (but identical) directory structures.\n        assert first.filepath == second.filepath\n\n\ndef assertEqualComment(first: ParserNode, second: ParserNode) -> None:  # pragma: no cover\n    \"\"\" Equality assertion for CommentNode \"\"\"\n\n    assert isinstance(first, interfaces.CommentNode)\n    assert isinstance(second, interfaces.CommentNode)\n\n    if not isPass(first.comment) and not isPass(second.comment):\n        assert first.comment == second.comment\n\n\ndef _assertEqualDirectiveComponents(first: ParserNode,  # pragma: no cover\n                                    second: ParserNode) -> None:\n    \"\"\" Handles assertion for instance variables for DirectiveNode and BlockNode\"\"\"\n\n    # Enabled value cannot be asserted, because Augeas implementation\n    # is unable to figure that out.\n    # assert first.enabled == second.enabled\n    assert isinstance(first, DirectiveNode)\n    assert isinstance(second, DirectiveNode)\n\n    if not isPass(first.name) and not isPass(second.name):\n        assert first.name == second.name\n\n    if not isPass(first.parameters) and not isPass(second.parameters):\n        assert first.parameters == second.parameters\n\n\ndef assertEqualDirective(first: ParserNode, second: ParserNode) -> None:\n    \"\"\" Equality assertion for DirectiveNode \"\"\"\n\n    assert isinstance(first, interfaces.DirectiveNode)\n    assert isinstance(second, interfaces.DirectiveNode)\n    _assertEqualDirectiveComponents(first, second)\n\n\ndef isPass(value: Any) -> bool:  # pragma: no cover\n    \"\"\"Checks if the value is set to PASS\"\"\"\n    if isinstance(value, bool):\n        return True\n    return PASS in value\n\n\ndef isPassDirective(block: DirectiveNode) -> bool:\n    \"\"\" Checks if BlockNode or DirectiveNode should pass the assertion \"\"\"\n\n    if isPass(block.name):\n        return True\n    if isPass(block.parameters):  # pragma: no cover\n        return True\n    if isPass(block.filepath):  # pragma: no cover\n        return True\n    return False\n\n\ndef isPassComment(comment: CommentNode) -> bool:\n    \"\"\" Checks if CommentNode should pass the assertion \"\"\"\n\n    if isPass(comment.comment):\n        return True\n    if isPass(comment.filepath):  # pragma: no cover\n        return True\n    return False\n\n\ndef isPassNodeList(nodelist: list[Union[DirectiveNode, CommentNode]]) -> bool:  # pragma: no cover\n    \"\"\" Checks if a ParserNode in the nodelist should pass the assertion,\n    this function is used for results of find_* methods. Unimplemented find_*\n    methods should return a sequence containing a single ParserNode instance\n    with assertion pass string.\"\"\"\n\n    node: Optional[Union[DirectiveNode, CommentNode]]\n    try:\n        node = nodelist[0]\n    except IndexError:\n        node = None\n\n    if not node:  # pragma: no cover\n        return False\n\n    if isinstance(node, interfaces.DirectiveNode):\n        return isPassDirective(node)\n    return isPassComment(node)\n\n\ndef assertEqualSimple(first: Any, second: Any) -> None:\n    \"\"\" Simple assertion \"\"\"\n    if not isPass(first) and not isPass(second):\n        assert first == second\n\n\ndef isEqualVirtualHost(first: VirtualHost, second: VirtualHost) -> bool:\n    \"\"\"\n    Checks that two VirtualHost objects are similar. There are some built\n    in differences with the implementations: VirtualHost created by ParserNode\n    implementation doesn't have \"path\" defined, as it was used for Augeas path\n    and that cannot obviously be used in the future. Similarly the legacy\n    version lacks \"node\" variable, that has a reference to the BlockNode for the\n    VirtualHost.\n    \"\"\"\n    return (\n        first.name == second.name and\n        first.aliases == second.aliases and\n        first.filep == second.filep and\n        first.addrs == second.addrs and\n        first.ssl == second.ssl and\n        first.enabled == second.enabled and\n        first.modmacro == second.modmacro and\n        first.ancestor == second.ancestor\n    )\n\n\ndef assertEqualPathsList(first: Iterable[str], second: Iterable[str]) -> None:  # pragma: no cover\n    \"\"\"\n    Checks that the two lists of file paths match. This assertion allows for wildcard\n    paths.\n    \"\"\"\n    if any(isPass(path) for path in first):\n        return\n    if any(isPass(path) for path in second):\n        return\n    for fpath in first:\n        assert any(fnmatch.fnmatch(fpath, spath) for spath in second)\n    for spath in second:\n        assert any(fnmatch.fnmatch(fpath, spath) for fpath in first)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/augeas_lens/README",
    "content": "Certbot includes the very latest Augeas lenses in order to ship bug fixes \nto Apache configuration handling bugs as quickly as possible\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/augeas_lens/httpd.aug",
    "content": "(* Apache HTTPD lens for Augeas\n\nAuthors:\n  David Lutterkort <lutter@redhat.com>\n  Francis Giraldeau <francis.giraldeau@usherbrooke.ca>\n  Raphael Pinson <raphink@gmail.com>\n\nAbout: Reference\n  Online Apache configuration manual: https://httpd.apache.org/docs/trunk/\n\nAbout: License\n    This file is licensed under the LGPL v2+.\n\nAbout: Lens Usage\n  Sample usage of this lens in augtool\n\n  Apache configuration is represented by two main structures, nested sections\n  and directives. Sections are used as labels, while directives are kept as a\n  value. Sections and directives can have positional arguments inside values\n  of \"arg\" nodes. Arguments of sections must be the firsts child of the\n  section node.\n\n  This lens doesn't support automatic string quoting. Hence, the string must\n  be quoted when containing a space.\n\n  Create a new VirtualHost section with one directive:\n  > clear /files/etc/apache2/sites-available/foo/VirtualHost\n  > set /files/etc/apache2/sites-available/foo/VirtualHost/arg \"172.16.0.1:80\"\n  > set /files/etc/apache2/sites-available/foo/VirtualHost/directive \"ServerAdmin\"\n  > set /files/etc/apache2/sites-available/foo/VirtualHost/*[self::directive=\"ServerAdmin\"]/arg \"admin@example.com\"\n\nAbout: Configuration files\n  This lens applies to files in /etc/httpd and /etc/apache2. See <filter>.\n\n*)\n\n\nmodule Httpd =\n\nautoload xfm\n\n(******************************************************************\n *                           Utilities lens\n *****************************************************************)\nlet dels (s:string)     = del s s\n\n(* The continuation sequence that indicates that we should consider the\n * next line part of the current line *)\nlet cont = /\\\\\\\\\\r?\\n/\n\n(* Whitespace within a line: space, tab, and the continuation sequence *)\nlet ws = /[ \\t]/ | cont\n\n(* Any possible character - '.' does not match \\n *)\nlet any = /(.|\\n)/\n\n(* Any character preceded by a backslash *)\nlet esc_any = /\\\\\\\\(.|\\n)/\n\n(* Newline sequence - both for Unix and DOS newlines *)\nlet nl = /\\r?\\n/\n\n(* Whitespace at the end of a line *)\nlet eol = del (ws* . nl) \"\\n\"\n\n(* deal with continuation lines *)\nlet sep_spc             = del ws+ \" \"\nlet sep_osp             = del ws* \"\"\nlet sep_eq              = del (ws* . \"=\" . ws*) \"=\"\n\nlet nmtoken             = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/\nlet word                = /[a-z][a-z0-9._-]*/i\n\n(* A complete line that is either just whitespace or a comment that only\n * contains whitespace *)\nlet empty = [ del (ws* . /#?/ . ws* . nl) \"\\n\" ]\n\nlet indent              = Util.indent\n\n(* A comment that is not just whitespace. We define it in terms of the\n * things that are not allowed as part of such a comment:\n *   1) Starts with whitespace\n *   2) Ends with whitespace, a backslash or \\r\n *   3) Unescaped newlines\n *)\nlet comment =\n  let comment_start = del (ws* . \"#\" . ws* ) \"# \" in\n  let unesc_eol = /[^\\]?/ . nl in\n  let w = /[^\\t\\n\\r \\\\]/ in\n  let r = /[\\r\\\\]/ in\n  let s = /[\\t\\r ]/ in\n  (*\n   * we'd like to write\n   * let b = /\\\\\\\\/ in\n   * let t = /[\\t\\n\\r ]/ in\n   * let x = b . (t? . (s|w)* ) in\n   * but the definition of b depends on commit 244c0edd in 1.9.0 and\n   * would make the lens unusable with versions before 1.9.0. So we write\n   * x out which works in older versions, too\n   *)\n  let x = /\\\\\\\\[\\t\\n\\r ]?[^\\n\\\\]*/ in\n  let line = ((r . s* . w|w|r) . (s|w)* . x*|(r.s* )?).w.(s*.w)* in\n  [ label \"#comment\" . comment_start . store line . eol ]\n\n(* borrowed from shellvars.aug *)\nlet char_arg_sec  = /([^\\\\ '\"\\t\\r\\n>]|[^ '\"\\t\\r\\n>]+[^\\\\ \\t\\r\\n>])|\\\\\\\\\"|\\\\\\\\'|\\\\\\\\ /\nlet char_arg_wl   = /([^\\\\ '\"},\\t\\r\\n]|[^ '\"},\\t\\r\\n]+[^\\\\ '\"},\\t\\r\\n])/\n\nlet dquot =\n     let no_dquot = /[^\"\\\\\\r\\n]/\n  in /\"/ . (no_dquot|esc_any)* . /\"/\nlet dquot_msg =\n     let no_dquot = /([^ \\t\"\\\\\\r\\n]|[^\"\\\\\\r\\n]+[^ \\t\"\\\\\\r\\n])/\n  in /\"/ . (no_dquot|esc_any)* . no_dquot\n\nlet squot =\n     let no_squot = /[^'\\\\\\r\\n]/\n  in /'/ . (no_squot|esc_any)* . /'/\nlet comp = /[<>=]?=/\n\n(******************************************************************\n *                            Attributes\n *****************************************************************)\n\n(* The arguments for a directive come in two flavors: quoted with single or\n * double quotes, or bare. Bare arguments may not start with a single or\n * double quote; since we also treat \"word lists\" special, i.e. lists\n * enclosed in curly braces, bare arguments may not start with those,\n * either.\n *\n * Bare arguments may not contain unescaped spaces, but we allow escaping\n * with '\\\\'. Quoted arguments can contain anything, though the quote must\n * be escaped with '\\\\'.\n *)\nlet bare = /([^{\"' \\t\\n\\r]|\\\\\\\\.)([^ \\t\\n\\r]|\\\\\\\\.)*[^ \\t\\n\\r\\\\]|[^{\"' \\t\\n\\r\\\\]/\n\nlet arg_quoted = [ label \"arg\" . store (dquot|squot) ]\nlet arg_bare = [ label \"arg\" . store bare ]\n\n(* message argument starts with \" but ends at EOL *)\nlet arg_dir_msg = [ label \"arg\" . store dquot_msg ]\nlet arg_wl  = [ label \"arg\" . store (char_arg_wl+|dquot|squot) ]\n\n(* comma-separated wordlist as permitted in the SSLRequire directive *)\nlet arg_wordlist =\n     let wl_start = dels \"{\" in\n     let wl_end   = dels \"}\" in\n     let wl_sep   = del /[ \\t]*,[ \\t]*/ \", \"\n  in [ label \"wordlist\" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]\n\nlet argv (l:lens) = l . (sep_spc . l)*\n\n(* the arguments of a directive. We use this once we have parsed the name\n * of the directive, and the space right after it. When dir_args is used,\n * we also know that we have at least one argument. We need to be careful\n * with the spacing between arguments: quoted arguments and word lists do\n * not need to have space between them, but bare arguments do.\n *\n * Apache apparently is also happy if the last argument starts with a double\n * quote, but has no corresponding closing double quote, which is what\n * arg_dir_msg handles\n *)\nlet dir_args =\n  let arg_nospc = arg_quoted|arg_wordlist in\n  (arg_bare . sep_spc | arg_nospc . sep_osp)* . (arg_bare|arg_nospc|arg_dir_msg)\n\nlet directive =\n  [ indent . label \"directive\" . store word .  (sep_spc . dir_args)? . eol ]\n\nlet arg_sec = [ label \"arg\" . store (char_arg_sec+|comp|dquot|squot) ]\n\nlet section (body:lens) =\n    (* opt_eol includes empty lines *)\n    let opt_eol = del /([ \\t]*#?[ \\t]*\\r?\\n)*/ \"\\n\" in\n    let inner = (sep_spc . argv arg_sec)? . sep_osp .\n             dels \">\" . opt_eol . ((body|comment) . (body|empty|comment)*)? .\n             indent . dels \"</\" in\n    let kword = key (word - /perl/i) in\n    let dword = del (word - /perl/i) \"a\" in\n        [ indent . dels \"<\" . square kword inner dword . del />[ \\t\\n\\r]*/ \">\\n\" ]\n\nlet perl_section = [ indent . label \"Perl\" . del /<perl>/i \"<Perl>\"\n                   . store /[^<]*/\n                   . del /<\\/perl>/i \"</Perl>\" . eol ]\n\n\nlet rec content = section (content|directive)\n                | perl_section\n\nlet lns = (content|directive|comment|empty)*\n\nlet filter = (incl \"/etc/apache2/apache2.conf\") .\n             (incl \"/etc/apache2/httpd.conf\") .\n             (incl \"/etc/apache2/ports.conf\") .\n             (incl \"/etc/apache2/conf.d/*\") .\n             (incl \"/etc/apache2/conf-available/*.conf\") .\n             (incl \"/etc/apache2/mods-available/*\") .\n             (incl \"/etc/apache2/sites-available/*\") .\n             (incl \"/etc/apache2/vhosts.d/*.conf\") .\n             (incl \"/etc/httpd/conf.d/*.conf\") .\n             (incl \"/etc/httpd/httpd.conf\") .\n             (incl \"/etc/httpd/conf/httpd.conf\") .\n             (incl \"/etc/httpd/conf.modules.d/*.conf\") .\n             Util.stdexcl\n\nlet xfm = transform lns filter\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/augeasparser.py",
    "content": "\"\"\"\nAugeas implementation of the ParserNode interfaces.\n\nAugeas works internally by using XPATH notation. The following is a short example\nof how this all works internally, to better understand what's going on under the\nhood.\n\nA configuration file /etc/apache2/apache2.conf with the following content:\n\n    # First comment line\n    # Second comment line\n    WhateverDirective whatevervalue\n    <ABlock>\n        DirectiveInABlock dirvalue\n    </ABlock>\n    SomeDirective somedirectivevalue\n    <ABlock>\n        AnotherDirectiveInABlock dirvalue\n    </ABlock>\n    # Yet another comment\n\n\nTranslates over to Augeas path notation (of immediate children), when calling\nfor example: aug.match(\"/files/etc/apache2/apache2.conf/*\")\n\n[\n    \"/files/etc/apache2/apache2.conf/#comment[1]\",\n    \"/files/etc/apache2/apache2.conf/#comment[2]\",\n    \"/files/etc/apache2/apache2.conf/directive[1]\",\n    \"/files/etc/apache2/apache2.conf/ABlock[1]\",\n    \"/files/etc/apache2/apache2.conf/directive[2]\",\n    \"/files/etc/apache2/apache2.conf/ABlock[2]\",\n    \"/files/etc/apache2/apache2.conf/#comment[3]\"\n]\n\nRegardless of directives name, its key in the Augeas tree is always \"directive\",\nwith index where needed of course. Comments work similarly, while blocks\nhave their own key in the Augeas XPATH notation.\n\nIt's important to note that all of the unique keys have their own indices.\n\nAugeas paths are case sensitive, while Apache configuration is case insensitive.\nIt looks like this:\n\n    <block>\n        directive value\n    </block>\n    <Block>\n        Directive Value\n    </Block>\n    <block>\n        directive value\n    </block>\n    <bLoCk>\n        DiReCtiVe VaLuE\n    </bLoCk>\n\nTranslates over to:\n\n[\n    \"/files/etc/apache2/apache2.conf/block[1]\",\n    \"/files/etc/apache2/apache2.conf/Block[1]\",\n    \"/files/etc/apache2/apache2.conf/block[2]\",\n    \"/files/etc/apache2/apache2.conf/bLoCk[1]\",\n]\n\"\"\"\nfrom typing import Any\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import interfaces\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal import parsernode_util as util\n\n\nclass AugeasParserNode(interfaces.ParserNode):\n    \"\"\" Augeas implementation of ParserNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        # pylint: disable=unused-variable\n        ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs)\n        super().__init__(**kwargs)\n        self.ancestor = ancestor\n        self.filepath = filepath\n        self.dirty = dirty\n        self.metadata = metadata\n        self.parser = cast(parser.ApacheParser,\n                                                self.metadata.get(\"augeasparser\"))\n        try:\n            if self.metadata[\"augeaspath\"].endswith(\"/\"):\n                raise errors.PluginError(\n                    \"Augeas path: {} has a trailing slash\".format(\n                        self.metadata[\"augeaspath\"]\n                    )\n                )\n        except KeyError:\n            raise errors.PluginError(\"Augeas path is required\")\n\n    def save(self, msg: Iterable[str]) -> None:\n        self.parser.save(msg)\n\n    def find_ancestors(self, name: str) -> list[\"AugeasParserNode\"]:\n        \"\"\"\n        Searches for ancestor BlockNodes with a given name.\n\n        :param str name: Name of the BlockNode parent to search for\n\n        :returns: List of matching ancestor nodes.\n        :rtype: list of AugeasParserNode\n        \"\"\"\n\n        ancestors: list[\"AugeasParserNode\"] = []\n\n        parent = self.metadata[\"augeaspath\"]\n        while True:\n            # Get the path of ancestor node\n            parent = parent.rpartition(\"/\")[0]\n            # Root of the tree\n            if not parent or parent == \"/files\":\n                break\n            anc = self._create_blocknode(parent)\n            if anc.name is not None and anc.name.lower() == name.lower():\n                ancestors.append(anc)\n\n        return ancestors\n\n    def _create_blocknode(self, path: str) -> \"AugeasBlockNode\":\n        \"\"\"\n        Helper function to create a BlockNode from Augeas path. This is used by\n        AugeasParserNode.find_ancestors and AugeasBlockNode.\n        and AugeasBlockNode.find_blocks\n\n        \"\"\"\n\n        name: str = self._aug_get_name(path)\n        metadata: dict[str, Union[parser.ApacheParser, str]] = {\n            \"augeasparser\": self.parser, \"augeaspath\": path\n        }\n\n        # Check if the file was included from the root config or initial state\n        file_path = apache_util.get_file_path(path)\n        if file_path is None:\n            raise ValueError(f\"No file path found for vhost: {path}.\")  # pragma: no cover\n\n        enabled = self.parser.parsed_in_original(file_path)\n\n        return AugeasBlockNode(name=name,\n                               enabled=enabled,\n                               ancestor=assertions.PASS,\n                               filepath=file_path,\n                               metadata=metadata)\n\n    def _aug_get_name(self, path: str) -> str:\n        \"\"\"\n        Helper function to get name of a configuration block or variable from path.\n        \"\"\"\n\n        # Remove the ending slash if any\n        if path[-1] == \"/\":  # pragma: no cover\n            path = path[:-1]\n\n        # Get the block name\n        name = path.split(\"/\")[-1]\n\n        # remove [...], it's not allowed in Apache configuration and is used\n        # for indexing within Augeas\n        return name.split(\"[\")[0]\n\n\nclass AugeasCommentNode(AugeasParserNode):\n    \"\"\" Augeas implementation of CommentNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        comment, kwargs = util.commentnode_kwargs(kwargs)  # pylint: disable=unused-variable\n        super().__init__(**kwargs)\n        self.comment = comment\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.comment == other.comment and\n                    self.filepath == other.filepath and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata)\n        return False\n\n\nclass AugeasDirectiveNode(AugeasParserNode):\n    \"\"\" Augeas implementation of DirectiveNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)\n        super().__init__(**kwargs)\n        self.name = name\n        self.enabled = enabled\n        if parameters:\n            self.set_parameters(parameters)\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.name == other.name and\n                    self.filepath == other.filepath and\n                    self.parameters == other.parameters and\n                    self.enabled == other.enabled and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata)\n        return False\n\n    def set_parameters(self, parameters: Iterable[str]) -> None:\n        \"\"\"\n        Sets parameters of a DirectiveNode or BlockNode object.\n\n        :param list parameters: List of all parameters for the node to set.\n        \"\"\"\n        orig_params = self._aug_get_params(self.metadata[\"augeaspath\"])\n\n        # Clear out old parameters\n        for _ in orig_params:\n            # When the first parameter is removed, the indices get updated\n            param_path = \"{}/arg[1]\".format(self.metadata[\"augeaspath\"])\n            self.parser.aug.remove(param_path)\n        # Insert new ones\n        for pi, param in enumerate(parameters):\n            param_path = \"{}/arg[{}]\".format(self.metadata[\"augeaspath\"], pi+1)\n            self.parser.aug.set(param_path, param)\n\n    @property\n    def parameters(self) -> tuple[str, ...]:\n        \"\"\"\n        Fetches the parameters from Augeas tree, ensuring that the sequence always\n        represents the current state\n\n        :returns: Tuple of parameters for this DirectiveNode\n        :rtype: tuple:\n        \"\"\"\n        return tuple(self._aug_get_params(self.metadata[\"augeaspath\"]))\n\n    def _aug_get_params(self, path: str) -> list[str]:\n        \"\"\"Helper function to get parameters for DirectiveNodes and BlockNodes\"\"\"\n\n        arg_paths = self.parser.aug.match(path + \"/arg\")\n        args = [self.parser.get_arg(apath) for apath in arg_paths]\n        return [arg for arg in args if arg is not None]\n\n\nclass AugeasBlockNode(AugeasDirectiveNode):\n    \"\"\" Augeas implementation of BlockNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        super().__init__(**kwargs)\n        self.children: tuple[\"AugeasBlockNode\", ...] = ()\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.name == other.name and\n                    self.filepath == other.filepath and\n                    self.parameters == other.parameters and\n                    self.children == other.children and\n                    self.enabled == other.enabled and\n                    self.dirty == other.dirty and\n                    self.ancestor == other.ancestor and\n                    self.metadata == other.metadata)\n        return False\n\n    # pylint: disable=unused-argument\n    def add_child_block(self, name: str,  # pragma: no cover\n                        parameters: Optional[list[str]] = None,\n                        position: Optional[int] = None) -> \"AugeasBlockNode\":\n        \"\"\"Adds a new BlockNode to the sequence of children\"\"\"\n\n        insertpath, realpath, before = self._aug_resolve_child_position(\n            name,\n            position\n        )\n        new_metadata: dict[str, Any] = {\"augeasparser\": self.parser, \"augeaspath\": realpath}\n\n        # Create the new block\n        self.parser.aug.insert(insertpath, name, before)\n        # Check if the file was included from the root config or initial state\n        file_path = apache_util.get_file_path(realpath)\n        if file_path is None:\n            raise errors.Error(f\"No file path found for vhost: {realpath}\")  # pragma: no cover\n        enabled = self.parser.parsed_in_original(file_path)\n\n        # Parameters will be set at the initialization of the new object\n        return AugeasBlockNode(\n            name=name,\n            parameters=parameters,\n            enabled=enabled,\n            ancestor=assertions.PASS,\n            filepath=file_path,\n            metadata=new_metadata,\n        )\n\n    # pylint: disable=unused-argument\n    def add_child_directive(self, name: str,  # pragma: no cover\n                            parameters: Optional[list[str]] = None,\n                            position: Optional[int] = None) -> AugeasDirectiveNode:\n        \"\"\"Adds a new DirectiveNode to the sequence of children\"\"\"\n\n        if not parameters:\n            raise errors.PluginError(\"Directive requires parameters and none were set.\")\n\n        insertpath, realpath, before = self._aug_resolve_child_position(\n            \"directive\",\n            position\n        )\n        new_metadata = {\"augeasparser\": self.parser, \"augeaspath\": realpath}\n\n        # Create the new directive\n        self.parser.aug.insert(insertpath, \"directive\", before)\n        # Set the directive key\n        self.parser.aug.set(realpath, name)\n        # Check if the file was included from the root config or initial state\n        file_path = apache_util.get_file_path(realpath)\n        if file_path is None:\n            raise errors.Error(f\"No file path found for vhost: {realpath}\")  # pragma: no cover\n        enabled = self.parser.parsed_in_original(file_path)\n\n        return AugeasDirectiveNode(\n            name=name,\n            parameters=parameters,\n            enabled=enabled,\n            ancestor=assertions.PASS,\n            filepath=file_path,\n            metadata=new_metadata,\n        )\n\n    def add_child_comment(\n        self, comment: str = \"\", position: Optional[int] = None\n    ) -> \"AugeasCommentNode\":\n        \"\"\"Adds a new CommentNode to the sequence of children\"\"\"\n\n        insertpath, realpath, before = self._aug_resolve_child_position(\n            \"#comment\",\n            position\n        )\n        new_metadata: dict[str, Any] = {\n            \"augeasparser\": self.parser, \"augeaspath\": realpath,\n        }\n\n        # Create the new comment\n        self.parser.aug.insert(insertpath, \"#comment\", before)\n        # Set the comment content\n        self.parser.aug.set(realpath, comment)\n\n        return AugeasCommentNode(\n            comment=comment,\n            ancestor=assertions.PASS,\n            filepath=apache_util.get_file_path(realpath),\n            metadata=new_metadata,\n        )\n\n    def find_blocks(self, name: str, exclude: bool = True) -> list[\"AugeasBlockNode\"]:\n        \"\"\"Recursive search of BlockNodes from the sequence of children\"\"\"\n\n        nodes: list[\"AugeasBlockNode\"] = []\n        paths: Iterable[str] = self._aug_find_blocks(name)\n        if exclude:\n            paths = self.parser.exclude_dirs(paths)\n        for path in paths:\n            nodes.append(self._create_blocknode(path))\n\n        return nodes\n\n    def find_directives(self, name: str, exclude: bool = True) -> list[\"AugeasDirectiveNode\"]:\n        \"\"\"Recursive search of DirectiveNodes from the sequence of children\"\"\"\n\n        nodes = []\n        ownpath = self.metadata.get(\"augeaspath\")\n\n        directives = self.parser.find_dir(name, start=ownpath, exclude=exclude)\n        already_parsed: set[str] = set()\n        for directive in directives:\n            # Remove the /arg part from the Augeas path\n            directive = directive.partition(\"/arg\")[0]\n            # find_dir returns an object for each _parameter_ of a directive\n            # so we need to filter out duplicates.\n            if directive not in already_parsed:\n                nodes.append(self._create_directivenode(directive))\n                already_parsed.add(directive)\n\n        return nodes\n\n    def find_comments(self, comment: str) -> list[\"AugeasCommentNode\"]:\n        \"\"\"\n        Recursive search of DirectiveNodes from the sequence of children.\n\n        :param str comment: Comment content to search for.\n        \"\"\"\n\n        nodes: list[\"AugeasCommentNode\"] = []\n        ownpath = self.metadata.get(\"augeaspath\")\n\n        comments = self.parser.find_comments(comment, start=ownpath)\n        for com in comments:\n            nodes.append(self._create_commentnode(com))\n\n        return nodes\n\n    def delete_child(self, child: \"AugeasParserNode\") -> None:\n        \"\"\"\n        Deletes a ParserNode from the sequence of children, and raises an\n        exception if it's unable to do so.\n        :param AugeasParserNode child: A node to delete.\n        \"\"\"\n        if not self.parser.aug.remove(child.metadata[\"augeaspath\"]):\n\n            raise errors.PluginError(\n                (\"Could not delete child node, the Augeas path: {} doesn't \" +\n                 \"seem to exist.\").format(child.metadata[\"augeaspath\"])\n            )\n\n    def unsaved_files(self) -> set[str]:\n        \"\"\"Returns a list of unsaved filepaths\"\"\"\n        return self.parser.unsaved_files()\n\n    def parsed_paths(self) -> list[str]:\n        \"\"\"\n        Returns a list of file paths that have currently been parsed into the parser\n        tree. The returned list may include paths with wildcard characters, for\n        example: ['/etc/apache2/conf.d/*.load']\n\n        This is typically called on the root node of the ParserNode tree.\n\n        :returns: list of file paths of files that have been parsed\n        \"\"\"\n\n        res_paths: list[str] = []\n\n        paths = self.parser.existing_paths\n        for directory in paths:\n            for filename in paths[directory]:\n                res_paths.append(os.path.join(directory, filename))\n\n        return res_paths\n\n    def _create_commentnode(self, path: str) -> \"AugeasCommentNode\":\n        \"\"\"Helper function to create a CommentNode from Augeas path\"\"\"\n\n        comment = self.parser.aug.get(path)\n        metadata = {\"augeasparser\": self.parser, \"augeaspath\": path}\n\n        # Because of the dynamic nature of AugeasParser and the fact that we're\n        # not populating the complete node tree, the ancestor has a dummy value\n        return AugeasCommentNode(comment=comment,\n                                 ancestor=assertions.PASS,\n                                 filepath=apache_util.get_file_path(path),\n                                 metadata=metadata)\n\n    def _create_directivenode(self, path: str) -> \"AugeasDirectiveNode\":\n        \"\"\"Helper function to create a DirectiveNode from Augeas path\"\"\"\n\n        name = self.parser.get_arg(path)\n        metadata: dict[str, Union[parser.ApacheParser, str]] = {\n            \"augeasparser\": self.parser, \"augeaspath\": path,\n        }\n\n        # Check if the file was included from the root config or initial state\n        enabled: bool = self.parser.parsed_in_original(\n            apache_util.get_file_path(path)\n        )\n        return AugeasDirectiveNode(name=name,\n                                   ancestor=assertions.PASS,\n                                   enabled=enabled,\n                                   filepath=apache_util.get_file_path(path),\n                                   metadata=metadata)\n\n    def _aug_find_blocks(self, name: str) -> set[str]:\n        \"\"\"Helper function to perform a search to Augeas DOM tree to search\n        configuration blocks with a given name\"\"\"\n\n        # The code here is modified from configurator.get_virtual_hosts()\n        blk_paths: set[str] = set()\n        for vhost_path in list(self.parser.parser_paths):\n            paths = self.parser.aug.match(\n                (\"/files%s//*[label()=~regexp('%s')]\" %\n                    (vhost_path, parser.case_i(name))))\n            blk_paths.update([path for path in paths if\n                              name.lower() in os.path.basename(path).lower()])\n        return blk_paths\n\n    def _aug_resolve_child_position(\n        self, name: str, position: Optional[int]) -> tuple[str, str, bool]:\n        \"\"\"\n        Helper function that iterates through the immediate children and figures\n        out the insertion path for a new AugeasParserNode.\n\n        Augeas also generalizes indices for directives and comments, simply by\n        using \"directive\" or \"comment\" respectively as their names.\n\n        This function iterates over the existing children of the AugeasBlockNode,\n        returning their insertion path, resulting Augeas path and if the new node\n        should be inserted before or after the returned insertion path.\n\n        Note: while Apache is case insensitive, Augeas is not, and blocks like\n        Nameofablock and NameOfABlock have different indices.\n\n        :param str name: Name of the AugeasBlockNode to insert, \"directive\" for\n            AugeasDirectiveNode or \"comment\" for AugeasCommentNode\n        :param int position: The position to insert the child AugeasParserNode to\n\n        :returns: Tuple of insert path, resulting path and a boolean if the new\n            node should be inserted before it.\n        :rtype: tuple of str, str, bool\n        \"\"\"\n\n        # Default to appending\n        before: bool = False\n\n        all_children: str = self.parser.aug.match(\"{}/*\".format(\n            self.metadata[\"augeaspath\"])\n        )\n\n        # Calculate resulting_path\n        # Augeas indices start at 1. We use counter to calculate the index to\n        # be used in resulting_path.\n        counter: int = 1\n        for i, child in enumerate(all_children):\n            if position is not None and i >= position:\n                # We're not going to insert the new node to an index after this\n                break\n            childname = self._aug_get_name(child)\n            if name == childname:\n                counter += 1\n\n        resulting_path: str = \"{}/{}[{}]\".format(\n            self.metadata[\"augeaspath\"],\n            name,\n            counter\n        )\n\n        # Form the correct insert_path\n        # Inserting the only child and appending as the last child work\n        # similarly in Augeas.\n        append = not all_children or position is None or position >= len(all_children)\n        if append:\n            insert_path = \"{}/*[last()]\".format(\n                self.metadata[\"augeaspath\"]\n            )\n        elif position == 0:\n            # Insert as the first child, before the current first one.\n            insert_path = all_children[0]\n            before = True\n        else:\n            insert_path = \"{}/*[{}]\".format(\n                self.metadata[\"augeaspath\"],\n                position\n            )\n\n        return insert_path, resulting_path, before\n\n\ninterfaces.CommentNode.register(AugeasCommentNode)\ninterfaces.DirectiveNode.register(AugeasDirectiveNode)\ninterfaces.BlockNode.register(AugeasBlockNode)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/configurator.py",
    "content": "\"\"\"Apache Configurator.\"\"\"\n# pylint: disable=too-many-lines\nfrom collections import defaultdict\nimport copy\nimport fnmatch\nimport logging\nimport re\nimport socket\nimport time\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot.interfaces import RenewableCert\nfrom certbot.plugins import common\nfrom certbot.plugins.enhancements import AutoHSTSEnhancement\nfrom certbot.plugins.util import path_surgery\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import constants\nfrom certbot_apache._internal import display_ops\nfrom certbot_apache._internal import dualparser\nfrom certbot_apache._internal import http_01\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal.apacheparser import ApacheBlockNode\n\ntry:\n    import apacheconfig\n    HAS_APACHECONFIG = True\nexcept ImportError:  # pragma: no cover\n    HAS_APACHECONFIG = False\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass OsOptions:\n    \"\"\"\n    Dedicated class to describe the OS specificities (eg. paths, binary names)\n    that the Apache configurator needs to be aware to operate properly.\n    \"\"\"\n    def __init__(self,\n                 server_root: str = \"/etc/apache2\",\n                 vhost_root: str = \"/etc/apache2/sites-available\",\n                 vhost_files: str = \"*\",\n                 logs_root: str = \"/var/log/apache2\",\n                 ctl: str = \"apache2ctl\",\n                 version_cmd: Optional[list[str]] = None,\n                 restart_cmd: Optional[list[str]] = None,\n                 restart_cmd_alt: Optional[list[str]] = None,\n                 conftest_cmd: Optional[list[str]] = None,\n                 enmod: Optional[str] = None,\n                 dismod: Optional[str] = None,\n                 le_vhost_ext: str = \"-le-ssl.conf\",\n                 handle_modules: bool = False,\n                 handle_sites: bool = False,\n                 challenge_location: str = \"/etc/apache2\",\n                 apache_bin: Optional[str] = None,\n                 ) -> None:\n        self.server_root = server_root\n        self.vhost_root = vhost_root\n        self.vhost_files = vhost_files\n        self.logs_root = logs_root\n        self.ctl = ctl\n        self.version_cmd = ['apache2ctl', '-v'] if not version_cmd else version_cmd\n        self.restart_cmd = ['apache2ctl', 'graceful'] if not restart_cmd else restart_cmd\n        self.restart_cmd_alt = restart_cmd_alt\n        self.conftest_cmd = ['apache2ctl', 'configtest'] if not conftest_cmd else conftest_cmd\n        syntax_tests_cmd_base = [ctl, '-t', '-D']\n        self.get_defines_cmd = syntax_tests_cmd_base + ['DUMP_RUN_CFG']\n        self.get_includes_cmd = syntax_tests_cmd_base + ['DUMP_INCLUDES']\n        self.get_modules_cmd = syntax_tests_cmd_base + ['DUMP_MODULES']\n        self.enmod = enmod\n        self.dismod = dismod\n        self.le_vhost_ext = le_vhost_ext\n        self.handle_modules = handle_modules\n        self.handle_sites = handle_sites\n        self.challenge_location = challenge_location\n        self.bin = apache_bin\n\n\n# TODO: Augeas sections ie. <VirtualHost>, <IfModule> beginning and closing\n# tags need to be the same case, otherwise Augeas doesn't recognize them.\n# This is not able to be completely remedied by regular expressions because\n# Augeas views <VirtualHost> </Virtualhost> as an error. This will just\n# require another check_parsing_errors() after all files are included...\n# (after a find_directive search is executed currently). It can be a one\n# time check however because all of LE's transactions will ensure\n# only properly formed sections are added.\n\n# Note: This protocol works for filenames with spaces in it, the sites are\n# properly set up and directives are changed appropriately, but Apache won't\n# recognize names in sites-enabled that have spaces. These are not added to the\n# Apache configuration. It may be wise to warn the user if they are trying\n# to use vhost filenames that contain spaces and offer to change ' ' to '_'\n\n# Note: FILEPATHS and changes to files are transactional.  They are copied\n# over before the updates are made to the existing files. NEW_FILES is\n# transactional due to the use of register_file_creation()\n\n\n# TODO: Verify permissions on configuration root... it is easier than\n#     checking permissions on each of the relative directories and less error\n#     prone.\n# TODO: Write a server protocol finder. Listen <port> <protocol> or\n#     Protocol <protocol>.  This can verify partial setups are correct\n# TODO: Add directives to sites-enabled... not sites-available.\n#     sites-available doesn't allow immediate find_dir search even with save()\n#     and load()\nclass ApacheConfigurator(common.Configurator):\n    \"\"\"Apache configurator.\n\n    :ivar config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n\n    :ivar parser: Handles low level parsing\n    :type parser: :class:`~certbot_apache._internal.parser`\n\n    :ivar tup version: version of Apache\n    :ivar list vhosts: All vhosts found in the configuration\n        (:class:`list` of :class:`~certbot_apache._internal.obj.VirtualHost`)\n\n    :ivar dict assoc: Mapping between domains and vhosts\n\n    \"\"\"\n\n    description: str = \"Apache Web Server plugin\"\n    if os.environ.get(\"CERTBOT_DOCS\") == \"1\":\n        description += (  # pragma: no cover\n            \" (Please note that the default values of the Apache plugin options\"\n            \" change depending on the operating system Certbot is run on.)\"\n        )\n\n    OS_DEFAULTS: OsOptions = OsOptions()\n\n    def pick_apache_config(self, warn_on_no_mod_ssl: bool = True) -> str:\n        \"\"\"\n        Pick the appropriate TLS Apache configuration file for current version of Apache and OS.\n\n        :param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.\n\n        :return: the path to the TLS Apache configuration file to use\n        :rtype: str\n        \"\"\"\n        # Disabling TLS session tickets is supported by Apache 2.4.11+ and OpenSSL 1.0.2l+.\n        # So for old versions of Apache we pick a configuration without this option.\n        min_openssl_version = util.parse_loose_version('1.0.2l')\n        openssl_version = self.openssl_version(warn_on_no_mod_ssl)\n        if self.version < (2, 4, 11) or not openssl_version or \\\n            util.parse_loose_version(openssl_version) < min_openssl_version:\n            # If we're supposed to warn about failing to find mod_ssl, we already did it in the\n            # openssl_version function and don't need to do it again here.\n            if openssl_version is not None:\n                logger.warning('Certbot has detected that apache version < 2.4.11 or compiled '\n                    'against openssl < 1.0.2l. Since these are deprecated, the configuration file '\n                    'being installed at %s will not receive future updates. To get the latest '\n                    'configuration version, update apache.',\n                    self.mod_ssl_conf)\n            return apache_util.find_ssl_apache_conf(\"old\")\n        return apache_util.find_ssl_apache_conf(\"current\")\n\n    def _override_cmds(self) -> None:\n        \"\"\"\n        Set our various command binaries to whatever the user has overridden for apachectl\n        \"\"\"\n        self.options.version_cmd[0] = self.options.ctl\n        self.options.restart_cmd[0] = self.options.ctl\n        self.options.conftest_cmd[0] = self.options.ctl\n        self.options.get_modules_cmd[0] = self.options.ctl\n        self.options.get_includes_cmd[0] = self.options.ctl\n        self.options.get_defines_cmd[0] = self.options.ctl\n\n    def _prepare_options(self) -> None:\n        \"\"\"\n        Set the values possibly changed by command line parameters to\n        OS_DEFAULTS constant dictionary\n        \"\"\"\n        opts = [\"enmod\", \"dismod\", \"le_vhost_ext\", \"server_root\", \"vhost_root\",\n                \"logs_root\", \"challenge_location\", \"handle_modules\", \"handle_sites\",\n                \"ctl\", \"bin\"]\n        for o in opts:\n            # Config options use dashes instead of underscores\n            if self.conf(o.replace(\"_\", \"-\")) is not None:\n                setattr(self.options, o, self.conf(o.replace(\"_\", \"-\")))\n            else:\n                setattr(self.options, o, getattr(self.OS_DEFAULTS, o))\n\n        self._override_cmds()\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        # When adding, modifying or deleting command line arguments, be sure to\n        # include the changes in the list used in method _prepare_options() to\n        # ensure consistent behavior.\n\n        # Respect CERTBOT_DOCS environment variable and use default values from\n        # base class regardless of the underlying distribution (overrides).\n        if os.environ.get(\"CERTBOT_DOCS\") == \"1\":\n            DEFAULTS = ApacheConfigurator.OS_DEFAULTS\n        else:\n            # cls.OS_DEFAULTS can be distribution specific, see override classes\n            DEFAULTS = cls.OS_DEFAULTS\n        add(\"enmod\", default=DEFAULTS.enmod,\n            help=\"Path to the Apache 'a2enmod' binary\")\n        add(\"dismod\", default=DEFAULTS.dismod,\n            help=\"Path to the Apache 'a2dismod' binary\")\n        add(\"le-vhost-ext\", default=DEFAULTS.le_vhost_ext,\n            help=\"SSL vhost configuration extension\")\n        add(\"server-root\", default=DEFAULTS.server_root,\n            help=\"Apache server root directory\")\n        add(\"vhost-root\", default=None,\n            help=\"Apache server VirtualHost configuration root\")\n        add(\"logs-root\", default=DEFAULTS.logs_root,\n            help=\"Apache server logs directory\")\n        add(\"challenge-location\",\n            default=DEFAULTS.challenge_location,\n            help=\"Directory path for challenge configuration\")\n        add(\"handle-modules\", default=DEFAULTS.handle_modules,\n            help=\"Let installer handle enabling required modules for you \" +\n                 \"(Only Ubuntu/Debian currently)\")\n        add(\"handle-sites\", default=DEFAULTS.handle_sites,\n            help=\"Let installer handle enabling sites for you \" +\n                 \"(Only Ubuntu/Debian currently)\")\n        add(\"ctl\", default=DEFAULTS.ctl,\n            help=\"Full path to Apache control script\")\n        add(\"bin\", default=DEFAULTS.bin,\n            help=\"Full path to apache2/httpd binary\")\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Initialize an Apache Configurator.\n\n        :param tup version: version of Apache as a tuple (2, 4, 7)\n            (used mostly for unittesting)\n\n        \"\"\"\n        version = kwargs.pop(\"version\", None)\n        use_parsernode = kwargs.pop(\"use_parsernode\", False)\n        openssl_version = kwargs.pop(\"openssl_version\", None)\n        super().__init__(*args, **kwargs)\n\n        # Add name_server association dict\n        self.assoc: dict[str, obj.VirtualHost] = {}\n        # Outstanding challenges\n        self._chall_out: set[achallenges.AnnotatedChallenge] = set()\n        # List of vhosts configured per wildcard domain on this run.\n        # used by deploy_cert() and enhance()\n        self._wildcard_vhosts: dict[str, list[obj.VirtualHost]] = {}\n        # Maps enhancements to vhosts we've enabled the enhancement for\n        self._enhanced_vhosts: defaultdict[str, set[obj.VirtualHost]] = defaultdict(set)\n        # Temporary state for AutoHSTS enhancement\n        self._autohsts: dict[str, dict[str, Union[int, float]]] = {}\n        # Reverter save notes\n        self.save_notes: str = \"\"\n        # Should we use ParserNode implementation instead of the old behavior\n        self.USE_PARSERNODE = use_parsernode\n        # Saves the list of file paths that were parsed initially, and\n        # not added to parser tree by self.conf(\"vhost-root\") for example.\n        self.parsed_paths: list[str] = []\n        # These will be set in the prepare function\n        self._prepared: bool = False\n        self.parser: parser.ApacheParser\n        self.parser_root: Optional[dualparser.DualBlockNode] = None\n        self.version = version\n        self._openssl_version: Optional[str] = openssl_version\n        self.vhosts: list[obj.VirtualHost]\n        self.options = copy.deepcopy(self.OS_DEFAULTS)\n        self._enhance_func: dict[str, Callable[[obj.VirtualHost, Any], None]] = {\n            \"redirect\": self._enable_redirect,\n            \"ensure-http-header\": self._set_http_header,\n            \"staple-ocsp\": self._enable_ocsp_stapling,\n        }\n\n    @property\n    def mod_ssl_conf(self) -> str:\n        \"\"\"Full absolute path to SSL configuration file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)\n\n    @property\n    def updated_mod_ssl_conf_digest(self) -> str:\n        \"\"\"Full absolute path to digest of updated SSL configuration file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)\n\n    def _open_module_file(self, ssl_module_location: str) -> Optional[bytes]:\n        \"\"\"Extract the open lines of openssl_version for testing purposes\"\"\"\n        try:\n            with open(ssl_module_location, mode=\"rb\") as f:\n                contents = f.read()\n        except OSError as error:\n            logger.debug(str(error), exc_info=True)\n            return None\n        return contents\n\n    def openssl_version(self, warn_on_no_mod_ssl: bool = True) -> Optional[str]:\n        \"\"\"Lazily retrieve openssl version\n\n        :param bool warn_on_no_mod_ssl: `True` if we should warn if mod_ssl is not found. Set to\n            `False` when we know we'll try to enable mod_ssl later. This is currently debian/ubuntu,\n            when called from `prepare`.\n\n        :return: the OpenSSL version as a string, or None.\n        :rtype: str or None\n        \"\"\"\n        if self._openssl_version:\n            return self._openssl_version\n        # Step 1. Determine the location of ssl_module\n        try:\n            ssl_module_location = self.parser.modules['ssl_module']\n        except KeyError:\n            if warn_on_no_mod_ssl:\n                logger.warning(\"Could not find ssl_module; not disabling session tickets.\")\n            return None\n        if ssl_module_location:\n            # Possibility A: ssl_module is a DSO\n            ssl_module_location = self.parser.standard_path_from_server_root(ssl_module_location)\n        else:\n            # Possibility B: ssl_module is statically linked into Apache\n            if self.options.bin:\n                ssl_module_location = self.options.bin\n            else:\n                logger.warning(\"ssl_module is statically linked but --apache-bin is \"\n                               \"missing; not disabling session tickets.\")\n                return None\n        # Step 2. Grep in the binary for openssl version\n        contents = self._open_module_file(ssl_module_location)\n        if not contents:\n            logger.warning(\"Unable to read ssl_module file; not disabling session tickets.\")\n            return None\n        # looks like: OpenSSL 1.0.2s  28 May 2019\n        matches = re.findall(br\"OpenSSL ([0-9]\\.[^ ]+) \", contents)\n        if not matches:\n            logger.warning(\"Could not find OpenSSL version; not disabling session tickets.\")\n            return None\n        self._openssl_version = matches[0].decode('UTF-8')\n        return self._openssl_version\n\n    def prepare(self) -> None:\n        \"\"\"Prepare the authenticator/installer.\n\n        :raises .errors.NoInstallationError: If Apache configs cannot be found\n        :raises .errors.MisconfigurationError: If Apache is misconfigured\n        :raises .errors.NotSupportedError: If Apache version is not supported\n        :raises .errors.PluginError: If there is any other error\n\n        \"\"\"\n        self._prepare_options()\n\n        # Verify Apache is installed\n        self._verify_exe_availability(self.options.ctl)\n\n        # Make sure configuration is valid\n        self.config_test()\n\n        # Set Version\n        if self.version is None:\n            self.version = self.get_version()\n            logger.debug('Apache version is %s',\n                         '.'.join(str(i) for i in self.version))\n        if self.version < (2, 4):\n            raise errors.NotSupportedError(\n                \"Apache Version {0} not supported.\".format(str(self.version)))\n\n        # Recover from previous crash before Augeas initialization to have the\n        # correct parse tree from the get go.\n        self.recovery_routine()\n        # Perform the actual Augeas initialization to be able to react\n        self.parser = self.get_parser()\n\n        # Set up ParserNode root\n        pn_meta = {\"augeasparser\": self.parser,\n                   \"augeaspath\": self.parser.get_root_augpath(),\n                   \"ac_ast\": None}\n        if self.USE_PARSERNODE:\n            parser_root = self.get_parsernode_root(pn_meta)\n            self.parser_root = parser_root\n            self.parsed_paths = parser_root.parsed_paths()\n\n        # Check for errors in parsing files with Augeas\n        self.parser.check_parsing_errors(\"httpd.aug\")\n\n        # Get all of the available vhosts\n        self.vhosts = self.get_virtual_hosts()\n\n        # We may try to enable mod_ssl later. If so, we shouldn't warn if we can't find it now.\n        # This is currently only true for debian/ubuntu.\n        warn_on_no_mod_ssl = not self.options.handle_modules\n        self.install_ssl_options_conf(self.mod_ssl_conf,\n                                      self.updated_mod_ssl_conf_digest,\n                                      warn_on_no_mod_ssl)\n\n        # Prevent two Apache plugins from modifying a config at once\n        try:\n            util.lock_dir_until_exit(self.options.server_root)\n        except (OSError, errors.LockError):\n            logger.debug(\"Encountered error:\", exc_info=True)\n            raise errors.PluginError(\n                \"Unable to create a lock file in {0}. Are you running\"\n                \" Certbot with sufficient privileges to modify your\"\n                \" Apache configuration?\".format(self.options.server_root))\n        self._prepared = True\n\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        \"\"\"Saves all changes to the configuration files.\n\n        This function first checks for save errors, if none are found,\n        all configuration changes made will be saved. According to the\n        function parameters. If an exception is raised, a new checkpoint\n        was not created.\n\n        :param str title: The title of the save. If a title is given, the\n            configuration will be saved as a new checkpoint and put in a\n            timestamped directory.\n\n        :param bool temporary: Indicates whether the changes made will\n            be quickly reversed in the future (ie. challenges)\n\n        \"\"\"\n        save_files = self.parser.unsaved_files()\n        if save_files:\n            self.add_to_checkpoint(save_files,\n                                   self.save_notes, temporary=temporary)\n        # Handle the parser specific tasks\n        self.parser.save(save_files)\n        if title and not temporary:\n            self.finalize_checkpoint(title)\n\n    def recovery_routine(self) -> None:\n        \"\"\"Revert all previously modified files.\n\n        Reverts all modified files that have not been saved as a checkpoint\n\n        :raises .errors.PluginError: If unable to recover the configuration\n\n        \"\"\"\n        super().recovery_routine()\n        # Reload configuration after these changes take effect if needed\n        # ie. ApacheParser has been initialized.\n        if hasattr(self, \"parser\"):\n            # TODO: wrap into non-implementation specific  parser interface\n            self.parser.aug.load()\n\n    def revert_challenge_config(self) -> None:\n        \"\"\"Used to cleanup challenge configurations.\n\n        :raises .errors.PluginError: If unable to revert the challenge config.\n\n        \"\"\"\n        self.revert_temporary_config()\n        self.parser.aug.load()\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        \"\"\"Rollback saved checkpoints.\n\n        :param int rollback: Number of checkpoints to revert\n\n        :raises .errors.PluginError: If there is a problem with the input or\n            the function is unable to correctly revert the configuration\n\n        \"\"\"\n        super().rollback_checkpoints(rollback)\n        self.parser.aug.load()\n\n    def _verify_exe_availability(self, exe: str) -> None:\n        \"\"\"Checks availability of Apache executable\"\"\"\n        if not util.exe_exists(exe):\n            if not path_surgery(exe):\n                raise errors.NoInstallationError(\n                    'Cannot find Apache executable {0}'.format(exe))\n\n    def get_parser(self) -> parser.ApacheParser:\n        \"\"\"Initializes the ApacheParser\"\"\"\n        # If user provided vhost_root value in command line, use it\n        return parser.ApacheParser(\n            self.options.server_root, self, self.conf(\"vhost-root\"), version=self.version)\n\n    def get_parsernode_root(self, metadata: dict[str, Any]) -> dualparser.DualBlockNode:\n        \"\"\"Initializes the ParserNode parser root instance.\"\"\"\n\n        if HAS_APACHECONFIG:\n            apache_vars = {\n                \"defines\": apache_util.parse_defines(self.options.get_defines_cmd),\n                \"includes\": apache_util.parse_includes(self.options.get_includes_cmd),\n                \"modules\": apache_util.parse_modules(self.options.get_modules_cmd),\n            }\n            metadata[\"apache_vars\"] = apache_vars\n\n            with open(self.parser.loc[\"root\"]) as f:\n                with apacheconfig.make_loader(writable=True,\n                                              **apacheconfig.flavors.NATIVE_APACHE) as loader:\n                    metadata[\"ac_ast\"] = loader.loads(f.read())\n\n        return dualparser.DualBlockNode(\n            name=assertions.PASS,\n            ancestor=None,\n            filepath=self.parser.loc[\"root\"],\n            metadata=metadata,\n        )\n\n    def deploy_cert(\n        self, domain: str, cert_path: str, key_path: str,\n        chain_path: Optional[str] = None, fullchain_path: Optional[str] = None\n    ) -> None:\n        \"\"\"Deploys certificate to specified virtual host.\n\n        Currently tries to find the last directives to deploy the certificate\n        in the VHost associated with the given domain. If it can't find the\n        directives, it searches the \"included\" confs. The function verifies\n        that it has located the three directives and finally modifies them\n        to point to the correct destination. After the certificate is\n        installed, the VirtualHost is enabled if it isn't already.\n\n        .. todo:: Might be nice to remove chain directive if none exists\n                  This shouldn't happen within certbot though\n\n        :raises errors.PluginError: When unable to deploy certificate due to\n            a lack of directives\n\n        \"\"\"\n        vhosts = self.choose_vhosts(domain)\n        for vhost in vhosts:\n            self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)\n            display_util.notify(\"Successfully deployed certificate for {} to {}\"\n                                .format(domain, vhost.filep))\n\n    def choose_vhosts(self, domain: str, create_if_no_ssl: bool = True) -> list[obj.VirtualHost]:\n        \"\"\"\n        Finds VirtualHosts that can be used with the provided domain\n\n        :param str domain: Domain name to match VirtualHosts to\n        :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS\n            counterpart, should one get created\n\n        :returns: List of VirtualHosts or None\n        :rtype: `list` of :class:`~certbot_apache._internal.obj.VirtualHost`\n        \"\"\"\n\n        if util.is_wildcard_domain(domain):\n            if domain in self._wildcard_vhosts:\n                # Vhosts for a wildcard domain were already selected\n                return self._wildcard_vhosts[domain]\n            # Ask user which VHosts to support.\n            # Returned objects are guaranteed to be ssl vhosts\n            return self._choose_vhosts_wildcard(domain, create_if_no_ssl)\n        else:\n            return [self.choose_vhost(domain, create_if_no_ssl)]\n\n    def _vhosts_for_wildcard(self, domain: str) -> list[obj.VirtualHost]:\n        \"\"\"\n        Get VHost objects for every VirtualHost that the user wants to handle\n        with the wildcard certificate.\n        \"\"\"\n\n        # Collect all vhosts that match the name\n        matched: set[obj.VirtualHost] = set()\n        for vhost in self.vhosts:\n            for name in vhost.get_names():\n                if self._in_wildcard_scope(name, domain):\n                    matched.add(vhost)\n\n        return list(matched)\n\n    def _generate_no_suitable_vhost_error(self, target_name: str) -> errors.PluginError:\n        \"\"\"\n        Prepare an error to notify the user that Certbot could not find a vhost\n        to secure.\n        :param str target_name: The server name that could not be mapped\n        :return: An instance of PluginError\n        :rtype: errors.PluginError\n        \"\"\"\n        return errors.PluginError(\n            \"Certbot could not find a VirtualHost for {0} in the Apache \"\n            \"configuration. Please create a VirtualHost with a ServerName \"\n            \"matching {0} and try again.\".format(target_name)\n        )\n\n    def _in_wildcard_scope(self, name: str, domain: str) -> Optional[bool]:\n        \"\"\"\n        Helper method for _vhosts_for_wildcard() that makes sure that the domain\n        is in the scope of wildcard domain.\n\n        eg. in scope: domain = *.wild.card, name = 1.wild.card\n        not in scope: domain = *.wild.card, name = 1.2.wild.card\n        \"\"\"\n        if len(name.split(\".\")) == len(domain.split(\".\")):\n            return fnmatch.fnmatch(name, domain)\n        return None\n\n    def _choose_vhosts_wildcard(self, domain: str, create_ssl: bool = True\n                                ) -> list[obj.VirtualHost]:\n        \"\"\"Prompts user to choose vhosts to install a wildcard certificate for\"\"\"\n\n        # Get all vhosts that are covered by the wildcard domain\n        vhosts: list[obj.VirtualHost] = self._vhosts_for_wildcard(domain)\n\n        # Go through the vhosts, making sure that we cover all the names\n        # present, but preferring the SSL vhosts\n        filtered_vhosts = {}\n        for vhost in vhosts:\n            for name in vhost.get_names():\n                if vhost.ssl:\n                    # Always prefer SSL vhosts\n                    filtered_vhosts[name] = vhost\n                elif name not in filtered_vhosts and create_ssl:\n                    # Add if not in list previously\n                    filtered_vhosts[name] = vhost\n\n        # Only unique VHost objects\n        dialog_input = set(filtered_vhosts.values())\n\n        # Ask the user which of names to enable, expect list of names back\n        dialog_output = display_ops.select_vhost_multiple(list(dialog_input))\n\n        if not dialog_output:\n            raise self._generate_no_suitable_vhost_error(domain)\n\n        # Make sure we create SSL vhosts for the ones that are HTTP only\n        # if requested.\n        return_vhosts = []\n        for vhost in dialog_output:\n            if not vhost.ssl:\n                return_vhosts.append(self.make_vhost_ssl(vhost))\n            else:\n                return_vhosts.append(vhost)\n\n        self._wildcard_vhosts[domain] = return_vhosts\n        return return_vhosts\n\n    def _deploy_cert(\n        self, vhost: obj.VirtualHost, cert_path: str, key_path: str,\n        chain_path: Optional[str] = None, fullchain_path: Optional[str] = None,\n    ) -> None:\n        \"\"\"\n        Helper function for deploy_cert() that handles the actual deployment\n        this exists because we might want to do multiple deployments per\n        domain originally passed for deploy_cert(). This is especially true\n        with wildcard certificates\n        \"\"\"\n        # This is done first so that ssl module is enabled and cert_path,\n        # cert_key... can all be parsed appropriately\n        self.prepare_server_https(\"443\")\n\n        # If we haven't managed to enable mod_ssl by this point, error out\n        if \"ssl_module\" not in self.parser.modules:\n            raise errors.MisconfigurationError(\"Could not find ssl_module; \"\n                                               \"not installing certificate.\")\n\n        # Add directives and remove duplicates\n        self._add_dummy_ssl_directives(vhost.path)\n        self._clean_vhost(vhost)\n\n        path: dict[str, Any] = {\n            \"cert_path\": self.parser.find_dir(\"SSLCertificateFile\", None, vhost.path),\n            \"cert_key\": self.parser.find_dir(\"SSLCertificateKeyFile\", None, vhost.path),\n        }\n\n        # Only include if a certificate chain is specified\n        if chain_path is not None:\n            path[\"chain_path\"] = self.parser.find_dir(\n                \"SSLCertificateChainFile\", None, vhost.path)\n\n        logger.info(\"Deploying Certificate to VirtualHost %s\", vhost.filep)\n\n        if self.version < (2, 4, 8) or (chain_path and not fullchain_path):\n            # install SSLCertificateFile, SSLCertificateKeyFile,\n            # and SSLCertificateChainFile directives\n            set_cert_path = cert_path\n            self.parser.aug.set(path[\"cert_path\"][-1], cert_path)\n            self.parser.aug.set(path[\"cert_key\"][-1], key_path)\n            if chain_path is not None:\n                self.parser.add_dir(vhost.path,\n                                    \"SSLCertificateChainFile\", chain_path)\n            else:\n                raise errors.PluginError(\"--chain-path is required for your \"\n                                         \"version of Apache\")\n        else:\n            if not fullchain_path:\n                raise errors.PluginError(\"Please provide the --fullchain-path \"\n                                         \"option pointing to your full chain file\")\n            set_cert_path = fullchain_path\n            self.parser.aug.set(path[\"cert_path\"][-1], fullchain_path)\n            self.parser.aug.set(path[\"cert_key\"][-1], key_path)\n\n        # Enable the new vhost if needed\n        if not vhost.enabled:\n            self.enable_site(vhost)\n\n        # Save notes about the transaction that took place\n        self.save_notes += (\"Changed vhost at %s with addresses of %s\\n\"\n                            \"\\tSSLCertificateFile %s\\n\"\n                            \"\\tSSLCertificateKeyFile %s\\n\" %\n                            (vhost.filep,\n                             \", \".join(str(addr) for addr in vhost.addrs),\n                             set_cert_path, key_path))\n        if chain_path is not None:\n            self.save_notes += \"\\tSSLCertificateChainFile %s\\n\" % chain_path\n\n    def choose_vhost(self, target_name: str, create_if_no_ssl: bool = True) -> obj.VirtualHost:\n        \"\"\"Chooses a virtual host based on the given domain name.\n\n        If there is no clear virtual host to be selected, the user is prompted\n        with all available choices.\n\n        The returned vhost is guaranteed to have TLS enabled unless\n        create_if_no_ssl is set to False, in which case there is no such guarantee\n        and the result is not cached.\n\n        :param str target_name: domain name\n        :param bool create_if_no_ssl: If found VirtualHost doesn't have a HTTPS\n            counterpart, should one get created\n\n        :returns: vhost associated with name\n        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises .errors.PluginError: If no vhost is available or chosen\n\n        \"\"\"\n        # Allows for domain names to be associated with a virtual host\n        if target_name in self.assoc:\n            return self.assoc[target_name]\n\n        # Try to find a reasonable vhost\n        vhost = self._find_best_vhost(target_name)\n        if vhost is not None:\n            if not create_if_no_ssl:\n                return vhost\n            if not vhost.ssl:\n                vhost = self.make_vhost_ssl(vhost)\n\n            self._add_servername_alias(target_name, vhost)\n            self.assoc[target_name] = vhost\n            return vhost\n\n        # Negate create_if_no_ssl value to indicate if we want a SSL vhost\n        # to get created if a non-ssl vhost is selected.\n        return self._choose_vhost_from_list(target_name, temp=not create_if_no_ssl)\n\n    def _choose_vhost_from_list(self, target_name: str,\n                                temp: bool = False) -> obj.VirtualHost:\n        # Select a vhost from a list\n        vhost = display_ops.select_vhost(target_name, self.vhosts)\n        if vhost is None:\n            raise self._generate_no_suitable_vhost_error(target_name)\n        if temp:\n            return vhost\n        if not vhost.ssl:\n            addrs = self._get_proposed_addrs(vhost, \"443\")\n            # TODO: Conflicts is too conservative\n            if not any(one_vhost.enabled and one_vhost.conflicts(addrs) for\n                       one_vhost in self.vhosts):\n                vhost = self.make_vhost_ssl(vhost)\n            else:\n                logger.error(\n                    \"The selected vhost would conflict with other HTTPS \"\n                    \"VirtualHosts within Apache. Please select another \"\n                    \"vhost or add ServerNames to your configuration.\")\n                raise errors.PluginError(\"VirtualHost not able to be selected.\")\n\n        self._add_servername_alias(target_name, vhost)\n        self.assoc[target_name] = vhost\n        return vhost\n\n    def domain_in_names(self, names: Iterable[str], target_name: str) -> bool:\n        \"\"\"Checks if target domain is covered by one or more of the provided\n        names. The target name is matched by wildcard as well as exact match.\n\n        :param names: server aliases\n        :type names: `collections.Iterable` of `str`\n        :param str target_name: name to compare with wildcards\n\n        :returns: True if target_name is covered by a wildcard,\n            otherwise, False\n        :rtype: bool\n\n        \"\"\"\n        # use lowercase strings because fnmatch can be case sensitive\n        target_name = target_name.lower()\n        for name in names:\n            name = name.lower()\n            # fnmatch treats \"[seq]\" specially and [ or ] characters aren't\n            # valid in Apache but Apache doesn't error out if they are present\n            if \"[\" not in name and fnmatch.fnmatch(target_name, name):\n                return True\n        return False\n\n    def find_best_http_vhost(\n        self, target: str, filter_defaults: bool, port: str = \"80\"\n    ) -> Optional[obj.VirtualHost]:\n        \"\"\"Returns non-HTTPS vhost objects found from the Apache config\n\n        :param str target: Domain name of the desired VirtualHost\n        :param bool filter_defaults: whether _default_ vhosts should be\n            included if it is the best match\n        :param str port: port number the vhost should be listening on\n\n        :returns: VirtualHost object that's the best match for target name\n        :rtype: `obj.VirtualHost` or None\n        \"\"\"\n        filtered_vhosts = []\n        for vhost in self.vhosts:\n            if any(a.is_wildcard() or a.get_port() == port for a in vhost.addrs) and not vhost.ssl:\n                filtered_vhosts.append(vhost)\n        return self._find_best_vhost(target, filtered_vhosts, filter_defaults)\n\n    def _find_best_vhost(\n        self, target_name: str, vhosts: Optional[list[obj.VirtualHost]] = None,\n        filter_defaults: bool = True\n    ) -> Optional[obj.VirtualHost]:\n        \"\"\"Finds the best vhost for a target_name.\n\n        This does not upgrade a vhost to HTTPS... it only finds the most\n        appropriate vhost for the given target_name.\n\n        :param str target_name: domain handled by the desired vhost\n        :param vhosts: vhosts to consider\n        :type vhosts: `collections.Iterable` of :class:`~certbot_apache._internal.obj.VirtualHost`\n        :param bool filter_defaults: whether a vhost with a _default_\n            addr is acceptable\n\n        :returns: VHost or None\n\n        \"\"\"\n        # Points 6 - Servername SSL\n        # Points 5 - Wildcard SSL\n        # Points 4 - Address name with SSL\n        # Points 3 - Servername no SSL\n        # Points 2 - Wildcard no SSL\n        # Points 1 - Address name with no SSL\n        best_candidate: Optional[obj.VirtualHost] = None\n        best_points: int = 0\n\n        if vhosts is None:\n            vhosts = self.vhosts\n\n        for vhost in vhosts:\n            if vhost.modmacro is True:\n                continue\n            names = vhost.get_names()\n            if target_name in names:\n                points = 3\n            elif self.domain_in_names(names, target_name):\n                points = 2\n            elif any(addr.get_addr() == target_name for addr in vhost.addrs):\n                points = 1\n            else:\n                # No points given if names can't be found.\n                # This gets hit but doesn't register\n                continue  # pragma: no cover\n\n            if vhost.ssl:\n                points += 3\n\n            if points > best_points:\n                best_points = points\n                best_candidate = vhost\n\n        # No winners here... is there only one reasonable vhost?\n        if best_candidate is None:\n            if filter_defaults:\n                vhosts = self._non_default_vhosts(vhosts)\n            # remove mod_macro hosts from reasonable vhosts\n            reasonable_vhosts = [vh for vh\n                                 in vhosts if vh.modmacro is False]\n            if len(reasonable_vhosts) == 1:\n                best_candidate = reasonable_vhosts[0]\n\n        return best_candidate\n\n    def _non_default_vhosts(self, vhosts: list[obj.VirtualHost]) -> list[obj.VirtualHost]:\n        \"\"\"Return all non _default_ only vhosts.\"\"\"\n        return [vh for vh in vhosts if not all(\n            addr.get_addr() == \"_default_\" for addr in vh.addrs\n        )]\n\n    def get_all_names(self) -> set[str]:\n        \"\"\"Returns all names found in the Apache Configuration.\n\n        :returns: All ServerNames, ServerAliases, and reverse DNS entries for\n                  virtual host addresses\n        :rtype: set\n\n        \"\"\"\n        all_names: set[str] = set()\n\n        vhost_macro = []\n\n        for vhost in self.vhosts:\n            all_names.update(vhost.get_names())\n            if vhost.modmacro:\n                vhost_macro.append(vhost.filep)\n\n            for addr in vhost.addrs:\n                if common.hostname_regex.match(addr.get_addr()):\n                    all_names.add(addr.get_addr())\n                else:\n                    name = self.get_name_from_ip(addr)\n                    if name:\n                        all_names.add(name)\n\n        if vhost_macro:\n            display_util.notification(\n                \"Apache mod_macro seems to be in use in file(s):\\n{0}\"\n                \"\\n\\nUnfortunately mod_macro is not yet supported\".format(\n                    \"\\n  \".join(vhost_macro)), force_interactive=True)\n\n        return util.get_filtered_names(all_names)\n\n    def get_name_from_ip(self, addr: obj.Addr) -> str:\n        \"\"\"Returns a reverse dns name if available.\n\n        :param addr: IP Address\n        :type addr: ~.common.Addr\n\n        :returns: name or empty string if name cannot be determined\n        :rtype: str\n\n        \"\"\"\n        # If it isn't a private IP, do a reverse DNS lookup\n        if not common.private_ips_regex.match(addr.get_addr()):\n            try:\n                socket.inet_aton(addr.get_addr())\n                return socket.gethostbyaddr(addr.get_addr())[0]\n            except (OSError, socket.herror, socket.timeout):\n                pass\n\n        return \"\"\n\n    def _get_vhost_names(self, path: Optional[str]) -> tuple[Optional[str], list[Optional[str]]]:\n        \"\"\"Helper method for getting the ServerName and\n        ServerAlias values from vhost in path\n\n        :param path: Path to read ServerName and ServerAliases from\n\n        :returns: Tuple including ServerName and `list` of ServerAlias strings\n        \"\"\"\n\n        servername_match = self.parser.find_dir(\n            \"ServerName\", None, start=path, exclude=False)\n        serveralias_match = self.parser.find_dir(\n            \"ServerAlias\", None, start=path, exclude=False)\n\n        serveraliases = []\n        for alias in serveralias_match:\n            serveralias = self.parser.get_arg(alias)\n            serveraliases.append(serveralias)\n\n        servername = None\n        if servername_match:\n            # Get last ServerName as each overwrites the previous\n            servername = self.parser.get_arg(servername_match[-1])\n\n        return servername, serveraliases\n\n    def _add_servernames(self, host: obj.VirtualHost) -> None:\n        \"\"\"Helper function for get_virtual_hosts().\n\n        :param host: In progress vhost whose names will be added\n        :type host: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        \"\"\"\n\n        servername, serveraliases = self._get_vhost_names(host.path)\n\n        for alias in serveraliases:\n            if not host.modmacro and alias is not None:\n                host.aliases.add(alias)\n\n        if not host.modmacro:\n            host.name = servername\n\n    def _create_vhost(self, path: str) -> Optional[obj.VirtualHost]:\n        \"\"\"Used by get_virtual_hosts to create vhost objects\n\n        :param str path: Augeas path to virtual host\n\n        :returns: newly created vhost\n        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        \"\"\"\n        addrs: set[obj.Addr] = set()\n        try:\n            args = self.parser.aug.match(path + \"/arg\")\n        except RuntimeError:\n            logger.warning(\"Encountered a problem while parsing file: %s, skipping\", path)\n            return None\n        for arg in args:\n            arg_value = self.parser.get_arg(arg)\n            if arg_value is not None:\n                addrs.add(obj.Addr.fromstring(arg_value))\n        is_ssl = False\n\n        if self.parser.find_dir(\"SSLEngine\", \"on\", start=path, exclude=False):\n            is_ssl = True\n\n        # \"SSLEngine on\" might be set outside of <VirtualHost>\n        # Treat vhosts with port 443 as ssl vhosts\n        for addr in addrs:\n            if addr.get_port() == \"443\":\n                is_ssl = True\n\n        filename = apache_util.get_file_path(\n            self.parser.aug.get(f\"/augeas/files{apache_util.get_file_path(path)}/path\"))\n        if filename is None:\n            return None\n\n        macro = False\n        if \"/macro/\" in path.lower():\n            macro = True\n\n        vhost_enabled = self.parser.parsed_in_original(filename)\n\n        vhost = obj.VirtualHost(filename, path, addrs, is_ssl,\n                                vhost_enabled, modmacro=macro)\n        self._add_servernames(vhost)\n        return vhost\n\n    def get_virtual_hosts(self) -> list[obj.VirtualHost]:\n        \"\"\"\n        Temporary wrapper for legacy and ParserNode version for\n        get_virtual_hosts. This should be replaced with the ParserNode\n        implementation when ready.\n        \"\"\"\n\n        v1_vhosts = self.get_virtual_hosts_v1()\n        if self.USE_PARSERNODE and HAS_APACHECONFIG:\n            v2_vhosts = self.get_virtual_hosts_v2()\n\n            for v1_vh in v1_vhosts:\n                found = False\n                for v2_vh in v2_vhosts:\n                    if assertions.isEqualVirtualHost(v1_vh, v2_vh):\n                        found = True\n                        break\n                if not found:\n                    raise AssertionError(\"Equivalent for {} was not found\".format(v1_vh.path))\n\n            return v2_vhosts\n        return v1_vhosts\n\n    def get_virtual_hosts_v1(self) -> list[obj.VirtualHost]:\n        \"\"\"Returns list of virtual hosts found in the Apache configuration.\n\n        :returns: List of :class:`~certbot_apache._internal.obj.VirtualHost`\n            objects found in configuration\n        :rtype: list\n\n        \"\"\"\n        # Search base config, and all included paths for VirtualHosts\n        file_paths: dict[str, str] = {}\n        internal_paths: defaultdict[str, set[str]] = defaultdict(set)\n        vhs = []\n        # Make a list of parser paths because the parser_paths\n        # dictionary may be modified during the loop.\n        for vhost_path in list(self.parser.parser_paths):\n            paths = self.parser.aug.match(\n                (\"/files%s//*[label()=~regexp('%s')]\" %\n                 (vhost_path, parser.case_i(\"VirtualHost\"))))\n            paths = [path for path in paths if\n                     \"virtualhost\" in os.path.basename(path).lower()]\n            for path in paths:\n                new_vhost = self._create_vhost(path)\n                if not new_vhost:\n                    continue\n                internal_path = apache_util.get_internal_aug_path(new_vhost.path)\n                realpath = filesystem.realpath(new_vhost.filep)\n                if realpath not in file_paths:\n                    file_paths[realpath] = new_vhost.filep\n                    internal_paths[realpath].add(internal_path)\n                    vhs.append(new_vhost)\n                elif (realpath == new_vhost.filep and\n                      realpath != file_paths[realpath]):\n                    # Prefer \"real\" vhost paths instead of symlinked ones\n                    # ex: sites-enabled/vh.conf -> sites-available/vh.conf\n\n                    # remove old (most likely) symlinked one\n                    new_vhs = []\n                    for v in vhs:\n                        if v.filep == file_paths[realpath]:\n                            internal_paths[realpath].remove(\n                                apache_util.get_internal_aug_path(v.path))\n                        else:\n                            new_vhs.append(v)\n                    vhs = new_vhs\n\n                    file_paths[realpath] = realpath\n                    internal_paths[realpath].add(internal_path)\n                    vhs.append(new_vhost)\n                elif internal_path not in internal_paths[realpath]:\n                    internal_paths[realpath].add(internal_path)\n                    vhs.append(new_vhost)\n        return vhs\n\n    def get_virtual_hosts_v2(self) -> list[obj.VirtualHost]:\n        \"\"\"Returns list of virtual hosts found in the Apache configuration using\n        ParserNode interface.\n        :returns: List of :class:`~certbot_apache.obj.VirtualHost`\n            objects found in configuration\n        :rtype: list\n        \"\"\"\n\n        if not self.parser_root:\n            raise errors.Error(\"This ApacheConfigurator instance is not\"  # pragma: no cover\n                               \" configured to use a node parser.\")\n        vhs = []\n        vhosts = self.parser_root.find_blocks(\"VirtualHost\", exclude=False)\n        for vhblock in vhosts:\n            vhs.append(self._create_vhost_v2(cast(ApacheBlockNode, vhblock)))\n        return vhs\n\n    def _create_vhost_v2(self, node: ApacheBlockNode) -> obj.VirtualHost:\n        \"\"\"Used by get_virtual_hosts_v2 to create vhost objects using ParserNode\n        interfaces.\n        :param ApacheBlockNode node: The BlockNode object of VirtualHost block\n        :returns: newly created vhost\n        :rtype: :class:`~certbot_apache.obj.VirtualHost`\n        \"\"\"\n        addrs = set()\n        for param in node.parameters:\n            addrs.add(obj.Addr.fromstring(param))\n\n        is_ssl = False\n        # Exclusion to match the behavior in get_virtual_hosts_v2\n        sslengine = node.find_directives(\"SSLEngine\", exclude=False)\n        if sslengine:\n            for directive in sslengine:\n                if directive.parameters[0].lower() == \"on\":\n                    is_ssl = True\n                    break\n\n        # \"SSLEngine on\" might be set outside of <VirtualHost>\n        # Treat vhosts with port 443 as ssl vhosts\n        for addr in addrs:\n            if addr.get_port() == \"443\":\n                is_ssl = True\n\n        if node.filepath is None:\n            raise errors.Error(\"Node filepath cannot be None.\")  # pragma: no cover\n\n        enabled = apache_util.included_in_paths(node.filepath, self.parsed_paths)\n\n        macro = False\n        # Check if the VirtualHost is contained in a mod_macro block\n        if node.find_ancestors(\"Macro\"):\n            macro = True\n        # VirtualHost V2 is part of migration to the pure-Python Apache parser project. It is not\n        # used on production as of now.\n        # TODO: Use a meaning full value for augeas path instead of an empty string\n        vhost = obj.VirtualHost(\n            node.filepath, \"\", addrs, is_ssl, enabled, modmacro=macro, node=node\n        )\n        self._populate_vhost_names_v2(vhost)\n        return vhost\n\n    def _populate_vhost_names_v2(self, vhost: obj.VirtualHost) -> None:\n        \"\"\"Helper function that populates the VirtualHost names.\n        :param host: In progress vhost whose names will be added\n        :type host: :class:`~certbot_apache.obj.VirtualHost`\n        \"\"\"\n        if not vhost.node:\n            raise errors.PluginError(\"Current VirtualHost has no node.\")  # pragma: no cover\n\n        servername_match = vhost.node.find_directives(\"ServerName\", exclude=False)\n        serveralias_match = vhost.node.find_directives(\"ServerAlias\", exclude=False)\n\n        servername = None\n        if servername_match:\n            servername = servername_match[-1].parameters[-1]\n\n        if not vhost.modmacro:\n            for alias in serveralias_match:\n                for serveralias in alias.parameters:\n                    vhost.aliases.add(serveralias)\n            vhost.name = servername\n\n    def prepare_server_https(self, port: str, temp: bool = False) -> None:\n        \"\"\"Prepare the server for HTTPS.\n\n        Make sure that the ssl_module is loaded and that the server\n        is appropriately listening on port.\n\n        :param str port: Port to listen on\n        :param bool temp: If this is just temporary\n        \"\"\"\n\n        self.prepare_https_modules(temp)\n        self.ensure_listen(port, https=True)\n\n    def ensure_listen(self, port: str, https: bool = False) -> None:\n        \"\"\"Make sure that Apache is listening on the port. Checks if the\n        Listen statement for the port already exists, and adds it to the\n        configuration if necessary.\n\n        :param str port: Port number to check and add Listen for if not in\n            place already\n        :param bool https: If the port will be used for HTTPS\n\n        \"\"\"\n\n        # If HTTPS requested for nonstandard port, add service definition\n        if https and port != \"443\":\n            port_service = f\"{port} https\"\n        else:\n            port_service = port\n\n        # Check for Listen <port>\n        # Note: This could be made to also look for ip:443 combo\n        directives = [self.parser.get_arg(x) for x in self.parser.find_dir(\"Listen\")]\n        listens = [directive.split()[0] for directive in directives if directive]\n\n        # Listen already in place\n        if self._has_port_already(listens, port):\n            return\n\n        listen_dirs = set(listens)\n\n        if not listens:\n            listen_dirs.add(port_service)\n\n        for listen in listens:\n            # For any listen statement, check if the machine also listens on\n            # the given port. If not, add such a listen statement.\n            if len(listen.split(\":\")) == 1:\n                # Its listening to all interfaces\n                if port not in listen_dirs and port_service not in listen_dirs:\n                    listen_dirs.add(port_service)\n            else:\n                # The Listen statement specifies an ip\n                _, ip = listen[::-1].split(\":\", 1)\n                ip = ip[::-1]\n                if \"%s:%s\" % (ip, port_service) not in listen_dirs and (\n                    \"%s:%s\" % (ip, port_service) not in listen_dirs):\n                    listen_dirs.add(\"%s:%s\" % (ip, port_service))\n        if https:\n            self._add_listens_https(listen_dirs, listens, port)\n        else:\n            self._add_listens_http(listen_dirs, listens, port)\n\n    def _add_listens_http(self, listens: set[str], listens_orig: list[str], port: str) -> None:\n        \"\"\"Helper method for ensure_listen to figure out which new\n        listen statements need adding for listening HTTP on port\n\n        :param set listens: Set of all needed Listen statements\n        :param list listens_orig: List of existing listen statements\n        :param string port: Port number we're adding\n        \"\"\"\n\n        new_listens = listens.difference(listens_orig)\n\n        if port in new_listens:\n            # We have wildcard, skip the rest\n            self.parser.add_dir(parser.get_aug_path(self.parser.loc[\"listen\"]),\n                                \"Listen\", port)\n            self.save_notes += (\n                f\"Added Listen {port} directive to {self.parser.loc['listen']}\\n\"\n            )\n        else:\n            for listen in new_listens:\n                self.parser.add_dir(parser.get_aug_path(\n                    self.parser.loc[\"listen\"]), \"Listen\", listen.split(\" \"))\n                self.save_notes += (f\"Added Listen {listen} directive to \"\n                                    f\"{self.parser.loc['listen']}\\n\")\n\n    def _add_listens_https(self, listens: set[str], listens_orig: list[str], port: str) -> None:\n        \"\"\"Helper method for ensure_listen to figure out which new\n        listen statements need adding for listening HTTPS on port\n\n        :param set listens: Set of all needed Listen statements\n        :param list listens_orig: List of existing listen statements\n        :param string port: Port number we're adding\n        \"\"\"\n\n        # Add service definition for non-standard ports\n        if port != \"443\":\n            port_service = f\"{port} https\"\n        else:\n            port_service = port\n\n        new_listens = listens.difference(listens_orig)\n\n        if port in new_listens or port_service in new_listens:\n            # We have wildcard, skip the rest\n            self.parser.add_dir_to_ifmodssl(\n                parser.get_aug_path(self.parser.loc[\"listen\"]),\n                \"Listen\", port_service.split(\" \"))\n            self.save_notes += (\n                f\"Added Listen {port_service} directive to {self.parser.loc['listen']}\\n\"\n            )\n        else:\n            for listen in new_listens:\n                self.parser.add_dir_to_ifmodssl(\n                    parser.get_aug_path(self.parser.loc[\"listen\"]),\n                    \"Listen\", listen.split(\" \"))\n                self.save_notes += (f\"Added Listen {listen} directive to \"\n                                    f\"{self.parser.loc['listen']}\\n\")\n\n    def _has_port_already(self, listens: list[str], port: str) -> Optional[bool]:\n        \"\"\"Helper method for prepare_server_https to find out if user\n        already has an active Listen statement for the port we need\n\n        :param list listens: List of listen variables\n        :param string port: Port in question\n        \"\"\"\n\n        if port in listens:\n            return True\n        # Check if Apache is already listening on a specific IP\n        for listen in listens:\n            if len(listen.split(\":\")) > 1:\n                # Ugly but takes care of protocol def, eg: 1.1.1.1:443 https\n                if listen.split(\":\")[-1].split(\" \")[0] == port:\n                    return True\n        return None\n\n    def prepare_https_modules(self, temp: bool) -> None:\n        \"\"\"Helper method for prepare_server_https, taking care of enabling\n        needed modules\n\n        :param boolean temp: If the change is temporary\n        \"\"\"\n\n        if self.options.handle_modules:\n            if \"socache_shmcb_module\" not in self.parser.modules:\n                self.enable_mod(\"socache_shmcb\", temp=temp)\n            if \"ssl_module\" not in self.parser.modules:\n                self.enable_mod(\"ssl\", temp=temp)\n                # Make sure we're not throwing away any unwritten changes to the config\n                self.parser.ensure_augeas_state()\n                self.parser.aug.load()\n                self.parser.reset_modules() # Reset to load the new ssl_module path\n                # Call again because now we can gate on openssl version\n                self.install_ssl_options_conf(self.mod_ssl_conf,\n                                              self.updated_mod_ssl_conf_digest,\n                                              warn_on_no_mod_ssl=True)\n\n    def make_vhost_ssl(self, nonssl_vhost: obj.VirtualHost) -> obj.VirtualHost:\n        \"\"\"Makes an ssl_vhost version of a nonssl_vhost.\n\n        Duplicates vhost and adds default ssl options\n        New vhost will reside as (nonssl_vhost.path) +\n        ``self.options.le_vhost_ext``\n\n        .. note:: This function saves the configuration\n\n        :param nonssl_vhost: Valid VH that doesn't have SSLEngine on\n        :type nonssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :returns: SSL vhost\n        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises .errors.PluginError: If more than one virtual host is in\n            the file or if plugin is unable to write/read vhost files.\n\n        \"\"\"\n        avail_fp = nonssl_vhost.filep\n        ssl_fp = self._get_ssl_vhost_path(avail_fp)\n\n        orig_matches = self.parser.aug.match(\"/files%s//* [label()=~regexp('%s')]\" %\n                                             (self._escape(ssl_fp),\n                                              parser.case_i(\"VirtualHost\")))\n\n        self._copy_create_ssl_vhost_skeleton(nonssl_vhost, ssl_fp)\n\n        # Reload augeas to take into account the new vhost\n        self.parser.aug.load()\n        # Get Vhost augeas path for new vhost\n        new_matches = self.parser.aug.match(\"/files%s//* [label()=~regexp('%s')]\" %\n                                            (self._escape(ssl_fp),\n                                             parser.case_i(\"VirtualHost\")))\n\n        vh_p = self._get_new_vh_path(orig_matches, new_matches)\n\n        if not vh_p:\n            # The vhost was not found on the currently parsed paths\n            # Make Augeas aware of the new vhost\n            self.parser.parse_file(ssl_fp)\n            # Try to search again\n            new_matches = self.parser.aug.match(\n                \"/files%s//* [label()=~regexp('%s')]\" %\n                (self._escape(ssl_fp),\n                 parser.case_i(\"VirtualHost\")))\n            vh_p = self._get_new_vh_path(orig_matches, new_matches)\n            if not vh_p:\n                raise errors.PluginError(\n                    \"Could not reverse map the HTTPS VirtualHost to the original\")\n\n        # Update Addresses\n        self._update_ssl_vhosts_addrs(vh_p)\n\n        # Log actions and create save notes\n        logger.info(\"Created an SSL vhost at %s\", ssl_fp)\n        self.save_notes += \"Created ssl vhost at %s\\n\" % ssl_fp\n        self.save()\n\n        # We know the length is one because of the assertion above\n        # Create the Vhost object\n        vhost = self._create_vhost(vh_p)\n        if not vhost:\n            raise errors.Error(\"Could not create a vhost\")  # pragma: no cover\n        ssl_vhost: obj.VirtualHost = vhost\n        ssl_vhost.ancestor = nonssl_vhost\n\n        self.vhosts.append(ssl_vhost)\n\n        # NOTE: Searches through Augeas seem to ruin changes to directives\n        #       The configuration must also be saved before being searched\n        #       for the new directives; For these reasons... this is tacked\n        #       on after fully creating the new vhost\n\n        return ssl_vhost\n\n    def _get_new_vh_path(self, orig_matches: list[str], new_matches: list[str]) -> Optional[str]:\n        \"\"\" Helper method for make_vhost_ssl for matching augeas paths. Returns\n        VirtualHost path from new_matches that's not present in orig_matches.\n\n        Paths are normalized, because augeas leaves indices out for paths\n        with only single directive with a similar key \"\"\"\n\n        orig_matches = [i.replace(\"[1]\", \"\") for i in orig_matches]\n        for match in new_matches:\n            if match.replace(\"[1]\", \"\") not in orig_matches:\n                # Return the unmodified path\n                return match\n        return None\n\n    def _get_ssl_vhost_path(self, non_ssl_vh_fp: str) -> str:\n        \"\"\" Get a file path for SSL vhost, uses user defined path as priority,\n        but if the value is invalid or not defined, will fall back to non-ssl\n        vhost filepath.\n\n        :param str non_ssl_vh_fp: Filepath of non-SSL vhost\n\n        :returns: Filepath for SSL vhost\n        :rtype: str\n        \"\"\"\n\n        if self.conf(\"vhost-root\") and os.path.exists(self.conf(\"vhost-root\")):\n            fp = os.path.join(filesystem.realpath(self.options.vhost_root),\n                              os.path.basename(non_ssl_vh_fp))\n        else:\n            # Use non-ssl filepath\n            fp = filesystem.realpath(non_ssl_vh_fp)\n\n        if fp.endswith(\".conf\"):\n            return fp[:-(len(\".conf\"))] + self.options.le_vhost_ext\n        return fp + self.options.le_vhost_ext\n\n    def _sift_rewrite_rule(self, line: str) -> bool:\n        \"\"\"Decides whether a line should be copied to a SSL vhost.\n\n        A canonical example of when sifting a line is required:\n        When the http vhost contains a RewriteRule that unconditionally\n        redirects any request to the https version of the same site.\n        e.g:\n        RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,QSA,R=permanent]\n        Copying the above line to the ssl vhost would cause a\n        redirection loop.\n\n        :param str line: a line extracted from the http vhost.\n\n        :returns: True - don't copy line from http vhost to SSL vhost.\n        :rtype: bool\n\n        \"\"\"\n        if not line.lower().lstrip().startswith(\"rewriterule\"):\n            return False\n\n        # According to: https://httpd.apache.org/docs/2.4/rewrite/flags.html\n        # The syntax of a RewriteRule is:\n        # RewriteRule pattern target [Flag1,Flag2,Flag3]\n        # i.e. target is required, so it must exist.\n        target = line.split()[2].strip()\n\n        # target may be surrounded with quotes\n        if target[0] in (\"'\", '\"') and target[0] == target[-1]:\n            target = target[1:-1]\n\n        # Sift line if it redirects the request to a HTTPS site\n        return target.startswith(\"https://\")\n\n    def _copy_create_ssl_vhost_skeleton(self, vhost: obj.VirtualHost, ssl_fp: str) -> None:\n        \"\"\"Copies over existing Vhost with IfModule mod_ssl.c> skeleton.\n\n        :param obj.VirtualHost vhost: Original VirtualHost object\n        :param str ssl_fp: Full path where the new ssl_vhost will reside.\n\n        A new file is created on the filesystem.\n\n        \"\"\"\n        # First register the creation so that it is properly removed if\n        # configuration is rolled back\n        if os.path.exists(ssl_fp):\n            notes = \"Appended new VirtualHost directive to file %s\" % ssl_fp\n            files = set()\n            files.add(ssl_fp)\n            self.reverter.add_to_checkpoint(files, notes)\n        else:\n            self.reverter.register_file_creation(False, ssl_fp)\n        sift: bool\n\n        try:\n            orig_contents = self._get_vhost_block(vhost)\n            ssl_vh_contents, sift = self._sift_rewrite_rules(orig_contents)\n\n            with open(ssl_fp, \"a\") as new_file:\n                new_file.write(\"<IfModule mod_ssl.c>\\n\")\n                new_file.write(\"\\n\".join(ssl_vh_contents))\n                # The content does not include the closing tag, so add it\n                new_file.write(\"</VirtualHost>\\n\")\n                new_file.write(\"</IfModule>\\n\")\n            # Add new file to augeas paths if we're supposed to handle\n            # activation (it's not included as default)\n            if not self.parser.parsed_in_current(ssl_fp):\n                self.parser.parse_file(ssl_fp)\n        except OSError:\n            logger.critical(\"Error writing/reading to file in make_vhost_ssl\", exc_info=True)\n            raise errors.PluginError(\"Unable to write/read in make_vhost_ssl\")\n\n        if sift:\n            display_util.notify(\n                f\"Some rewrite rules copied from {vhost.filep} were disabled in the \"\n                f\"vhost for your HTTPS site located at {ssl_fp} because they have \"\n                \"the potential to create redirection loops.\"\n            )\n        self.parser.aug.set(\"/augeas/files%s/mtime\" % (self._escape(ssl_fp)), \"0\")\n        self.parser.aug.set(\"/augeas/files%s/mtime\" % (self._escape(vhost.filep)), \"0\")\n\n    def _sift_rewrite_rules(self, contents: Iterable[str]) -> tuple[list[str], bool]:\n        \"\"\" Helper function for _copy_create_ssl_vhost_skeleton to prepare the\n        new HTTPS VirtualHost contents. Currently disabling the rewrites \"\"\"\n\n        result: list[str] = []\n        sift: bool = False\n        contents = iter(contents)\n\n        comment = (\"# Some rewrite rules in this file were \"\n                   \"disabled on your HTTPS site,\\n\"\n                   \"# because they have the potential to create \"\n                   \"redirection loops.\\n\")\n\n        for line in contents:\n            A = line.lower().lstrip().startswith(\"rewritecond\")\n            B = line.lower().lstrip().startswith(\"rewriterule\")\n\n            if not (A or B):\n                result.append(line)\n                continue\n\n            # A RewriteRule that doesn't need filtering\n            if B and not self._sift_rewrite_rule(line):\n                result.append(line)\n                continue\n\n            # A RewriteRule that does need filtering\n            if B and self._sift_rewrite_rule(line):\n                if not sift:\n                    result.append(comment)\n                    sift = True\n                result.append(\"# \" + line)\n                continue\n\n            # We save RewriteCond(s) and their corresponding\n            # RewriteRule in 'chunk'.\n            # We then decide whether we comment out the entire\n            # chunk based on its RewriteRule.\n            chunk = []\n            if A:\n                chunk.append(line)\n                line = next(contents)\n\n                # RewriteCond(s) must be followed by one RewriteRule\n                while not line.lower().lstrip().startswith(\"rewriterule\"):\n                    chunk.append(line)\n                    line = next(contents)\n\n                # Now, current line must start with a RewriteRule\n                chunk.append(line)\n\n                if self._sift_rewrite_rule(line):\n                    if not sift:\n                        result.append(comment)\n                        sift = True\n\n                    result.append('\\n'.join('# ' + l for l in chunk))\n                else:\n                    result.append('\\n'.join(chunk))\n        return result, sift\n\n    def _get_vhost_block(self, vhost: obj.VirtualHost) -> list[str]:\n        \"\"\" Helper method to get VirtualHost contents from the original file.\n        This is done with help of augeas span, which returns the span start and\n        end positions\n\n        :returns: `list` of VirtualHost block content lines without closing tag\n        \"\"\"\n\n        try:\n            span_val = self.parser.aug.span(vhost.path)\n        except ValueError:\n            logger.critical(\"Error while reading the VirtualHost %s from \"\n                            \"file %s\", vhost.name, vhost.filep, exc_info=True)\n            raise errors.PluginError(\"Unable to read VirtualHost from file\")\n        span_filep = span_val[0]\n        span_start = span_val[5]\n        span_end = span_val[6]\n        with open(span_filep, 'r') as fh:\n            fh.seek(span_start)\n            vh_contents = fh.read(span_end-span_start).split(\"\\n\")\n        self._remove_closing_vhost_tag(vh_contents)\n        return vh_contents\n\n    def _remove_closing_vhost_tag(self, vh_contents: list[str]) -> None:\n        \"\"\"Removes the closing VirtualHost tag if it exists.\n\n        This method modifies vh_contents directly to remove the closing\n        tag. If the closing vhost tag is found, everything on the line\n        after it is also removed. Whether or not this tag is included\n        in the result of span depends on the Augeas version.\n\n        :param list vh_contents: VirtualHost block contents to check\n\n        \"\"\"\n        for offset, line in enumerate(reversed(vh_contents)):\n            if line:\n                line_index = line.lower().find(\"</virtualhost>\")\n                if line_index != -1:\n                    content_index = len(vh_contents) - offset - 1\n                    vh_contents[content_index] = line[:line_index]\n                break\n\n    def _update_ssl_vhosts_addrs(self, vh_path: str) -> set[obj.Addr]:\n        ssl_addrs: set[obj.Addr] = set()\n        ssl_addr_p: list[str] = self.parser.aug.match(vh_path + \"/arg\")\n\n        for addr in ssl_addr_p:\n            old_addr = obj.Addr.fromstring(\n                str(self.parser.get_arg(addr)))\n            ssl_addr = old_addr.get_addr_obj(\"443\")\n            self.parser.aug.set(addr, str(ssl_addr))\n            ssl_addrs.add(ssl_addr)\n\n        return ssl_addrs\n\n    def _clean_vhost(self, vhost: obj.VirtualHost) -> None:\n        # remove duplicated or conflicting ssl directives\n        self._deduplicate_directives(vhost.path,\n                                     [\"SSLCertificateFile\",\n                                      \"SSLCertificateKeyFile\"])\n        # remove all problematic directives\n        self._remove_directives(vhost.path, [\"SSLCertificateChainFile\"])\n\n    def _deduplicate_directives(self, vh_path: Optional[str], directives: list[str]) -> None:\n        for directive in directives:\n            while len(self.parser.find_dir(directive, None,\n                                           vh_path, False)) > 1:\n                directive_path = self.parser.find_dir(directive, None,\n                                                      vh_path, False)\n                self.parser.aug.remove(re.sub(r\"/\\w*$\", \"\", directive_path[0]))\n\n    def _remove_directives(self, vh_path: Optional[str], directives: list[str]) -> None:\n        for directive in directives:\n            while self.parser.find_dir(directive, None, vh_path, False):\n                directive_path = self.parser.find_dir(directive, None,\n                                                      vh_path, False)\n                self.parser.aug.remove(re.sub(r\"/\\w*$\", \"\", directive_path[0]))\n\n    def _add_dummy_ssl_directives(self, vh_path: str) -> None:\n        self.parser.add_dir(vh_path, \"SSLCertificateFile\",\n                            \"insert_cert_file_path\")\n        self.parser.add_dir(vh_path, \"SSLCertificateKeyFile\",\n                            \"insert_key_file_path\")\n        # Only include the TLS configuration if not already included\n        existing_inc = self.parser.find_dir(\"Include\", self.mod_ssl_conf, vh_path)\n        if not existing_inc:\n            self.parser.add_dir(vh_path, \"Include\", self.mod_ssl_conf)\n\n    def _add_servername_alias(self, target_name: str, vhost: obj.VirtualHost) -> None:\n        vh_path = vhost.path\n        sname, saliases = self._get_vhost_names(vh_path)\n        if target_name == sname or target_name in saliases:\n            return\n        if self._has_matching_wildcard(vh_path, target_name):\n            return\n        if not self.parser.find_dir(\"ServerName\", None,\n                                    start=vh_path, exclude=False):\n            self.parser.add_dir(vh_path, \"ServerName\", target_name)\n        else:\n            self.parser.add_dir(vh_path, \"ServerAlias\", target_name)\n        self._add_servernames(vhost)\n\n    def _has_matching_wildcard(self, vh_path: str, target_name: str) -> bool:\n        \"\"\"Is target_name already included in a wildcard in the vhost?\n\n        :param str vh_path: Augeas path to the vhost\n        :param str target_name: name to compare with wildcards\n\n        :returns: True if there is a wildcard covering target_name in\n            the vhost in vhost_path, otherwise, False\n        :rtype: bool\n\n        \"\"\"\n        matches = self.parser.find_dir(\n            \"ServerAlias\", start=vh_path, exclude=False)\n        aliases = (self.parser.aug.get(match) for match in matches)\n        return self.domain_in_names(aliases, target_name)\n\n    def find_vhost_by_id(self, id_str: str) -> obj.VirtualHost:\n        \"\"\"\n        Searches through VirtualHosts and tries to match the id in a comment\n\n        :param str id_str: Id string for matching\n\n        :returns: The matched VirtualHost\n        :rtype: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises .errors.PluginError: If no VirtualHost is found\n        \"\"\"\n\n        for vh in self.vhosts:\n            if self._find_vhost_id(vh) == id_str:\n                return vh\n        msg = \"No VirtualHost with ID {} was found.\".format(id_str)\n        logger.warning(msg)\n        raise errors.PluginError(msg)\n\n    def _find_vhost_id(self, vhost: obj.VirtualHost) -> Optional[str]:\n        \"\"\"Tries to find the unique ID from the VirtualHost comments. This is\n        used for keeping track of VirtualHost directive over time.\n\n        :param vhost: Virtual host to add the id\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :returns: The unique ID or None\n        :rtype: str or None\n        \"\"\"\n\n        # Strip the {} off from the format string\n        search_comment = constants.MANAGED_COMMENT_ID.format(\"\")\n\n        id_comment = self.parser.find_comments(search_comment, vhost.path)\n        if id_comment:\n            # Use the first value, multiple ones shouldn't exist\n            comment = self.parser.get_arg(id_comment[0])\n            return comment.split(\" \")[-1] if comment else None\n        return None\n\n    def add_vhost_id(self, vhost: obj.VirtualHost) -> Optional[str]:\n        \"\"\"Adds an unique ID to the VirtualHost as a comment for mapping back\n        to it on later invocations, as the config file order might have changed.\n        If ID already exists, returns that instead.\n\n        :param vhost: Virtual host to add or find the id\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :returns: The unique ID for vhost\n        :rtype: str or None\n        \"\"\"\n\n        vh_id = self._find_vhost_id(vhost)\n        if vh_id:\n            return vh_id\n\n        id_string = apache_util.unique_id()\n        comment: str = constants.MANAGED_COMMENT_ID.format(id_string)\n        self.parser.add_comment(vhost.path, comment)\n        return id_string\n\n    def _escape(self, fp: str) -> str:\n        fp = fp.replace(\",\", \"\\\\,\")\n        fp = fp.replace(\"[\", \"\\\\[\")\n        fp = fp.replace(\"]\", \"\\\\]\")\n        fp = fp.replace(\"|\", \"\\\\|\")\n        fp = fp.replace(\"=\", \"\\\\=\")\n        fp = fp.replace(\"(\", \"\\\\(\")\n        fp = fp.replace(\")\", \"\\\\)\")\n        fp = fp.replace(\"!\", \"\\\\!\")\n        return fp\n\n    ######################################################################\n    # Enhancements\n    ######################################################################\n    def supported_enhancements(self) -> list[str]:\n        \"\"\"Returns currently supported enhancements.\"\"\"\n        return [\"redirect\", \"ensure-http-header\", \"staple-ocsp\"]\n\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[list[str], str]] = None) -> None:\n        \"\"\"Enhance configuration.\n\n        :param str domain: domain to enhance\n        :param str enhancement: enhancement type defined in\n            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n        :param options: options for the enhancement\n            See :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n            documentation for appropriate parameter.\n\n        :raises .errors.PluginError: If Enhancement is not supported, or if\n            there is any other problem with the enhancement.\n\n        \"\"\"\n        try:\n            func = self._enhance_func[enhancement]\n        except KeyError:\n            raise errors.PluginError(\n                \"Unsupported enhancement: {0}\".format(enhancement))\n\n        matched_vhosts = self.choose_vhosts(domain, create_if_no_ssl=False)\n        # We should be handling only SSL vhosts for enhancements\n        vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]\n\n        if not vhosts:\n            msg_tmpl = (\"Certbot was not able to find SSL VirtualHost for a \"\n                        \"domain {0} for enabling enhancement \\\"{1}\\\". The requested \"\n                        \"enhancement was not configured.\")\n            msg_enhancement = enhancement\n            if options:\n                msg_enhancement += \": \" + str(options)\n            msg = msg_tmpl.format(domain, msg_enhancement)\n            logger.error(msg)\n            raise errors.PluginError(msg)\n        try:\n            for vhost in vhosts:\n                func(vhost, options)\n        except errors.PluginError:\n            logger.error(\"Failed %s for %s\", enhancement, domain)\n            raise\n\n    def _autohsts_increase(\n        self, vhost: obj.VirtualHost, id_str: str, nextstep: int\n    ) -> None:\n        \"\"\"Increase the AutoHSTS max-age value\n\n        :param vhost: Virtual host object to modify\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :param str id_str: The unique ID string of VirtualHost\n\n        :param int nextstep: Next AutoHSTS max-age value index\n\n        \"\"\"\n        nextstep_value = constants.AUTOHSTS_STEPS[nextstep]\n        self._autohsts_write(vhost, nextstep_value)\n        self._autohsts[id_str] = {\"laststep\": nextstep, \"timestamp\": time.time()}\n\n    def _autohsts_write(self, vhost: obj.VirtualHost, nextstep_value: float) -> None:\n        \"\"\"\n        Write the new HSTS max-age value to the VirtualHost file\n        \"\"\"\n\n        hsts_dirpath = None\n        header_path = self.parser.find_dir(\"Header\", None, vhost.path)\n        if header_path:\n            pat = '(?:[ \"]|^)(strict-transport-security)(?:[ \"]|$)'\n            for match in header_path:\n                if re.search(pat, self.parser.aug.get(match).lower()):\n                    hsts_dirpath = match\n        if not hsts_dirpath:\n            err_msg = (\"Certbot was unable to find the existing HSTS header \"\n                       \"from the VirtualHost at path {0}.\").format(vhost.filep)\n            raise errors.PluginError(err_msg)\n\n        # Prepare the HSTS header value\n        hsts_maxage = \"\\\"max-age={0}\\\"\".format(nextstep_value)\n\n        # Update the header\n        # Our match statement was for string strict-transport-security, but\n        # we need to update the value instead. The next index is for the value\n        hsts_dirpath = hsts_dirpath.replace(\"arg[3]\", \"arg[4]\")\n        self.parser.aug.set(hsts_dirpath, hsts_maxage)\n        note_msg = (\"Increasing HSTS max-age value to {0} for VirtualHost \"\n                    \"in {1}\\n\".format(nextstep_value, vhost.filep))\n        logger.debug(note_msg)\n        self.save_notes += note_msg\n        self.save(note_msg)\n\n    def _autohsts_fetch_state(self) -> None:\n        \"\"\"\n        Populates the AutoHSTS state from the pluginstorage\n        \"\"\"\n        try:\n            self._autohsts = self.storage.fetch(\"autohsts\")\n        except KeyError:\n            self._autohsts = {}\n\n    def _autohsts_save_state(self) -> None:\n        \"\"\"\n        Saves the state of AutoHSTS object to pluginstorage\n        \"\"\"\n        self.storage.put(\"autohsts\", self._autohsts)\n        self.storage.save()\n\n    def _autohsts_vhost_in_lineage(\n        self, vhost: obj.VirtualHost, lineage: RenewableCert\n    ) -> bool:\n        \"\"\"\n        Searches AutoHSTS managed VirtualHosts that belong to the lineage.\n        Matches the private key path.\n        \"\"\"\n        return bool(self.parser.find_dir(\"SSLCertificateKeyFile\", lineage.key_path, vhost.path))\n\n    def _enable_ocsp_stapling(self, ssl_vhost: obj.VirtualHost, unused_options: Any) -> None:\n        \"\"\"Enables OCSP Stapling\n\n        In OCSP, each client (e.g. browser) would have to query the\n        OCSP Responder to validate that the site certificate was not revoked.\n\n        Enabling OCSP Stapling, would allow the web-server to query the OCSP\n        Responder, and staple its response to the offered certificate during\n        TLS. i.e. clients would not have to query the OCSP responder.\n\n        OCSP Stapling enablement on Apache implicitly depends on\n        SSLCertificateChainFile being set by other code.\n\n        .. note:: This function saves the configuration\n\n        :param ssl_vhost: Destination of traffic, an ssl enabled vhost\n        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :param unused_options: Not currently used\n        :type unused_options: Not Available\n        \"\"\"\n        if \"socache_shmcb_module\" not in self.parser.modules:\n            self.enable_mod(\"socache_shmcb\")\n\n        # Check if there's an existing SSLUseStapling directive on.\n        use_stapling_aug_path = self.parser.find_dir(\"SSLUseStapling\",\n                                                     \"on\", start=ssl_vhost.path)\n        if not use_stapling_aug_path:\n            self.parser.add_dir(ssl_vhost.path, \"SSLUseStapling\", \"on\")\n\n        ssl_vhost_aug_path = self._escape(parser.get_aug_path(ssl_vhost.filep))\n\n        # Check if there's an existing SSLStaplingCache directive.\n        stapling_cache_aug_path = self.parser.find_dir('SSLStaplingCache',\n                                                       None, ssl_vhost_aug_path)\n\n        # We'll simply delete the directive, so that we'll have a\n        # consistent OCSP cache path.\n        if stapling_cache_aug_path:\n            self.parser.aug.remove(\n                re.sub(r\"/\\w*$\", \"\", stapling_cache_aug_path[0]))\n\n        self.parser.add_dir_to_ifmodssl(ssl_vhost_aug_path,\n                                        \"SSLStaplingCache\",\n                                        [\"shmcb:/var/run/apache2/stapling_cache(128000)\"])\n\n        msg = \"OCSP Stapling was enabled on SSL Vhost: %s.\\n\"%(\n            ssl_vhost.filep)\n        self.save_notes += msg\n        self.save()\n        logger.info(msg)\n\n    def _set_http_header(self, ssl_vhost: obj.VirtualHost, header_substring: str) -> None:\n        \"\"\"Enables header that is identified by header_substring on ssl_vhost.\n\n        If the header identified by header_substring is not already set,\n        a new Header directive is placed in ssl_vhost's configuration with\n        arguments from: constants.HTTP_HEADER[header_substring]\n\n        .. note:: This function saves the configuration\n\n        :param ssl_vhost: Destination of traffic, an ssl enabled vhost\n        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :param header_substring: string that uniquely identifies a header.\n                e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.\n        :type str\n\n        :raises .errors.PluginError: If no viable HTTP host can be created or\n            set with header header_substring.\n\n        \"\"\"\n        if \"headers_module\" not in self.parser.modules:\n            self.enable_mod(\"headers\")\n\n        # Check if selected header is already set\n        self._verify_no_matching_http_header(ssl_vhost, header_substring)\n\n        # Add directives to server\n        self.parser.add_dir(ssl_vhost.path, \"Header\",\n                            constants.HEADER_ARGS[header_substring])\n\n        self.save_notes += (\"Adding %s header to ssl vhost in %s\\n\" %\n                            (header_substring, ssl_vhost.filep))\n\n        self.save()\n        logger.info(\"Adding %s header to ssl vhost in %s\", header_substring,\n                    ssl_vhost.filep)\n\n    def _verify_no_matching_http_header(\n        self, ssl_vhost: obj.VirtualHost, header_substring: str\n    ) -> None:\n        \"\"\"Checks to see if there is an existing Header directive that\n        contains the string header_substring.\n\n        :param ssl_vhost: vhost to check\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :param header_substring: string that uniquely identifies a header.\n                e.g: Strict-Transport-Security, Upgrade-Insecure-Requests.\n        :type str\n\n        :returns: boolean\n        :rtype: (bool)\n\n        :raises errors.PluginEnhancementAlreadyPresent When header\n                header_substring exists\n\n        \"\"\"\n        header_path = self.parser.find_dir(\"Header\", None,\n                                           start=ssl_vhost.path)\n        if header_path:\n            # \"Existing Header directive for virtualhost\"\n            pat = '(?:[ \"]|^)(%s)(?:[ \"]|$)' % (header_substring.lower())\n            for match in header_path:\n                if re.search(pat, self.parser.aug.get(match).lower()):\n                    raise errors.PluginEnhancementAlreadyPresent(\n                        \"Existing %s header\" % header_substring)\n\n    def _enable_redirect(self, ssl_vhost: obj.VirtualHost, unused_options: Any) -> None:\n        \"\"\"Redirect all equivalent HTTP traffic to ssl_vhost.\n\n        .. todo:: This enhancement should be rewritten and will\n           unfortunately require lots of debugging by hand.\n\n        Adds Redirect directive to the port 80 equivalent of ssl_vhost\n        First the function attempts to find the vhost with equivalent\n        ip addresses that serves on non-ssl ports\n        The function then adds the directive\n\n        .. note:: This function saves the configuration\n\n        :param ssl_vhost: Destination of traffic, an ssl enabled vhost\n        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :param unused_options: Not currently used\n        :type unused_options: Not Available\n\n        :raises .errors.PluginError: If no viable HTTP host can be created or\n            used for the redirect.\n\n        \"\"\"\n        if \"rewrite_module\" not in self.parser.modules:\n            self.enable_mod(\"rewrite\")\n        general_vh = self._get_http_vhost(ssl_vhost)\n\n        if general_vh is None:\n            # Add virtual_server with redirect\n            logger.debug(\"Did not find http version of ssl virtual host \"\n                         \"attempting to create\")\n            redirect_addrs = self._get_proposed_addrs(ssl_vhost)\n            for vhost in self.vhosts:\n                if vhost.enabled and vhost.conflicts(redirect_addrs):\n                    raise errors.PluginError(\n                        \"Unable to find corresponding HTTP vhost; \"\n                        \"Unable to create one as intended addresses conflict; \"\n                        \"Current configuration does not support automated \"\n                        \"redirection\")\n            self._create_redirect_vhost(ssl_vhost)\n        else:\n            if general_vh in self._enhanced_vhosts[\"redirect\"]:\n                logger.debug(\"Already enabled redirect for this vhost\")\n                return\n\n            # Check if Certbot redirection already exists\n            self._verify_no_certbot_redirect(general_vh)\n\n            # Note: if code flow gets here it means we didn't find the exact\n            # certbot RewriteRule config for redirection. Finding\n            # another RewriteRule is likely to be fine in most or all cases,\n            # but redirect loops are possible in very obscure cases; see #1620\n            # for reasoning.\n            if self._is_rewrite_exists(general_vh):\n                logger.warning(\"Added an HTTP->HTTPS rewrite in addition to \"\n                               \"other RewriteRules; you may wish to check for \"\n                               \"overall consistency.\")\n\n            # Add directives to server\n            # Note: These are not immediately searchable in sites-enabled\n            #     even with save() and load()\n            if not self._is_rewrite_engine_on(general_vh):\n                self.parser.add_dir(general_vh.path, \"RewriteEngine\", \"on\")\n\n            names = ssl_vhost.get_names()\n            for idx, name in enumerate(names):\n                args = [\"%{SERVER_NAME}\", \"={0}\".format(name), \"[OR]\"]\n                if idx == len(names) - 1:\n                    args.pop()\n                self.parser.add_dir(general_vh.path, \"RewriteCond\", args)\n\n            self._set_https_redirection_rewrite_rule(general_vh)\n\n            self.save_notes += (\"Redirecting host in %s to ssl vhost in %s\\n\" %\n                                (general_vh.filep, ssl_vhost.filep))\n            self.save()\n\n            self._enhanced_vhosts[\"redirect\"].add(general_vh)\n            logger.info(\"Redirecting vhost in %s to ssl vhost in %s\",\n                        general_vh.filep, ssl_vhost.filep)\n\n    def _set_https_redirection_rewrite_rule(self, vhost: obj.VirtualHost) -> None:\n        self.parser.add_dir(vhost.path, \"RewriteRule\", constants.REWRITE_HTTPS_ARGS)\n\n    def _verify_no_certbot_redirect(self, vhost: obj.VirtualHost) -> None:\n        \"\"\"Checks to see if a redirect was already installed by certbot.\n\n        Checks to see if virtualhost already contains a rewrite rule that is\n        identical to Certbot's redirection rewrite rule.\n\n        For graceful transition to new rewrite rules for HTTPS redireciton we\n        delete certbot's old rewrite rules and set the new one instead.\n\n        :param vhost: vhost to check\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises errors.PluginEnhancementAlreadyPresent: When the exact\n                certbot redirection WriteRule exists in virtual host.\n        \"\"\"\n        rewrite_path = self.parser.find_dir(\n            \"RewriteRule\", None, start=vhost.path)\n\n        # There can be other RewriteRule directive lines in vhost config.\n        # rewrite_args_dict keys are directive ids and the corresponding value\n        # for each is a list of arguments to that directive.\n        rewrite_args_dict: defaultdict[str, list[str]] = defaultdict(list)\n        pat = r'(.*directive\\[\\d+\\]).*'\n        for match in rewrite_path:\n            m = re.match(pat, match)\n            if m:\n                dir_path = m.group(1)\n                rewrite_args_dict[dir_path].append(match)\n\n        if rewrite_args_dict:\n            for dir_path, args_paths in rewrite_args_dict.items():\n                arg_vals = [self.parser.aug.get(x) for x in args_paths]\n\n                # Search for past redirection rule, delete it, set the new one\n                if arg_vals in constants.OLD_REWRITE_HTTPS_ARGS:\n                    self.parser.aug.remove(dir_path)\n                    self._set_https_redirection_rewrite_rule(vhost)\n                    self.save()\n                    raise errors.PluginEnhancementAlreadyPresent(\n                        \"Certbot has already enabled redirection\")\n\n                if arg_vals == constants.REWRITE_HTTPS_ARGS:\n                    raise errors.PluginEnhancementAlreadyPresent(\n                        \"Certbot has already enabled redirection\")\n\n    def _is_rewrite_exists(self, vhost: obj.VirtualHost) -> bool:\n        \"\"\"Checks if there exists a RewriteRule directive in vhost\n\n        :param vhost: vhost to check\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :returns: True if a RewriteRule directive exists.\n        :rtype: bool\n\n        \"\"\"\n        rewrite_path = self.parser.find_dir(\n            \"RewriteRule\", None, start=vhost.path)\n        return bool(rewrite_path)\n\n    def _is_rewrite_engine_on(self, vhost: obj.VirtualHost) -> Optional[Union[str, bool]]:\n        \"\"\"Checks if a RewriteEngine directive is on\n\n        :param vhost: vhost to check\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        \"\"\"\n        rewrite_engine_path_list = self.parser.find_dir(\"RewriteEngine\", \"on\", start=vhost.path)\n        if rewrite_engine_path_list:\n            for re_path in rewrite_engine_path_list:\n                # A RewriteEngine directive may also be included in per\n                # directory .htaccess files. We only care about the VirtualHost.\n                if 'virtualhost' in re_path.lower():\n                    return self.parser.get_arg(re_path)\n        return False\n\n    def _create_redirect_vhost(self, ssl_vhost: obj.VirtualHost) -> None:\n        \"\"\"Creates an http_vhost specifically to redirect for the ssl_vhost.\n\n        :param ssl_vhost: ssl vhost\n        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n        \"\"\"\n        text = self._get_redirect_config_str(ssl_vhost)\n\n        redirect_filepath = self._write_out_redirect(ssl_vhost, text)\n\n        self.parser.aug.load()\n        # Make a new vhost data structure and add it to the lists\n        new_vhost = self._create_vhost(parser.get_aug_path(\n            self._escape(redirect_filepath))\n        )\n        if new_vhost is None:\n            raise errors.Error(\"Could not create a new vhost\")  # pragma: no cover\n        self.vhosts.append(new_vhost)\n        self._enhanced_vhosts[\"redirect\"].add(new_vhost)\n\n        # Finally create documentation for the change\n        self.save_notes += (\"Created a port 80 vhost, %s, for redirection to \"\n                            \"ssl vhost %s\\n\" %\n                            (new_vhost.filep, ssl_vhost.filep))\n\n    def _get_redirect_config_str(self, ssl_vhost: obj.VirtualHost) -> str:\n        # get servernames and serveraliases\n        serveralias = \"\"\n        servername = \"\"\n\n        if ssl_vhost.name is not None:\n            servername = \"ServerName \" + ssl_vhost.name\n        if ssl_vhost.aliases:\n            serveralias = \"ServerAlias \" + \" \".join(ssl_vhost.aliases)\n\n        return (\n            f\"<VirtualHost {' '.join(str(addr) for addr in self._get_proposed_addrs(ssl_vhost))}>\\n\"\n            f\"{servername} \\n\"\n            f\"{serveralias} \\n\"\n            f\"ServerSignature Off\\n\"\n            f\"\\n\"\n            f\"RewriteEngine On\\n\"\n            f\"RewriteRule {' '.join(constants.REWRITE_HTTPS_ARGS)}\\n\"\n            \"\\n\"\n            f\"ErrorLog {self.options.logs_root}/redirect.error.log\\n\"\n            f\"LogLevel warn\\n\"\n            f\"</VirtualHost>\\n\"\n        )\n\n    def _write_out_redirect(self, ssl_vhost: obj.VirtualHost, text: str) -> str:\n        # This is the default name\n        redirect_filename = \"le-redirect.conf\"\n\n        # See if a more appropriate name can be applied\n        if ssl_vhost.name is not None:\n            # make sure servername doesn't exceed filename length restriction\n            if len(ssl_vhost.name) < (255 - (len(redirect_filename) + 1)):\n                redirect_filename = \"le-redirect-%s.conf\" % ssl_vhost.name\n\n        redirect_filepath = os.path.join(self.options.vhost_root,\n                                         redirect_filename)\n\n        # Register the new file that will be created\n        # Note: always register the creation before writing to ensure file will\n        # be removed in case of unexpected program exit\n        self.reverter.register_file_creation(False, redirect_filepath)\n\n        # Write out file\n        with open(redirect_filepath, \"w\") as redirect_file:\n            redirect_file.write(text)\n\n        # Add new include to configuration if it doesn't exist yet\n        if not self.parser.parsed_in_current(redirect_filepath):\n            self.parser.parse_file(redirect_filepath)\n\n        logger.info(\"Created redirect file: %s\", redirect_filename)\n\n        return redirect_filepath\n\n    def _get_http_vhost(self, ssl_vhost: obj.VirtualHost) -> Optional[obj.VirtualHost]:\n        \"\"\"Find appropriate HTTP vhost for ssl_vhost.\"\"\"\n        # First candidate vhosts filter\n        if ssl_vhost.ancestor:\n            return ssl_vhost.ancestor\n        candidate_http_vhs = [\n            vhost for vhost in self.vhosts if not vhost.ssl\n        ]\n\n        # Second filter - check addresses\n        for http_vh in candidate_http_vhs:\n            if http_vh.same_server(ssl_vhost):\n                return http_vh\n        # Third filter - if none with same names, return generic\n        for http_vh in candidate_http_vhs:\n            if http_vh.same_server(ssl_vhost, generic=True):\n                return http_vh\n\n        return None\n\n    def _get_proposed_addrs(self, vhost: obj.VirtualHost, port: str = \"80\") -> Iterable[obj.Addr]:\n        \"\"\"Return all addrs of vhost with the port replaced with the specified.\n\n        :param obj.VirtualHost ssl_vhost: Original Vhost\n        :param str port: Desired port for new addresses\n\n        :returns: `set` of :class:`~obj.Addr`\n\n        \"\"\"\n        redirects = set()\n        for addr in vhost.addrs:\n            redirects.add(addr.get_addr_obj(port))\n\n        return redirects\n\n    def enable_site(self, vhost: obj.VirtualHost) -> None:\n        \"\"\"Enables an available site, Apache reload required.\n\n        .. note:: Does not make sure that the site correctly works or that all\n                  modules are enabled appropriately.\n        .. note:: The distribution specific override replaces functionality\n                  of this method where available.\n\n        :param vhost: vhost to enable\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises .errors.NotSupportedError: If filesystem layout is not\n            supported.\n\n        \"\"\"\n        if vhost.enabled:\n            return\n\n        if not self.parser.parsed_in_original(vhost.filep):\n            # Add direct include to root conf\n            logger.info(\"Enabling site %s by adding Include to root configuration\",\n                        vhost.filep)\n            self.save_notes += \"Enabled site %s\\n\" % vhost.filep\n            self.parser.add_include(self.parser.loc[\"default\"], vhost.filep)\n            vhost.enabled = True\n        return\n\n    # pylint: disable=unused-argument\n    def enable_mod(self, mod_name: str, temp: bool = False) -> None:\n        \"\"\"Enables module in Apache.\n\n        Both enables and reloads Apache so module is active.\n\n        :param str mod_name: Name of the module to enable. (e.g. 'ssl')\n        :param bool temp: Whether or not this is a temporary action.\n\n        .. note:: The distribution specific override replaces functionality\n                  of this method where available.\n\n        :raises .errors.MisconfigurationError: We cannot enable modules in\n            generic fashion.\n\n        \"\"\"\n        mod_message = (\n            f\"Apache needs to have module  \\\"{mod_name}\\\" active for the \"\n            \"requested installation options. Unfortunately Certbot is unable \"\n            \"to install or enable it for you. Please install the module, and \"\n            \"run Certbot again.\"\n        )\n        raise errors.MisconfigurationError(mod_message)\n\n    def restart(self) -> None:\n        \"\"\"Runs a config test and reloads the Apache server.\n\n        :raises .errors.MisconfigurationError: If either the config test\n            or reload fails.\n\n        \"\"\"\n        self.config_test()\n        self._reload()\n\n    def _reload(self) -> None:\n        \"\"\"Reloads the Apache server.\n\n        :raises .errors.MisconfigurationError: If reload fails\n\n        \"\"\"\n        try:\n            util.run_script(self.options.restart_cmd)\n        except errors.SubprocessError as err:\n            logger.warning(\"Unable to restart apache using %s\",\n                        self.options.restart_cmd)\n            if self.options.restart_cmd_alt:\n                logger.debug(\"Trying alternative restart command: %s\",\n                             self.options.restart_cmd_alt)\n                # There is an alternative restart command available\n                # This usually is \"restart\" verb while original is \"graceful\"\n                try:\n                    util.run_script(self.options.restart_cmd_alt)\n                    return\n                except errors.SubprocessError as secerr:\n                    error = str(secerr)\n            else:\n                error = str(err)\n            raise errors.MisconfigurationError(error)\n\n    def config_test(self) -> None:\n        \"\"\"Check the configuration of Apache for errors.\n\n        :raises .errors.MisconfigurationError: If config_test fails\n\n        \"\"\"\n        try:\n            util.run_script(self.options.conftest_cmd)\n        except errors.SubprocessError as err:\n            raise errors.MisconfigurationError(str(err))\n\n    def get_version(self) -> tuple[int, ...]:\n        \"\"\"Return version of Apache Server.\n\n        Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))\n\n        :returns: version\n        :rtype: tuple\n\n        :raises .PluginError: if unable to find Apache version\n\n        \"\"\"\n        try:\n            stdout, _ = util.run_script(self.options.version_cmd)\n        except errors.SubprocessError:\n            raise errors.PluginError(\n                \"Unable to run %s -v\" %\n                self.options.version_cmd)\n\n        regex = re.compile(r\"Apache/([0-9\\.]*)\", re.IGNORECASE)\n        matches = regex.findall(stdout)\n\n        if len(matches) != 1:\n            raise errors.PluginError(\"Unable to find Apache version\")\n\n        return tuple(int(i) for i in matches[0].split(\".\"))\n\n    def more_info(self) -> str:\n        \"\"\"Human-readable string to help understand the module\"\"\"\n        return (\n            \"Configures Apache to authenticate and install HTTPS.{0}\"\n            \"Server root: {root}{0}\"\n            \"Version: {version}\".format(\n                os.linesep, root=self.parser.loc[\"root\"],\n                version=\".\".join(str(i) for i in self.version))\n        )\n\n    def auth_hint(\n        self, failed_achalls: Iterable[achallenges.AnnotatedChallenge]\n    ) -> str:  # pragma: no cover\n        return (\"The Certificate Authority failed to verify the temporary Apache configuration \"\n                \"changes made by Certbot. Ensure that the listed domains point to this Apache \"\n                \"server and that it is accessible from the internet.\")\n\n    ###########################################################################\n    # Challenges Section\n    ###########################################################################\n    def get_chall_pref(self, unused_identifier: str) -> Sequence[type[challenges.HTTP01]]:\n        \"\"\"Return list of challenge preferences.\"\"\"\n        return [challenges.HTTP01]\n\n    def perform(self, achalls: list[achallenges.AnnotatedChallenge]\n                ) -> list[challenges.ChallengeResponse]:\n        \"\"\"Perform the configuration related challenge.\n\n        This function currently assumes all challenges will be fulfilled.\n        If this turns out not to be the case in the future. Cleanup and\n        outstanding challenges will have to be designed better.\n\n        \"\"\"\n        self._chall_out.update(achalls)\n        responses: list[Optional[challenges.ChallengeResponse]] = [None] * len(achalls)\n        http_doer = http_01.ApacheHttp01(self)\n\n        for i, achall in enumerate(achalls):\n            if not isinstance(achall, achallenges.KeyAuthorizationAnnotatedChallenge):\n                raise errors.Error(\"Challenge should be an instance \"  # pragma: no cover\n                                   \"of KeyAuthorizationAnnotatedChallenge\")\n            # Currently also have chall_doer hold associated index of the\n            # challenge. This helps to put all of the responses back together\n            # when they are all complete.\n            http_doer.add_chall(achall, i)\n\n        http_response = http_doer.perform()\n        if http_response:\n            # Must reload in order to activate the challenges.\n            # Handled here because we may be able to load up other challenge\n            # types\n            self.restart()\n\n            # TODO: Remove this dirty hack. We need to determine a reliable way\n            #       of identifying when the new configuration is being used.\n            time.sleep(3)\n\n            self._update_responses(responses, http_response, http_doer)\n\n        return [response for response in responses if response]\n\n    def _update_responses(\n        self,\n        responses: list[Optional[challenges.ChallengeResponse]],\n        chall_response: list[challenges.KeyAuthorizationChallengeResponse],\n        chall_doer: http_01.ApacheHttp01\n    ) -> None:\n        # Go through all of the challenges and assign them to the proper\n        # place in the responses return value. All responses must be in the\n        # same order as the original challenges.\n        for i, resp in enumerate(chall_response):\n            responses[chall_doer.indices[i]] = resp\n\n    def cleanup(self, achalls: list[achallenges.AnnotatedChallenge]) -> None:\n        \"\"\"Revert all challenges.\"\"\"\n        self._chall_out.difference_update(achalls)\n\n        # If all of the challenges have been finished, clean up everything\n        if not self._chall_out:\n            self.revert_challenge_config()\n            self.restart()\n            self.parser.reset_modules()\n\n    def install_ssl_options_conf(\n        self, options_ssl: str, options_ssl_digest: str, warn_on_no_mod_ssl: bool = True\n    ) -> None:\n        \"\"\"Copy Certbot's SSL options file into the system's config dir if required.\n\n        :param bool warn_on_no_mod_ssl: True if we should warn if mod_ssl is not found.\n        \"\"\"\n\n        # XXX if we ever try to enforce a local privilege boundary (eg, running\n        # certbot for unprivileged users via setuid), this function will need\n        # to be modified.\n        apache_config_path = self.pick_apache_config(warn_on_no_mod_ssl)\n\n        return common.install_version_controlled_file(\n            options_ssl, options_ssl_digest, apache_config_path, constants.ALL_SSL_OPTIONS_HASHES)\n\n    def enable_autohsts(self, _unused_lineage: RenewableCert, domains: list[str]) -> None:\n        \"\"\"\n        Enable the AutoHSTS enhancement for defined domains\n\n        :param _unused_lineage: Certificate lineage object, unused\n        :type _unused_lineage: certbot._internal.storage.RenewableCert\n\n        :param domains: List of domains in certificate to enhance\n        :type domains: `list` of `str`\n        \"\"\"\n\n        self._autohsts_fetch_state()\n        _enhanced_vhosts: list[obj.VirtualHost] = []\n        for d in domains:\n            matched_vhosts = self.choose_vhosts(d, create_if_no_ssl=False)\n            # We should be handling only SSL vhosts for AutoHSTS\n            vhosts = [vhost for vhost in matched_vhosts if vhost.ssl]\n\n            if not vhosts:\n                msg_tmpl = (\"Certbot was not able to find SSL VirtualHost for a \"\n                            \"domain {0} for enabling AutoHSTS enhancement.\")\n                msg = msg_tmpl.format(d)\n                logger.error(msg)\n                raise errors.PluginError(msg)\n            for vh in vhosts:\n                try:\n                    self._enable_autohsts_domain(vh)\n                    _enhanced_vhosts.append(vh)\n                except errors.PluginEnhancementAlreadyPresent:\n                    if vh in _enhanced_vhosts:\n                        continue\n                    msg = (\"VirtualHost for domain {0} in file {1} has a \" +\n                           \"String-Transport-Security header present, exiting.\")\n                    raise errors.PluginEnhancementAlreadyPresent(\n                        msg.format(d, vh.filep))\n        if _enhanced_vhosts:\n            note_msg = \"Enabling AutoHSTS\"\n            self.save(note_msg)\n            logger.info(note_msg)\n            self.restart()\n\n        # Save the current state to pluginstorage\n        self._autohsts_save_state()\n\n    def _enable_autohsts_domain(self, ssl_vhost: obj.VirtualHost) -> None:\n        \"\"\"Do the initial AutoHSTS deployment to a vhost\n\n        :param ssl_vhost: The VirtualHost object to deploy the AutoHSTS\n        :type ssl_vhost: :class:`~certbot_apache._internal.obj.VirtualHost` or None\n\n        :raises errors.PluginEnhancementAlreadyPresent: When already enhanced\n\n        \"\"\"\n        # This raises the exception\n        self._verify_no_matching_http_header(ssl_vhost, \"Strict-Transport-Security\")\n\n        if \"headers_module\" not in self.parser.modules:\n            self.enable_mod(\"headers\")\n        # Prepare the HSTS header value\n        hsts_header = constants.HEADER_ARGS[\"Strict-Transport-Security\"][:-1]\n        initial_maxage = constants.AUTOHSTS_STEPS[0]\n        hsts_header.append(\"\\\"max-age={0}\\\"\".format(initial_maxage))\n\n        # Add ID to the VirtualHost for mapping back to it later\n        uniq_id = self.add_vhost_id(ssl_vhost)\n        if uniq_id is None:\n            raise errors.Error(\"Could not generate a unique id\")  # pragma: no cover\n        self.save_notes += \"Adding unique ID {0} to VirtualHost in {1}\\n\".format(\n            uniq_id, ssl_vhost.filep)\n        # Add the actual HSTS header\n        self.parser.add_dir(ssl_vhost.path, \"Header\", hsts_header)\n        note_msg = (\"Adding gradually increasing HSTS header with initial value \"\n                    \"of {0} to VirtualHost in {1}\\n\".format(\n            initial_maxage, ssl_vhost.filep))\n        self.save_notes += note_msg\n\n        # Save the current state to pluginstorage\n        self._autohsts[uniq_id] = {\"laststep\": 0, \"timestamp\": time.time()}\n\n    def update_autohsts(self, _unused_domain: str) -> None:\n        \"\"\"\n        Increase the AutoHSTS values of VirtualHosts that the user has enabled\n        this enhancement for.\n\n        :param _unused_domain: Not currently used\n        :type _unused_domain: Not Available\n\n        \"\"\"\n        self._autohsts_fetch_state()\n        if not self._autohsts:\n            # No AutoHSTS enabled for any domain\n            return\n        curtime = time.time()\n        save_and_restart = False\n        for id_str, config in list(self._autohsts.items()):\n            if config[\"timestamp\"] + constants.AUTOHSTS_FREQ > curtime:\n                # Skip if last increase was < AUTOHSTS_FREQ ago\n                continue\n            nextstep = cast(int, config[\"laststep\"]) + 1\n            if nextstep < len(constants.AUTOHSTS_STEPS):\n                # If installer hasn't been prepared yet, do it now\n                if not self._prepared:\n                    self.prepare()\n                # Have not reached the max value yet\n                try:\n                    vhost = self.find_vhost_by_id(id_str)\n                except errors.PluginError:\n                    msg = (\"Could not find VirtualHost with ID {0}, disabling \"\n                           \"AutoHSTS for this VirtualHost\").format(id_str)\n                    logger.error(msg)\n                    # Remove the orphaned AutoHSTS entry from pluginstorage\n                    self._autohsts.pop(id_str)\n                    continue\n                self._autohsts_increase(vhost, id_str, nextstep)\n                msg = (\"Increasing HSTS max-age value for VirtualHost with id \"\n                       \"{0}\").format(id_str)\n                self.save_notes += msg\n                save_and_restart = True\n\n        if save_and_restart:\n            self.save(\"Increased HSTS max-age values\")\n            self.restart()\n\n        self._autohsts_save_state()\n\n    def deploy_autohsts(self, lineage: RenewableCert) -> None:\n        \"\"\"\n        Checks if autohsts vhost has reached maximum auto-increased value\n        and changes the HSTS max-age to a high value.\n\n        :param lineage: Certificate lineage object\n        :type lineage: certbot._internal.storage.RenewableCert\n        \"\"\"\n        self._autohsts_fetch_state()\n        if not self._autohsts:\n            # No autohsts enabled for any vhost\n            return\n\n        vhosts: list[obj.VirtualHost] = []\n        affected_ids = []\n        # Copy, as we are removing from the dict inside the loop\n        for id_str, config in list(self._autohsts.items()):\n            if config[\"laststep\"]+1 >= len(constants.AUTOHSTS_STEPS):\n                # max value reached, try to make permanent\n                try:\n                    vhost: obj.VirtualHost = self.find_vhost_by_id(id_str)\n                except errors.PluginError:\n                    msg = (\"VirtualHost with id {} was not found, unable to \"\n                           \"make HSTS max-age permanent.\").format(id_str)\n                    logger.error(msg)\n                    self._autohsts.pop(id_str)\n                    continue\n                if self._autohsts_vhost_in_lineage(vhost, lineage):\n                    vhosts.append(vhost)\n                    affected_ids.append(id_str)\n\n        save_and_restart = False\n        for vhost in vhosts:\n            self._autohsts_write(vhost, constants.AUTOHSTS_PERMANENT)\n            msg = (\"Strict-Transport-Security max-age value for \"\n                   \"VirtualHost in {0} was made permanent.\").format(vhost.filep)\n            logger.debug(msg)\n            self.save_notes += msg+\"\\n\"\n            save_and_restart = True\n\n        if save_and_restart:\n            self.save(\"Made HSTS max-age permanent\")\n            self.restart()\n\n        for id_str in affected_ids:\n            self._autohsts.pop(id_str)\n\n        # Update AutoHSTS storage (We potentially removed vhosts from managed)\n        self._autohsts_save_state()\n\n\nAutoHSTSEnhancement.register(ApacheConfigurator)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/constants.py",
    "content": "\"\"\"Apache plugin constants.\"\"\"\nimport atexit\nimport importlib.resources\nfrom contextlib import ExitStack\n\n\nMOD_SSL_CONF_DEST = \"options-ssl-apache.conf\"\n\"\"\"Name of the mod_ssl config file as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\n\nUPDATED_MOD_SSL_CONF_DIGEST = \".updated-options-ssl-apache-conf-digest.txt\"\n\"\"\"Name of the hash of the updated or informed mod_ssl_conf as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\n# NEVER REMOVE A SINGLE HASH FROM THIS LIST UNLESS YOU KNOW EXACTLY WHAT YOU ARE DOING!\nALL_SSL_OPTIONS_HASHES: list[str] = [\n    '2086bca02db48daf93468332543c60ac6acdb6f0b58c7bfdf578a5d47092f82a',\n    '4844d36c9a0f587172d9fa10f4f1c9518e3bcfa1947379f155e16a70a728c21a',\n    '5a922826719981c0a234b1fbcd495f3213e49d2519e845ea0748ba513044b65b',\n    '4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27',\n    'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88',\n    'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b',\n    '80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791',\n    'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082',\n    '717b0a89f5e4c39b09a42813ac6e747cfbdeb93439499e73f4f70a1fe1473f20',\n    '0fcdc81280cd179a07ec4d29d3595068b9326b455c488de4b09f585d5dafc137',\n    '86cc09ad5415cd6d5f09a947fe2501a9344328b1e8a8b458107ea903e80baa6c',\n    '06675349e457eae856120cdebb564efe546f0b87399f2264baeb41e442c724c7',\n    '5cc003edd93fb9cd03d40c7686495f8f058f485f75b5e764b789245a386e6daf',\n    '007cd497a56a3bb8b6a2c1aeb4997789e7e38992f74e44cc5d13a625a738ac73',\n    '34783b9e2210f5c4a23bced2dfd7ec289834716673354ed7c7abf69fe30192a3',\n    '61466bc2f98a623c02be8a5ee916ead1655b0ce883bdc936692076ea499ff5ce',\n    '3fd812e3e87fe5c645d3682a511b2a06c8286f19594f28e280f17cd6af1301b5',\n    '27155797e160fe43b6951354a0a0ca4d829e9e605b3b41fc223c20bf2f6cb3c6',\n    '3a6881d0a7e5740b039ec550c916105259f53b577a3d38d0ed11bd675bfeab88',\n    '0f3d9c62d4274aca0406925dc4ee0919599c397e7463bce792a915b60060d004',\n    '95f7367d4905a1cd0932a35ce476b4a639e2108dbd1eedf924a5ea9e51fecaf7',\n]\n\"\"\"SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC\"\"\"\n\ndef _generate_augeas_lens_dir_static() -> str:\n    # This code ensures that the resource is accessible as file for the lifetime of current\n    # Python process, and will be automatically cleaned up on exit.\n    file_manager = ExitStack()\n    atexit.register(file_manager.close)\n    augeas_lens_dir_ref = importlib.resources.files(\"certbot_apache\") / \"_internal\" / \"augeas_lens\"\n    return str(file_manager.enter_context(importlib.resources.as_file(augeas_lens_dir_ref)))\n\nAUGEAS_LENS_DIR = _generate_augeas_lens_dir_static()\n\"\"\"Path to the Augeas lens directory\"\"\"\n\nREWRITE_HTTPS_ARGS: list[str] = [\n    \"^\", \"https://%{SERVER_NAME}%{REQUEST_URI}\", \"[END,NE,R=permanent]\"]\n\"\"\"Apache version >= 2.3.9 rewrite rule arguments used for redirections to\n    https vhost\"\"\"\n\nOLD_REWRITE_HTTPS_ARGS: list[list[str]] = [\n    [\"^\", \"https://%{SERVER_NAME}%{REQUEST_URI}\", \"[L,QSA,R=permanent]\"],\n    [\"^\", \"https://%{SERVER_NAME}%{REQUEST_URI}\", \"[END,QSA,R=permanent]\"],\n    [\"^\", \"https://%{SERVER_NAME}%{REQUEST_URI}\", \"[L,NE,R=permanent]\"]]\n\nHSTS_ARGS: list[str] = [\"always\", \"set\", \"Strict-Transport-Security\",\n             \"\\\"max-age=31536000\\\"\"]\n\"\"\"Apache header arguments for HSTS\"\"\"\n\nUIR_ARGS: list[str] = [\"always\", \"set\", \"Content-Security-Policy\", \"upgrade-insecure-requests\"]\n\nHEADER_ARGS: dict[str, list[str]] = {\n    \"Strict-Transport-Security\": HSTS_ARGS, \"Upgrade-Insecure-Requests\": UIR_ARGS,\n}\n\nAUTOHSTS_STEPS: list[int] = [60, 300, 900, 3600, 21600, 43200, 86400]\n\"\"\"AutoHSTS increase steps: 1min, 5min, 15min, 1h, 6h, 12h, 24h\"\"\"\n\nAUTOHSTS_PERMANENT: int = 31536000\n\"\"\"Value for the last max-age of HSTS\"\"\"\n\nAUTOHSTS_FREQ: int = 172800\n\"\"\"Minimum time since last increase to perform a new one: 48h\"\"\"\n\nMANAGED_COMMENT: str = \"DO NOT REMOVE - Managed by Certbot\"\nMANAGED_COMMENT_ID: str = MANAGED_COMMENT + \", VirtualHost id: {0}\"\n\"\"\"Managed by Certbot comments and the VirtualHost identification template\"\"\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/display_ops.py",
    "content": "\"\"\"Contains UI methods for Apache operations.\"\"\"\nimport logging\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Sequence\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot_apache._internal.obj import VirtualHost\n\nlogger = logging.getLogger(__name__)\n\n\ndef select_vhost_multiple(vhosts: Optional[list[VirtualHost]]) -> list[VirtualHost]:\n    \"\"\"Select multiple Vhosts to install the certificate for\n\n    :param vhosts: Available Apache VirtualHosts\n    :type vhosts: :class:`list` of type `~VirtualHost`\n\n    :returns: List of VirtualHosts\n    :rtype: :class:`list`of type `~VirtualHost`\n    \"\"\"\n    if not vhosts:\n        return []\n    tags_list = [vhost.display_repr()+\"\\n\" for vhost in vhosts]\n    # Remove the extra newline from the last entry\n    if tags_list:\n        tags_list[-1] = tags_list[-1][:-1]\n    code, names = display_util.checklist(\n        \"Which VirtualHosts would you like to install the wildcard certificate for?\",\n        tags=tags_list, force_interactive=True)\n    if code == display_util.OK:\n        return_vhosts = _reversemap_vhosts(names, vhosts)\n        return return_vhosts\n    return []\n\n\ndef _reversemap_vhosts(names: Iterable[str], vhosts: Iterable[VirtualHost]) -> list[VirtualHost]:\n    \"\"\"Helper function for select_vhost_multiple for mapping string\n    representations back to actual vhost objects\"\"\"\n    return_vhosts = []\n\n    for selection in names:\n        for vhost in vhosts:\n            if vhost.display_repr().strip() == selection.strip():\n                return_vhosts.append(vhost)\n    return return_vhosts\n\n\ndef select_vhost(domain: str, vhosts: Sequence[VirtualHost]) -> Optional[VirtualHost]:\n    \"\"\"Select an appropriate Apache Vhost.\n\n    :param str domain: Domain for vhost selection\n\n    :param vhosts: Available Apache VirtualHosts\n    :type vhosts: :class:`list` of type `~VirtualHost`\n\n    :returns: VirtualHost or `None`\n    :rtype: `~obj.Vhost` or `None`\n\n    \"\"\"\n    if not vhosts:\n        return None\n    code, tag = _vhost_menu(domain, vhosts)\n    if code == display_util.OK:\n        return vhosts[tag]\n    return None\n\n\ndef _vhost_menu(domain: str, vhosts: Iterable[VirtualHost]) -> tuple[str, int]:\n    \"\"\"Select an appropriate Apache Vhost.\n\n    :param vhosts: Available Apache Virtual Hosts\n    :type vhosts: :class:`list` of type `~obj.Vhost`\n\n    :returns: Display tuple - ('code', tag')\n    :rtype: `tuple`\n\n    \"\"\"\n    # Free characters in the line of display text (9 is for ' | ' formatting)\n    free_chars = display_util.WIDTH - len(\"HTTPS\") - len(\"Enabled\") - 9\n\n    if free_chars < 2:\n        logger.debug(\"Display size is too small for \"\n                     \"certbot_apache._internal.display_ops._vhost_menu()\")\n        # This runs the edge off the screen, but it doesn't cause an \"error\"\n        filename_size = 1\n        disp_name_size = 1\n    else:\n        # Filename is a bit more important and probably longer with 000-*\n        filename_size = int(free_chars * .6)\n        disp_name_size = free_chars - filename_size\n\n    choices = []\n    for vhost in vhosts:\n        if len(vhost.get_names()) == 1:\n            disp_name = next(iter(vhost.get_names()))\n        elif not vhost.get_names():\n            disp_name = \"\"\n        else:\n            disp_name = \"Multiple Names\"\n\n        choices.append(\n            \"{fn:{fn_size}s} | {name:{name_size}s} | {https:5s} | \"\n            \"{active:7s}\".format(\n                fn=os.path.basename(vhost.filep)[:filename_size],\n                name=disp_name[:disp_name_size],\n                https=\"HTTPS\" if vhost.ssl else \"\",\n                active=\"Enabled\" if vhost.enabled else \"\",\n                fn_size=filename_size,\n                name_size=disp_name_size),\n        )\n\n    try:\n        code, tag = display_util.menu(\n            f\"We were unable to find a vhost with a ServerName \"\n            f\"or Address of {domain}.{os.linesep}Which virtual host would you \"\n            f\"like to choose?\",\n            choices, force_interactive=True)\n    except errors.MissingCommandlineFlag:\n        msg = (\n            f\"Encountered vhost ambiguity when trying to find a vhost for \"\n            f\"{domain} but was unable to ask for user \"\n            f\"guidance in non-interactive mode. Certbot may need \"\n            f\"vhosts to be explicitly labelled with ServerName or \"\n            f\"ServerAlias directives.\")\n        logger.error(msg)\n        raise errors.MissingCommandlineFlag(msg)\n\n    return code, tag\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/dualparser.py",
    "content": "\"\"\" Dual ParserNode implementation \"\"\"\nfrom typing import Any\nfrom typing import Generic\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\nfrom typing import TypeVar\n\nfrom certbot_apache._internal import apacheparser\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import augeasparser\nfrom certbot_apache._internal import interfaces\n\nif TYPE_CHECKING:\n    from certbot_apache._internal.apacheparser import ApacheParserNode  # pragma: no cover\n    from certbot_apache._internal.augeasparser import AugeasParserNode  # pragma: no cover\n\nGenericAugeasParserNode = TypeVar(\"GenericAugeasParserNode\", bound=\"AugeasParserNode\")\nGenericApacheParserNode = TypeVar(\"GenericApacheParserNode\", bound=\"ApacheParserNode\")\n# Circular imports needs Any, see https://github.com/python/mypy/issues/11910\nGenericDualNode = TypeVar(\"GenericDualNode\", bound=\"DualNodeBase[Any, Any]\")\n\n\nclass DualNodeBase(Generic[GenericAugeasParserNode, GenericApacheParserNode]):\n    \"\"\" Dual parser interface for in development testing. This is used as the\n    base class for dual parser interface classes. This class handles runtime\n    attribute value assertions.\"\"\"\n\n    def __init__(self, primary: GenericAugeasParserNode,\n                 secondary: GenericApacheParserNode) -> None:\n        self.primary = primary\n        self.secondary = secondary\n\n    def save(self, msg: str) -> None:  # pragma: no cover\n        \"\"\" Call save for both parsers \"\"\"\n        self.primary.save(msg)\n        self.secondary.save(msg)\n\n    def __getattr__(self, aname: str) -> Any:\n        \"\"\" Attribute value assertion \"\"\"\n        firstval = getattr(self.primary, aname)\n        secondval = getattr(self.secondary, aname)\n        exclusions = [\n            # Metadata will inherently be different, as ApacheParserNode does\n            # not have Augeas paths and so on.\n            aname == \"metadata\",\n            callable(firstval)\n        ]\n        if not any(exclusions):\n            assertions.assertEqualSimple(firstval, secondval)\n        return firstval\n\n    def find_ancestors(self, name: str) -> list[\"DualBlockNode\"]:\n        \"\"\" Traverses the ancestor tree and returns ancestors matching name \"\"\"\n        return self._find_helper(DualBlockNode, \"find_ancestors\", name)\n\n    def _find_helper(self, nodeclass: type[GenericDualNode], findfunc: str, search: str,\n                     **kwargs: Any) -> list[GenericDualNode]:\n        \"\"\"A helper for find_* functions. The function specific attributes should\n        be passed as keyword arguments.\n\n        :param interfaces.ParserNode nodeclass: The node class for results.\n        :param str findfunc: Name of the find function to call\n        :param str search: The search term\n        \"\"\"\n\n        primary_res = getattr(self.primary, findfunc)(search, **kwargs)\n        secondary_res = getattr(self.secondary, findfunc)(search, **kwargs)\n\n        # The order of search results for Augeas implementation cannot be\n        # assured.\n\n        pass_primary = assertions.isPassNodeList(primary_res)\n        pass_secondary = assertions.isPassNodeList(secondary_res)\n        new_nodes = []\n\n        if pass_primary and pass_secondary:\n            # Both unimplemented\n            new_nodes.append(nodeclass(primary=primary_res[0],\n                                       secondary=secondary_res[0]))  # pragma: no cover\n        elif pass_primary:\n            for c in secondary_res:\n                new_nodes.append(nodeclass(primary=primary_res[0],\n                                           secondary=c))\n        elif pass_secondary:\n            for c in primary_res:\n                new_nodes.append(nodeclass(primary=c,\n                                           secondary=secondary_res[0]))\n        else:\n            assert len(primary_res) == len(secondary_res)\n            matches = self._create_matching_list(primary_res, secondary_res)\n            for p, s in matches:\n                new_nodes.append(nodeclass(primary=p, secondary=s))\n\n        return new_nodes\n\n\nclass DualCommentNode(DualNodeBase[augeasparser.AugeasCommentNode,\n                                   apacheparser.ApacheCommentNode]):\n    \"\"\" Dual parser implementation of CommentNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\" This initialization implementation allows ordinary initialization\n        of CommentNode objects as well as creating a DualCommentNode object\n        using precreated or fetched CommentNode objects if provided as optional\n        arguments primary and secondary.\n\n        Parameters other than the following are from interfaces.CommentNode:\n\n        :param CommentNode primary: Primary pre-created CommentNode, mainly\n            used when creating new DualParser nodes using add_* methods.\n        :param CommentNode secondary: Secondary pre-created CommentNode\n        \"\"\"\n\n        kwargs.setdefault(\"primary\", None)\n        kwargs.setdefault(\"secondary\", None)\n        primary = kwargs.pop(\"primary\")\n        secondary = kwargs.pop(\"secondary\")\n\n        if primary or secondary:\n            assert primary and secondary\n            super().__init__(primary, secondary)\n        else:\n            super().__init__(augeasparser.AugeasCommentNode(**kwargs),\n                             apacheparser.ApacheCommentNode(**kwargs))\n\n        assertions.assertEqual(self.primary, self.secondary)\n\n\nclass DualDirectiveNode(DualNodeBase[augeasparser.AugeasDirectiveNode,\n                                     apacheparser.ApacheDirectiveNode]):\n    \"\"\" Dual parser implementation of DirectiveNode interface \"\"\"\n\n    parameters: str\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\" This initialization implementation allows ordinary initialization\n        of DirectiveNode objects as well as creating a DualDirectiveNode object\n        using precreated or fetched DirectiveNode objects if provided as optional\n        arguments primary and secondary.\n\n        Parameters other than the following are from interfaces.DirectiveNode:\n\n        :param DirectiveNode primary: Primary pre-created DirectiveNode, mainly\n            used when creating new DualParser nodes using add_* methods.\n        :param DirectiveNode secondary: Secondary pre-created DirectiveNode\n        \"\"\"\n\n        kwargs.setdefault(\"primary\", None)\n        kwargs.setdefault(\"secondary\", None)\n        primary = kwargs.pop(\"primary\")\n        secondary = kwargs.pop(\"secondary\")\n\n        if primary or secondary:\n            assert primary and secondary\n            super().__init__(primary, secondary)\n        else:\n            super().__init__(augeasparser.AugeasDirectiveNode(**kwargs),\n                             apacheparser.ApacheDirectiveNode(**kwargs))\n\n        assertions.assertEqual(self.primary, self.secondary)\n\n    def set_parameters(self, parameters: Iterable[str]) -> None:\n        \"\"\" Sets parameters and asserts that both implementation successfully\n        set the parameter sequence \"\"\"\n\n        self.primary.set_parameters(parameters)\n        self.secondary.set_parameters(parameters)\n        assertions.assertEqual(self.primary, self.secondary)\n\n\nclass DualBlockNode(DualNodeBase[augeasparser.AugeasBlockNode,\n                                 apacheparser.ApacheBlockNode]):\n    \"\"\" Dual parser implementation of BlockNode interface \"\"\"\n\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\" This initialization implementation allows ordinary initialization\n        of BlockNode objects as well as creating a DualBlockNode object\n        using precreated or fetched BlockNode objects if provided as optional\n        arguments primary and secondary.\n\n        Parameters other than the following are from interfaces.BlockNode:\n\n        :param BlockNode primary: Primary pre-created BlockNode, mainly\n            used when creating new DualParser nodes using add_* methods.\n        :param BlockNode secondary: Secondary pre-created BlockNode\n        \"\"\"\n\n        kwargs.setdefault(\"primary\", None)\n        kwargs.setdefault(\"secondary\", None)\n        primary: Optional[augeasparser.AugeasBlockNode] = kwargs.pop(\"primary\")\n        secondary: Optional[apacheparser.ApacheBlockNode] = kwargs.pop(\"secondary\")\n\n        if primary or secondary:\n            assert primary and secondary\n            super().__init__(primary, secondary)\n        else:\n            super().__init__(augeasparser.AugeasBlockNode(**kwargs),\n                             apacheparser.ApacheBlockNode(**kwargs))\n\n        assertions.assertEqual(self.primary, self.secondary)\n\n    def add_child_block(self, name: str, parameters: Optional[list[str]] = None,\n                        position: Optional[int] = None) -> \"DualBlockNode\":\n        \"\"\" Creates a new child BlockNode, asserts that both implementations\n        did it in a similar way, and returns a newly created DualBlockNode object\n        encapsulating both of the newly created objects \"\"\"\n\n        primary_new = self.primary.add_child_block(name, parameters, position)\n        secondary_new = self.secondary.add_child_block(name, parameters, position)\n        assertions.assertEqual(primary_new, secondary_new)\n        return DualBlockNode(primary=primary_new, secondary=secondary_new)\n\n    def add_child_directive(self, name: str, parameters: Optional[list[str]] = None,\n                            position: Optional[int] = None) -> DualDirectiveNode:\n        \"\"\" Creates a new child DirectiveNode, asserts that both implementations\n        did it in a similar way, and returns a newly created DualDirectiveNode\n        object encapsulating both of the newly created objects \"\"\"\n\n        primary_new = self.primary.add_child_directive(name, parameters, position)\n        secondary_new = self.secondary.add_child_directive(name, parameters, position)\n        assertions.assertEqual(primary_new, secondary_new)\n        return DualDirectiveNode(primary=primary_new, secondary=secondary_new)\n\n    def add_child_comment(self, comment: str = \"\",\n                          position: Optional[int] = None) -> DualCommentNode:\n        \"\"\" Creates a new child CommentNode, asserts that both implementations\n        did it in a similar way, and returns a newly created DualCommentNode\n        object encapsulating both of the newly created objects \"\"\"\n\n        primary_new = self.primary.add_child_comment(comment=comment, position=position)\n        secondary_new = self.secondary.add_child_comment(name=comment, position=position)\n        assertions.assertEqual(primary_new, secondary_new)\n        return DualCommentNode(primary=primary_new, secondary=secondary_new)\n\n    def _create_matching_list(self, primary_list: Iterable[interfaces.ParserNode],\n                              secondary_list: Iterable[interfaces.ParserNode]\n                              ) -> list[tuple[interfaces.ParserNode, interfaces.ParserNode]]:\n        \"\"\" Matches the list of primary_list to a list of secondary_list and\n        returns a list of tuples. This is used to create results for find_\n        methods.\n\n        This helper function exists, because we cannot ensure that the list of\n        search results returned by primary.find_* and secondary.find_* are ordered\n        in a same way. The function pairs the same search results from both\n        implementations to a list of tuples.\n        \"\"\"\n\n        matched = []\n        for p in primary_list:\n            match = None\n            for s in secondary_list:\n                try:\n                    assertions.assertEqual(p, s)\n                    match = s\n                    break\n                except AssertionError:\n                    continue\n            if match:\n                matched.append((p, match))\n            else:\n                raise AssertionError(\"Could not find a matching node.\")\n        return matched\n\n    def find_blocks(self, name: str, exclude: bool = True) -> list[\"DualBlockNode\"]:\n        \"\"\"\n        Performs a search for BlockNodes using both implementations and does simple\n        checks for results. This is built upon the assumption that unimplemented\n        find_* methods return a list with a single assertion passing object.\n        After the assertion, it creates a list of newly created DualBlockNode\n        instances that encapsulate the pairs of returned BlockNode objects.\n        \"\"\"\n\n        return self._find_helper(DualBlockNode, \"find_blocks\", name,\n                                 exclude=exclude)\n\n    def find_directives(self, name: str, exclude: bool = True) -> list[DualDirectiveNode]:\n        \"\"\"\n        Performs a search for DirectiveNodes using both implementations and\n        checks the results. This is built upon the assumption that unimplemented\n        find_* methods return a list with a single assertion passing object.\n        After the assertion, it creates a list of newly created DualDirectiveNode\n        instances that encapsulate the pairs of returned DirectiveNode objects.\n        \"\"\"\n\n        return self._find_helper(DualDirectiveNode, \"find_directives\", name,\n                                 exclude=exclude)\n\n    def find_comments(self, comment: str) -> list[DualCommentNode]:\n        \"\"\"\n        Performs a search for CommentNodes using both implementations and\n        checks the results. This is built upon the assumption that unimplemented\n        find_* methods return a list with a single assertion passing object.\n        After the assertion, it creates a list of newly created DualCommentNode\n        instances that encapsulate the pairs of returned CommentNode objects.\n        \"\"\"\n\n        return self._find_helper(DualCommentNode, \"find_comments\", comment)\n\n    def delete_child(self, child: \"DualBlockNode\") -> None:\n        \"\"\"Deletes a child from the ParserNode implementations. The actual\n        ParserNode implementations are used here directly in order to be able\n        to match a child to the list of children.\"\"\"\n\n        self.primary.delete_child(child.primary)\n        self.secondary.delete_child(child.secondary)\n\n    def unsaved_files(self) -> set[str]:\n        \"\"\" Fetches the list of unsaved file paths and asserts that the lists\n        match \"\"\"\n        primary_files = self.primary.unsaved_files()\n        secondary_files = self.secondary.unsaved_files()\n        assertions.assertEqualSimple(primary_files, secondary_files)\n\n        return primary_files\n\n    def parsed_paths(self) -> list[str]:\n        \"\"\"\n        Returns a list of file paths that have currently been parsed into the parser\n        tree. The returned list may include paths with wildcard characters, for\n        example: ['/etc/apache2/conf.d/*.load']\n\n        This is typically called on the root node of the ParserNode tree.\n\n        :returns: list of file paths of files that have been parsed\n        \"\"\"\n\n        primary_paths = self.primary.parsed_paths()\n        secondary_paths = self.secondary.parsed_paths()\n        assertions.assertEqualPathsList(primary_paths, secondary_paths)\n        return primary_paths\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/entrypoint.py",
    "content": "\"\"\" Entry point for Apache Plugin \"\"\"\n\nfrom certbot import util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import override_alpine\nfrom certbot_apache._internal import override_arch\nfrom certbot_apache._internal import override_centos\nfrom certbot_apache._internal import override_darwin\nfrom certbot_apache._internal import override_debian\nfrom certbot_apache._internal import override_fedora\nfrom certbot_apache._internal import override_gentoo\nfrom certbot_apache._internal import override_suse\nfrom certbot_apache._internal import override_void\n\nOVERRIDE_CLASSES: dict[str, type[configurator.ApacheConfigurator]] = {\n    \"alpine\": override_alpine.AlpineConfigurator,\n    \"arch\": override_arch.ArchConfigurator,\n    \"cloudlinux\": override_centos.CentOSConfigurator,\n    \"darwin\": override_darwin.DarwinConfigurator,\n    \"debian\": override_debian.DebianConfigurator,\n    \"ubuntu\": override_debian.DebianConfigurator,\n    \"centos\": override_centos.CentOSConfigurator,\n    \"centos linux\": override_centos.CentOSConfigurator,\n    \"fedora_old\": override_centos.CentOSConfigurator,\n    \"fedora\": override_fedora.FedoraConfigurator,\n    \"linuxmint\": override_debian.DebianConfigurator,\n    \"ol\": override_centos.CentOSConfigurator,\n    \"oracle\": override_centos.CentOSConfigurator,\n    \"redhatenterpriseserver\": override_centos.CentOSConfigurator,\n    \"red hat enterprise linux server\": override_centos.CentOSConfigurator,\n    \"rhel\": override_centos.CentOSConfigurator,\n    \"amazon\": override_centos.CentOSConfigurator,\n    \"gentoo\": override_gentoo.GentooConfigurator,\n    \"gentoo base system\": override_gentoo.GentooConfigurator,\n    \"opensuse\": override_suse.OpenSUSEConfigurator,\n    \"suse\": override_suse.OpenSUSEConfigurator,\n    \"sles\": override_suse.OpenSUSEConfigurator,\n    \"scientific\": override_centos.CentOSConfigurator,\n    \"scientific linux\": override_centos.CentOSConfigurator,\n    \"void\": override_void.VoidConfigurator,\n}\n\n\ndef get_configurator() -> type[configurator.ApacheConfigurator]:\n    \"\"\" Get correct configurator class based on the OS fingerprint \"\"\"\n    os_name, os_version = util.get_os_info()\n    os_name = os_name.lower()\n    override_class = None\n\n    # Special case for older Fedora versions\n    min_version = util.parse_loose_version('29')\n    if os_name == 'fedora' and util.parse_loose_version(os_version) < min_version:\n        os_name = 'fedora_old'\n\n    try:\n        override_class = OVERRIDE_CLASSES[os_name]\n    except KeyError:\n        # OS not found in the list\n        os_like = util.get_systemd_os_like()\n        if os_like:\n            for os_name in os_like:\n                override_class = OVERRIDE_CLASSES.get(os_name)\n        if not override_class:\n            # No override class found, return the generic configurator\n            override_class = configurator.ApacheConfigurator\n    return override_class\n\n\nENTRYPOINT = get_configurator()\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/http_01.py",
    "content": "\"\"\"A class that performs HTTP-01 challenges for Apache\"\"\"\nimport errno\nimport logging\nfrom typing import TYPE_CHECKING\n\nfrom acme.challenges import KeyAuthorizationChallengeResponse\nfrom acme import messages\nfrom certbot import errors\nfrom certbot.achallenges import KeyAuthorizationAnnotatedChallenge\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.plugins import common\nfrom certbot_apache._internal.obj import VirtualHost\nfrom certbot_apache._internal.parser import get_aug_path\n\nif TYPE_CHECKING:\n    from certbot_apache._internal.configurator import ApacheConfigurator  # pragma: no cover\n\nlogger = logging.getLogger(__name__)\n\n\nclass ApacheHttp01(common.ChallengePerformer):\n    \"\"\"Class that performs HTTP-01 challenges within the Apache configurator.\"\"\"\n\n    CONFIG_TEMPLATE24_PRE = \"\"\"\\\n        RewriteRule ^/\\\\.well-known/acme-challenge/([A-Za-z0-9-_=]+)$ {0}/$1 [END]\n    \"\"\"\n    CONFIG_TEMPLATE24_POST = \"\"\"\\\n        RewriteEngine on\n        <Directory {0}>\n            Require all granted\n        </Directory>\n        <Location /.well-known/acme-challenge>\n            Require all granted\n        </Location>\n    \"\"\"\n\n    def __init__(self, configurator: \"ApacheConfigurator\") -> None:\n        super().__init__(configurator)\n        self.configurator: \"ApacheConfigurator\"\n        self.challenge_conf_pre = os.path.join(\n            self.configurator.conf(\"challenge-location\"),\n            \"le_http_01_challenge_pre.conf\")\n        self.challenge_conf_post = os.path.join(\n            self.configurator.conf(\"challenge-location\"),\n            \"le_http_01_challenge_post.conf\")\n        self.challenge_dir = os.path.join(\n            self.configurator.config.work_dir,\n            \"http_challenges\")\n        self.moded_vhosts: set[VirtualHost] = set()\n\n    def perform(self) -> list[KeyAuthorizationChallengeResponse]:\n        \"\"\"Perform all HTTP-01 challenges.\"\"\"\n        if not self.achalls:\n            return []\n        if any(achall.identifier.typ == messages.IDENTIFIER_IP for achall in self.achalls):\n            raise errors.ConfigurationError(\n                \"Apache authenticator not supported for IP address certificates\")\n        # Save any changes to the configuration as a precaution\n        # About to make temporary changes to the config\n        self.configurator.save(\"Changes before challenge setup\", True)\n\n        self.configurator.ensure_listen(str(self.configurator.config.http01_port))\n        self.prepare_http01_modules()\n\n        responses = self._set_up_challenges()\n\n        self._mod_config()\n        # Save reversible changes\n        self.configurator.save(\"HTTP Challenge\", True)\n\n        return responses\n\n    def prepare_http01_modules(self) -> None:\n        \"\"\"Make sure that we have the needed modules available for http01\"\"\"\n\n        if self.configurator.conf(\"handle-modules\"):\n            needed_modules = [\"rewrite\", \"authz_core\"]\n            for mod in needed_modules:\n                if mod + \"_module\" not in self.configurator.parser.modules:\n                    self.configurator.enable_mod(mod, temp=True)\n\n    def _mod_config(self) -> None:\n        selected_vhosts: list[VirtualHost] = []\n        http_port = str(self.configurator.config.http01_port)\n\n        # Search for VirtualHosts matching by name\n        for chall in self.achalls:\n            selected_vhosts += self._matching_vhosts(chall.identifier.value)\n\n        # Ensure that we have one or more VirtualHosts that we can continue\n        # with. (one that listens to port configured with --http-01-port)\n        found = False\n        for vhost in selected_vhosts:\n            if any(a.is_wildcard() or a.get_port() == http_port for a in vhost.addrs):\n                found = True\n\n        # If there's at least one eligible VirtualHost, also add all unnamed VirtualHosts\n        # because they might match at runtime (#8890)\n        if found:\n            selected_vhosts += self._unnamed_vhosts()\n        # Otherwise, add every Virtualhost which listens on the right port\n        else:\n            selected_vhosts += self._relevant_vhosts()\n\n        # Add the challenge configuration\n        for vh in selected_vhosts:\n            self._set_up_include_directives(vh)\n\n        self.configurator.reverter.register_file_creation(\n            True, self.challenge_conf_pre)\n        self.configurator.reverter.register_file_creation(\n            True, self.challenge_conf_post)\n\n        config_text_pre = self.CONFIG_TEMPLATE24_PRE.format(self.challenge_dir)\n        config_text_post = self.CONFIG_TEMPLATE24_POST.format(self.challenge_dir)\n\n        logger.debug(\"writing a pre config file with text:\\n %s\", config_text_pre)\n        with open(self.challenge_conf_pre, \"w\") as new_conf:\n            new_conf.write(config_text_pre)\n        logger.debug(\"writing a post config file with text:\\n %s\", config_text_post)\n        with open(self.challenge_conf_post, \"w\") as new_conf:\n            new_conf.write(config_text_post)\n\n    def _matching_vhosts(self, domain: str) -> list[VirtualHost]:\n        \"\"\"Return all VirtualHost objects that have the requested domain name or\n        a wildcard name that would match the domain in ServerName or ServerAlias\n        directive.\n        \"\"\"\n        matching_vhosts = []\n        for vhost in self.configurator.vhosts:\n            if self.configurator.domain_in_names(vhost.get_names(), domain):\n                # domain_in_names also matches the exact names, so no need\n                # to check \"domain in vhost.get_names()\" explicitly here\n                matching_vhosts.append(vhost)\n\n        return matching_vhosts\n\n    def _relevant_vhosts(self) -> list[VirtualHost]:\n        http01_port = str(self.configurator.config.http01_port)\n        relevant_vhosts: list[VirtualHost] = []\n        for vhost in self.configurator.vhosts:\n            if any(a.is_wildcard() or a.get_port() == http01_port for a in vhost.addrs):\n                if not vhost.ssl:\n                    relevant_vhosts.append(vhost)\n        if not relevant_vhosts:\n            raise errors.PluginError(\n                \"Unable to find a virtual host listening on port {0} which is\"\n                \" currently needed for Certbot to prove to the CA that you\"\n                \" control your domain. Please add a virtual host for port\"\n                \" {0}.\".format(http01_port))\n\n        return relevant_vhosts\n\n    def _unnamed_vhosts(self) -> list[VirtualHost]:\n        \"\"\"Return all VirtualHost objects with no ServerName\"\"\"\n        return [vh for vh in self.configurator.vhosts if vh.name is None]\n\n    def _set_up_challenges(self) -> list[KeyAuthorizationChallengeResponse]:\n        if not os.path.isdir(self.challenge_dir):\n            with filesystem.temp_umask(0o022):\n                try:\n                    filesystem.makedirs(self.challenge_dir, 0o755)\n                except OSError as exception:\n                    if exception.errno not in (errno.EEXIST, errno.EISDIR):\n                        raise errors.PluginError(\n                            \"Couldn't create root for http-01 challenge\")\n\n        responses = []\n        for achall in self.achalls:\n            responses.append(self._set_up_challenge(achall))\n\n        return responses\n\n    def _set_up_challenge(self, achall: KeyAuthorizationAnnotatedChallenge\n                          ) -> KeyAuthorizationChallengeResponse:\n        response: KeyAuthorizationChallengeResponse\n        response, validation = achall.response_and_validation()\n\n        name: str = os.path.join(self.challenge_dir, achall.chall.encode(\"token\"))\n\n        self.configurator.reverter.register_file_creation(True, name)\n        with open(name, 'wb') as f:\n            f.write(validation.encode())\n        filesystem.chmod(name, 0o644)\n\n        return response\n\n    def _set_up_include_directives(self, vhost: VirtualHost) -> None:\n        \"\"\"Includes override configuration to the beginning and to the end of\n        VirtualHost. Note that this include isn't added to Augeas search tree\"\"\"\n\n        if vhost not in self.moded_vhosts:\n            logger.debug(\n                \"Adding a temporary challenge validation Include for name: %s in: %s\",\n                vhost.name, vhost.filep)\n            self.configurator.parser.add_dir_beginning(\n                vhost.path, \"Include\", self.challenge_conf_pre)\n            self.configurator.parser.add_dir(\n                vhost.path, \"Include\", self.challenge_conf_post)\n\n            if not vhost.enabled:\n                self.configurator.parser.add_dir(\n                    get_aug_path(self.configurator.parser.loc[\"default\"]),\n                    \"Include\", vhost.filep)\n\n            self.moded_vhosts.add(vhost)\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/interfaces.py",
    "content": "\"\"\"ParserNode interface for interacting with configuration tree.\n\nGeneral description\n-------------------\n\nThe ParserNode interfaces are designed to be able to contain all the parsing logic,\nwhile allowing their users to interact with the configuration tree in a Pythonic\nand well structured manner.\n\nThe structure allows easy traversal of the tree of ParserNodes. Each ParserNode\nstores a reference to its ancestor and immediate children, allowing the user to\ntraverse the tree using built in interface methods as well as accessing the interface\nproperties directly.\n\nParserNode interface implementation should stand between the actual underlying\nparser functionality and the business logic within Configurator code, interfacing\nwith both. The ParserNode tree is a result of configuration parsing action.\n\nParserNode tree will be in charge of maintaining the parser state and hence the\nabstract syntax tree (AST). Interactions between ParserNode tree and underlying\nparser should involve only parsing the configuration files to this structure, and\nwriting it back to the filesystem - while preserving the format including whitespaces.\n\nFor some implementations (Apache for example) it's important to keep track of and\nto use state information while parsing conditional blocks and directives. This\nallows the implementation to set a flag to parts of the parsed configuration\nstructure as not being in effect in a case of unmatched conditional block. It's\nimportant to store these blocks in the tree as well in order to not to conduct\ndestructive actions (failing to write back parts of the configuration) while writing\nthe AST back to the filesystem.\n\nThe ParserNode tree is in charge of maintaining the its own structure while every\nchild node fetched with find - methods or by iterating its list of children can be\nchanged in place. When making changes the affected nodes should be flagged as \"dirty\"\nin order for the parser implementation to figure out the parts of the configuration\nthat need to be written back to disk during the save() operation.\n\n\nMetadata\n--------\n\nThe metadata holds all the implementation specific attributes of the ParserNodes -\nthings like the positional information related to the AST, file paths, whitespacing,\nand any other information relevant to the underlying parser engine.\n\nAccess to the metadata should be handled by implementation specific methods, allowing\nthe Configurator functionality to access the underlying information where needed.\n\nFor some implementations the node can be initialized using the information carried\nin metadata alone. This is useful especially when populating the ParserNode tree\nwhile parsing the configuration.\n\n\nApache implementation\n---------------------\n\nThe Apache implementation of ParserNode interface requires some implementation\nspecific functionalities that are not described by the interface itself.\n\nInitialization\n\nWhen the user of a ParserNode class is creating these objects, they must specify\nthe parameters as described in the documentation for the __init__ methods below.\nWhen these objects are created internally, however, some parameters may not be\nneeded because (possibly more detailed) information is included in the metadata\nparameter. In this case, implementations can deviate from the required parameters\nfrom __init__, however, they should still behave the same when metadata is not\nprovided.\n\nFor consistency internally, if an argument is provided directly in the ParserNode\ninitialization parameters as well as within metadata it's recommended to establish\nclear behavior around this scenario within the implementation.\n\nConditional blocks\n\nApache configuration can have conditional blocks, for example: <IfModule ...>,\nresulting the directives and subblocks within it being either enabled or disabled.\nWhile find_* interface methods allow including the disabled parts of the configuration\ntree in searches a special care needs to be taken while parsing the structure in\norder to reflect the active state of configuration.\n\nWhitespaces\n\nEach ParserNode object is responsible of storing its prepending whitespace characters\nin order to be able to write the AST back to filesystem like it was, preserving the\nformat, this applies for parameters of BlockNode and DirectiveNode as well.\nWhen parameters of ParserNode are changed, the pre-existing whitespaces in the\nparameter sequence are discarded, as the general reason for storing them is to\nmaintain the ability to write the configuration back to filesystem exactly like\nit was. This loses its meaning when we have to change the directives or blocks\nparameters for other reasons.\n\nSearches and matching\n\nApache configuration is largely case insensitive, so the Apache implementation of\nParserNode interface needs to provide the user means to match block and directive\nnames and parameters in case insensitive manner. This does not apply to everything\nhowever, for example the parameters of a conditional statement may be case sensitive.\nFor this reason the internal representation of data should not ignore the case.\n\"\"\"\nimport abc\nfrom typing import Any\nfrom typing import Optional\nfrom typing import TypeVar\n\nGenericParserNode = TypeVar(\"GenericParserNode\", bound=\"ParserNode\")\n\n\nclass ParserNode(metaclass=abc.ABCMeta):\n    \"\"\"\n    ParserNode is the basic building block of the tree of such nodes,\n    representing the structure of the configuration. It is largely meant to keep\n    the structure information intact and idiomatically accessible.\n\n    The root node as well as the child nodes of it should be instances of ParserNode.\n    Nodes keep track of their differences to on-disk representation of configuration\n    by marking modified ParserNodes as dirty to enable partial write-to-disk for\n    different files in the configuration structure.\n\n    While for the most parts the usage and the child types are obvious, \"include\"-\n    and similar directives are an exception to this rule. This is because of the\n    nature of include directives - which unroll the contents of another file or\n    configuration block to their place. While we could unroll the included nodes\n    to the parent tree, it remains important to keep the context of include nodes\n    separate in order to write back the original configuration as it was.\n\n    For parsers that require the implementation to keep track of the whitespacing,\n    it's responsibility of each ParserNode object itself to store its prepending\n    whitespaces in order to be able to reconstruct the complete configuration file\n    as it was when originally read from the disk.\n\n    ParserNode objects should have the following attributes:\n\n    # Reference to ancestor node, or None if the node is the root node of the\n    # configuration tree.\n    ancestor: Optional[ParserNode]\n\n    # True if this node has been modified since last save.\n    dirty: bool\n\n    # Filepath of the file where the configuration element for this ParserNode\n    # object resides. For root node, the value for filepath is the httpd root\n    # configuration file. Filepath can be None if a configuration directive is\n    # defined in for example the httpd command line.\n    filepath: Optional[str]\n\n    # Metadata dictionary holds all the implementation specific key-value pairs\n    # for the ParserNode instance.\n    metadata: Dict[str, Any]\n    \"\"\"\n    ancestor: Optional[\"ParserNode\"]\n    dirty: bool\n    filepath: Optional[str]\n    metadata: dict[str, Any]\n\n    @abc.abstractmethod\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        Initializes the ParserNode instance, and sets the ParserNode specific\n        instance variables. This is not meant to be used directly, but through\n        specific classes implementing ParserNode interface.\n\n        :param ancestor: BlockNode ancestor for this CommentNode. Required.\n        :type ancestor: BlockNode or None\n\n        :param filepath: Filesystem path for the file where this CommentNode\n            does or should exist in the filesystem. Required.\n        :type filepath: str or None\n\n        :param dirty: Boolean flag for denoting if this CommentNode has been\n            created or changed after the last save. Default: False.\n        :type dirty: bool\n\n        :param metadata: Dictionary of metadata values for this ParserNode object.\n            Metadata information should be used only internally in the implementation.\n            Default: {}\n        :type metadata: dict\n        \"\"\"\n\n    @abc.abstractmethod\n    def save(self, msg: str) -> None:\n        \"\"\"\n        Save traverses the children, and attempts to write the AST to disk for\n        all the objects that are marked dirty. The actual operation of course\n        depends on the underlying implementation. save() shouldn't be called\n        from the Configurator outside of its designated save() method in order\n        to ensure that the Reverter checkpoints are created properly.\n\n        Note: this approach of keeping internal structure of the configuration\n        within the ParserNode tree does not represent the file inclusion structure\n        of actual configuration files that reside in the filesystem. To handle\n        file writes properly, the file specific temporary trees should be extracted\n        from the full ParserNode tree where necessary when writing to disk.\n\n        :param str msg: Message describing the reason for the save.\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def find_ancestors(self: GenericParserNode, name: str) -> list[GenericParserNode]:\n        \"\"\"\n        Traverses the ancestor tree up, searching for BlockNodes with a specific\n        name.\n\n        :param str name: Name of the ancestor BlockNode to search for\n\n        :returns: A list of ancestor BlockNodes that match the name\n        :rtype: list of BlockNode\n        \"\"\"\n\n\nclass CommentNode(ParserNode, metaclass=abc.ABCMeta):\n    \"\"\"\n    CommentNode class is used for representation of comments within the parsed\n    configuration structure. Because of the nature of comments, it is not able\n    to have child nodes and hence it is always treated as a leaf node.\n\n    CommentNode stores its contents in class variable 'comment' and does not\n    have a specific name.\n\n    CommentNode objects should have the following attributes in addition to\n    the ones described in ParserNode:\n\n    # Contains the contents of the comment without the directive notation\n    # (typically # or /* ... */).\n    comment: str\n\n    \"\"\"\n    comment: str\n\n    @abc.abstractmethod\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        Initializes the CommentNode instance and sets its instance variables.\n\n        :param comment: Contents of the comment. Required.\n        :type comment: str\n\n        :param ancestor: BlockNode ancestor for this CommentNode. Required.\n        :type ancestor: BlockNode or None\n\n        :param filepath: Filesystem path for the file where this CommentNode\n            does or should exist in the filesystem. Required.\n        :type filepath: str or None\n\n        :param dirty: Boolean flag for denoting if this CommentNode has been\n            created or changed after the last save. Default: False.\n        :type dirty: bool\n        \"\"\"\n        super().__init__(  # pragma: no cover\n            ancestor=kwargs['ancestor'],\n            dirty=kwargs.get('dirty', False),\n            filepath=kwargs['filepath'],\n            metadata=kwargs.get('metadata', {}),\n        )\n\n\nclass DirectiveNode(ParserNode, metaclass=abc.ABCMeta):\n    \"\"\"\n    DirectiveNode class represents a configuration directive within the configuration.\n    It can have zero or more parameters attached to it. Because of the nature of\n    single directives, it is not able to have child nodes and hence it is always\n    treated as a leaf node.\n\n    If a this directive was defined on the httpd command line, the ancestor instance\n    variable for this DirectiveNode should be None, and it should be inserted to the\n    beginning of root BlockNode children sequence.\n\n    DirectiveNode objects should have the following attributes in addition to\n    the ones described in ParserNode:\n\n    # True if this DirectiveNode is enabled and False if it is inside of an\n    # inactive conditional block.\n    enabled: bool\n\n    # Name, or key of the configuration directive. If BlockNode subclass of\n    # DirectiveNode is the root configuration node, the name should be None.\n    name: Optional[str]\n\n    # Tuple of parameters of this ParserNode object, excluding whitespaces.\n    parameters: Tuple[str, ...]\n\n    \"\"\"\n    enabled: bool\n    name: Optional[str]\n    parameters: tuple[str, ...]\n\n    @abc.abstractmethod\n    def __init__(self, **kwargs: Any) -> None:\n        \"\"\"\n        Initializes the DirectiveNode instance and sets its instance variables.\n\n        :param name: Name or key of the DirectiveNode object. Required.\n        :type name: str or None\n\n        :param tuple parameters: Tuple of str parameters for this DirectiveNode.\n            Default: ().\n        :type parameters: tuple\n\n        :param ancestor: BlockNode ancestor for this DirectiveNode, or None for\n            root configuration node. Required.\n        :type ancestor: BlockNode or None\n\n        :param filepath: Filesystem path for the file where this DirectiveNode\n            does or should exist in the filesystem, or None for directives introduced\n            in the httpd command line. Required.\n        :type filepath: str or None\n\n        :param dirty: Boolean flag for denoting if this DirectiveNode has been\n            created or changed after the last save. Default: False.\n        :type dirty: bool\n\n        :param enabled: True if this DirectiveNode object is parsed in the active\n            configuration of the httpd. False if the DirectiveNode exists within a\n            unmatched conditional configuration block. Default: True.\n        :type enabled: bool\n\n        \"\"\"\n        super().__init__(  # pragma: no cover\n            ancestor=kwargs['ancestor'],\n            dirty=kwargs.get('dirty', False),\n            filepath=kwargs['filepath'],\n            metadata=kwargs.get('metadata', {}),\n        )\n\n    @abc.abstractmethod\n    def set_parameters(self, parameters: list[str]) -> None:\n        \"\"\"\n        Sets the sequence of parameters for this ParserNode object without\n        whitespaces. While the whitespaces for parameters are discarded when using\n        this method, the whitespacing preceding the ParserNode itself should be\n        kept intact.\n\n        :param list parameters: sequence of parameters\n        \"\"\"\n\n\nclass BlockNode(DirectiveNode, metaclass=abc.ABCMeta):\n    \"\"\"\n    BlockNode class represents a block of nested configuration directives, comments\n    and other blocks as its children. A BlockNode can have zero or more parameters\n    attached to it.\n\n    Configuration blocks typically consist of one or more child nodes of all possible\n    types. Because of this, the BlockNode class has various discovery and structure\n    management methods.\n\n    Lists of parameters used as an optional argument for some of the methods should\n    be lists of strings that are applicable parameters for each specific BlockNode\n    or DirectiveNode type. As an example, for a following configuration example:\n\n        <VirtualHost *:80>\n           ...\n        </VirtualHost>\n\n    The node type would be BlockNode, name would be 'VirtualHost' and its parameters\n    would be: ['*:80'].\n\n    While for the following example:\n\n        LoadModule alias_module /usr/lib/apache2/modules/mod_alias.so\n\n    The node type would be DirectiveNode, name would be 'LoadModule' and its\n    parameters would be: ['alias_module', '/usr/lib/apache2/modules/mod_alias.so']\n\n    The applicable parameters are dependent on the underlying configuration language\n    and its grammar.\n\n    BlockNode objects should have the following attributes in addition to\n    the ones described in DirectiveNode:\n\n    # Tuple of direct children of this BlockNode object. The order of children\n    # in this tuple retain the order of elements in the parsed configuration\n    # block.\n    children: Tuple[ParserNode, ...]\n\n    \"\"\"\n    children: tuple[ParserNode, ...]\n\n    @abc.abstractmethod\n    def add_child_block(self, name: str, parameters: Optional[list[str]] = None,\n                        position: Optional[int] = None) -> \"BlockNode\":\n        \"\"\"\n        Adds a new BlockNode child node with provided values and marks the callee\n        BlockNode dirty. This is used to add new children to the AST. The preceding\n        whitespaces should not be added based on the ancestor or siblings for the\n        newly created object. This is to match the current behavior of the legacy\n        parser implementation.\n\n        :param str name: The name of the child node to add\n        :param list parameters: list of parameters for the node\n        :param int position: Position in the list of children to add the new child\n            node to. Defaults to None, which appends the newly created node to the list.\n            If an integer is given, the child is inserted before that index in the\n            list similar to list().insert.\n\n        :returns: BlockNode instance of the created child block\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def add_child_directive(self, name: str, parameters: Optional[list[str]] = None,\n                            position: Optional[int] = None) -> DirectiveNode:\n        \"\"\"\n        Adds a new DirectiveNode child node with provided values and marks the\n        callee BlockNode dirty. This is used to add new children to the AST. The\n        preceding whitespaces should not be added based on the ancestor or siblings\n        for the newly created object. This is to match the current behavior of the\n        legacy parser implementation.\n\n\n        :param str name: The name of the child node to add\n        :param list parameters: list of parameters for the node\n        :param int position: Position in the list of children to add the new child\n            node to. Defaults to None, which appends the newly created node to the list.\n            If an integer is given, the child is inserted before that index in the\n            list similar to list().insert.\n\n        :returns: DirectiveNode instance of the created child directive\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def add_child_comment(self, comment: str = \"\", position: Optional[int] = None) -> CommentNode:\n        \"\"\"\n        Adds a new CommentNode child node with provided value and marks the\n        callee BlockNode dirty. This is used to add new children to the AST. The\n        preceding whitespaces should not be added based on the ancestor or siblings\n        for the newly created object. This is to match the current behavior of the\n        legacy parser implementation.\n\n\n        :param str comment: Comment contents\n        :param int position: Position in the list of children to add the new child\n            node to. Defaults to None, which appends the newly created node to the list.\n            If an integer is given, the child is inserted before that index in the\n            list similar to list().insert.\n\n        :returns: CommentNode instance of the created child comment\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def find_blocks(self, name: str, exclude: bool = True) -> list[\"BlockNode\"]:\n        \"\"\"\n        Find a configuration block by name. This method walks the child tree of\n        ParserNodes under the instance it was called from. This way it is possible\n        to search for the whole configuration tree, when starting from root node or\n        to do a partial search when starting from a specified branch. The lookup\n        should be case insensitive.\n\n        :param str name: The name of the directive to search for\n        :param bool exclude: If the search results should exclude the contents of\n            ParserNode objects that reside within conditional blocks and because\n            of current state are not enabled.\n\n        :returns: A list of found BlockNode objects.\n        \"\"\"\n\n    @abc.abstractmethod\n    def find_directives(self, name: str, exclude: bool = True) -> list[DirectiveNode]:\n        \"\"\"\n        Find a directive by name. This method walks the child tree of ParserNodes\n        under the instance it was called from. This way it is possible to search\n        for the whole configuration tree, when starting from root node, or to do\n        a partial search when starting from a specified branch. The lookup should\n        be case insensitive.\n\n        :param str name: The name of the directive to search for\n        :param bool exclude: If the search results should exclude the contents of\n            ParserNode objects that reside within conditional blocks and because\n            of current state are not enabled.\n\n        :returns: A list of found DirectiveNode objects.\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def find_comments(self, comment: str) -> list[CommentNode]:\n        \"\"\"\n        Find comments with value containing the search term.\n\n        This method walks the child tree of ParserNodes under the instance it was\n        called from. This way it is possible to search for the whole configuration\n        tree, when starting from root node, or to do a partial search when starting\n        from a specified branch. The lookup should be case sensitive.\n\n        :param str comment: The content of comment to search for\n\n        :returns: A list of found CommentNode objects.\n\n        \"\"\"\n\n    @abc.abstractmethod\n    def delete_child(self, child: ParserNode) -> None:\n        \"\"\"\n        Remove a specified child node from the list of children of the called\n        BlockNode object.\n\n        :param ParserNode child: Child ParserNode object to remove from the list\n            of children of the callee.\n        \"\"\"\n\n    @abc.abstractmethod\n    def unsaved_files(self) -> list[str]:\n        \"\"\"\n        Returns a list of file paths that have been changed since the last save\n        (or the initial configuration parse). The intended use for this method\n        is to tell the Reverter which files need to be included in a checkpoint.\n\n        This is typically called for the root of the ParserNode tree.\n\n        :returns: list of file paths of files that have been changed but not yet\n            saved to disk.\n        \"\"\"\n\n    @abc.abstractmethod\n    def parsed_paths(self) -> list[str]:\n        \"\"\"\n        Returns a list of file paths that have currently been parsed into the parser\n        tree. The returned list may include paths with wildcard characters, for\n        example: ['/etc/apache2/conf.d/*.load']\n\n        This is typically called on the root node of the ParserNode tree.\n\n        :returns: list of file paths of files that have been parsed\n        \"\"\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/obj.py",
    "content": "\"\"\"Module contains classes used by the Apache Configurator.\"\"\"\nimport re\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nfrom certbot.plugins import common\nfrom certbot_apache._internal.apacheparser import ApacheBlockNode\nfrom certbot_apache._internal.augeasparser import AugeasBlockNode\nfrom certbot_apache._internal.dualparser import DualBlockNode\n\n\nclass Addr(common.Addr):\n    \"\"\"Represents an Apache address.\"\"\"\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"This is defined as equivalent within Apache.\n\n        ip_addr:* == ip_addr\n\n        \"\"\"\n        if isinstance(other, self.__class__):\n            return ((self.tup == other.tup) or\n                    (self.tup[0] == other.tup[0] and\n                     self.is_wildcard() and other.is_wildcard()))\n        return False\n\n    def __repr__(self) -> str:\n        return f\"certbot_apache._internal.obj.Addr({repr(self.tup)})\"\n\n    def __hash__(self) -> int:  # pylint: disable=useless-super-delegation\n        # Python 3 requires explicit overridden for __hash__ if __eq__ or\n        # __cmp__ is overridden. See https://bugs.python.org/issue2235\n        return super().__hash__()\n\n    def _addr_less_specific(self, addr: \"Addr\") -> bool:\n        \"\"\"Returns if addr.get_addr() is more specific than self.get_addr().\"\"\"\n        # pylint: disable=protected-access\n        return addr._rank_specific_addr() > self._rank_specific_addr()\n\n    def _rank_specific_addr(self) -> int:\n        \"\"\"Returns numerical rank for get_addr()\n\n        :returns: 2 - FQ, 1 - wildcard, 0 - _default_\n        :rtype: int\n\n        \"\"\"\n        if self.get_addr() == \"_default_\":\n            return 0\n        elif self.get_addr() == \"*\":\n            return 1\n        return 2\n\n    def conflicts(self, addr: \"Addr\") -> bool:\n        r\"\"\"Returns if address could conflict with correct function of self.\n\n        Could addr take away service provided by self within Apache?\n\n        .. note::IP Address is more important than wildcard.\n            Connection from 127.0.0.1:80 with choices of *:80 and 127.0.0.1:*\n            chooses 127.0.0.1:\\*\n\n        .. todo:: Handle domain name addrs...\n\n        Examples:\n\n        =========================================  =====\n        ``127.0.0.1:\\*.conflicts(127.0.0.1:443)``  True\n        ``127.0.0.1:443.conflicts(127.0.0.1:\\*)``  False\n        ``\\*:443.conflicts(\\*:80)``                False\n        ``_default_:443.conflicts(\\*:443)``        True\n        =========================================  =====\n\n        \"\"\"\n        if self._addr_less_specific(addr):\n            return True\n        elif self.get_addr() == addr.get_addr():\n            if self.is_wildcard() or self.get_port() == addr.get_port():\n                return True\n        return False\n\n    def is_wildcard(self) -> bool:\n        \"\"\"Returns if address has a wildcard port.\"\"\"\n        return self.tup[1] == \"*\" or not self.tup[1]\n\n    def get_sni_addr(self, port: str) -> common.Addr:\n        \"\"\"Returns the least specific address that resolves on the port.\n\n        Examples:\n\n        - ``1.2.3.4:443`` -> ``1.2.3.4:<port>``\n        - ``1.2.3.4:*`` -> ``1.2.3.4:*``\n\n        :param str port: Desired port\n\n        \"\"\"\n        if self.is_wildcard():\n            return self\n\n        return self.get_addr_obj(port)\n\n\nclass VirtualHost:\n    \"\"\"Represents an Apache Virtualhost.\n\n    :ivar str filep: file path of VH\n    :ivar str path: Augeas path to virtual host\n    :ivar set addrs: Virtual Host addresses (:class:`set` of\n        :class:`common.Addr`)\n    :ivar str name: ServerName of VHost\n    :ivar list aliases: Server aliases of vhost\n        (:class:`list` of :class:`str`)\n\n    :ivar bool ssl: SSLEngine on in vhost\n    :ivar bool enabled: Virtual host is enabled\n    :ivar bool modmacro: VirtualHost is using mod_macro\n    :ivar VirtualHost ancestor: A non-SSL VirtualHost this is based on\n\n    https://httpd.apache.org/docs/2.4/vhosts/details.html\n\n    .. todo:: Any vhost that includes the magic _default_ wildcard is given the\n              same ServerName as the main server.\n\n    \"\"\"\n    # ?: is used for not returning enclosed characters\n    strip_name: re.Pattern[str] = re.compile(r\"^(?:.+://)?([^ :$]*)\")\n\n    def __init__(self, filepath: str, path: str, addrs: set[\"Addr\"], ssl: bool,\n                 enabled: bool, name: Optional[str] = None, aliases: Optional[set[str]] = None,\n                 modmacro: bool = False, ancestor: Optional[\"VirtualHost\"] = None,\n                 node: Optional[Union[ApacheBlockNode, AugeasBlockNode, DualBlockNode]] = None\n                 ) -> None:\n\n        \"\"\"Initialize a VH.\"\"\"\n        self.filep = filepath\n        self.path = path\n        self.addrs = addrs\n        self.name = name\n        self.aliases = aliases if aliases is not None else set()\n        self.ssl = ssl\n        self.enabled = enabled\n        self.modmacro = modmacro\n        self.ancestor = ancestor\n        self.node = node\n\n    def get_names(self) -> set[str]:\n        \"\"\"Return a set of all names.\"\"\"\n        all_names: set[str] = set()\n        all_names.update(self.aliases)\n        # Strip out any scheme:// and <port> field from servername\n        if self.name is not None:\n            all_names.add(VirtualHost.strip_name.findall(self.name)[0])\n\n        return all_names\n\n    def __str__(self) -> str:\n        return (\n            f\"File: {self.filep}\\n\"\n            f\"Vhost path: {self.path}\\n\"\n            f\"Addresses: {', '.join(str(addr) for addr in self.addrs)}\\n\"\n            f\"Name: {self.name if self.name is not None else ''}\\n\"\n            f\"Aliases: {', '.join(name for name in self.aliases)}\\n\"\n            f\"TLS Enabled: {'Yes' if self.ssl else 'No'}\\n\"\n            f\"Site Enabled: {'Yes' if self.enabled else 'No'}\\n\"\n            f\"mod_macro Vhost: {'Yes' if self.modmacro else 'No'}\"\n        )\n\n    def display_repr(self) -> str:\n        \"\"\"Return a representation of VHost to be used in dialog\"\"\"\n        return (\n            f\"File: {self.filep}\\n\"\n            f\"Addresses: {', '.join(str(addr) for addr in self.addrs)}\\n\"\n            f\"Names: {', '.join(self.get_names())}\\n\"\n            f\"HTTPS: {'Yes' if self.ssl else 'No'}\\n\"\n        )\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.filep == other.filep and self.path == other.path and\n                    self.addrs == other.addrs and\n                    self.get_names() == other.get_names() and\n                    self.ssl == other.ssl and\n                    self.enabled == other.enabled and\n                    self.modmacro == other.modmacro)\n\n        return False\n\n    def __hash__(self) -> int:\n        return hash((self.filep, self.path,\n                     tuple(self.addrs), tuple(self.get_names()),\n                     self.ssl, self.enabled, self.modmacro))\n\n    def conflicts(self, addrs: Iterable[Addr]) -> bool:\n        \"\"\"See if vhost conflicts with any of the addrs.\n\n        This determines whether or not these addresses would/could overwrite\n        the vhost addresses.\n\n        :param addrs: Iterable Addresses\n        :type addrs: Iterable :class:~obj.Addr\n\n        :returns: If addresses conflicts with vhost\n        :rtype: bool\n\n        \"\"\"\n        for pot_addr in addrs:\n            for addr in self.addrs:\n                if addr.conflicts(pot_addr):\n                    return True\n        return False\n\n    def same_server(self, vhost: \"VirtualHost\", generic: bool = False) -> bool:\n        \"\"\"Determines if the vhost is the same 'server'.\n\n        Used in redirection - indicates whether or not the two virtual hosts\n        serve on the exact same IP combinations, but different ports.\n        The generic flag indicates that that we're trying to match to a\n        default or generic vhost\n\n        .. todo:: Handle _default_\n\n        \"\"\"\n\n        if not generic:\n            if vhost.get_names() != self.get_names():\n                return False\n\n            # If equal and set is not empty... assume same server\n            if self.name is not None or self.aliases:\n                return True\n        # If we're looking for a generic vhost,\n        # don't return one with a ServerName\n        elif self.name:\n            return False\n\n        # Both sets of names are empty.\n\n        # Make conservative educated guess... this is very restrictive\n        # Consider adding more safety checks.\n        if len(vhost.addrs) != len(self.addrs):\n            return False\n\n        # already_found acts to keep everything very conservative.\n        # Don't allow multiple ip:ports in same set.\n        already_found: set[str] = set()\n\n        for addr in vhost.addrs:\n            for local_addr in self.addrs:\n                if (local_addr.get_addr() == addr.get_addr() and\n                        local_addr != addr and\n                        local_addr.get_addr() not in already_found):\n\n                    # This intends to make sure we aren't double counting...\n                    # e.g. 127.0.0.1:* - We require same number of addrs\n                    #  currently\n                    already_found.add(local_addr.get_addr())\n                    break\n            else:\n                return False\n\n        return True\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_alpine.py",
    "content": "\"\"\" Distribution specific override class for Alpine Linux \"\"\"\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass AlpineConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Alpine Linux specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/apache2\",\n        vhost_root=\"/etc/apache2/conf.d\",\n        vhost_files=\"*.conf\",\n        logs_root=\"/var/log/apache2\",\n        ctl=\"apachectl\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/apache2/conf.d\",\n    )\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_arch.py",
    "content": "\"\"\" Distribution specific override class for Arch Linux \"\"\"\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass ArchConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Arch Linux specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/httpd\",\n        vhost_root=\"/etc/httpd/conf\",\n        vhost_files=\"*.conf\",\n        logs_root=\"/var/log/httpd\",\n        ctl=\"apachectl\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/httpd/conf\",\n    )\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_centos.py",
    "content": "\"\"\" Distribution specific override class for CentOS family (RHEL, Fedora) \"\"\"\nimport logging\nfrom typing import Any\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal.configurator import OsOptions\n\nlogger = logging.getLogger(__name__)\n\n\nclass CentOSConfigurator(configurator.ApacheConfigurator):\n    \"\"\"CentOS specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/httpd\",\n        vhost_root=\"/etc/httpd/conf.d\",\n        vhost_files=\"*.conf\",\n        logs_root=\"/var/log/httpd\",\n        ctl=\"apachectl\",\n        apache_bin=\"httpd\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        restart_cmd_alt=['apachectl', 'restart'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/httpd/conf.d\",\n    )\n\n    def config_test(self) -> None:\n        \"\"\"\n        Override config_test to mitigate configtest error in vanilla installation\n        of mod_ssl in Fedora. The error is caused by non-existent self-signed\n        certificates referenced by the configuration, that would be autogenerated\n        during the first (re)start of httpd.\n        \"\"\"\n\n        os_info = util.get_os_info()\n        fedora = os_info[0].lower() == \"fedora\"\n\n        try:\n            super().config_test()\n        except errors.MisconfigurationError:\n            if fedora:\n                self._try_restart_fedora()\n            else:\n                raise\n\n    def _rhel9_or_newer(self) -> bool:\n        os_name, os_version = util.get_os_info()\n        rhel_derived = os_name in [\n            \"centos\", \"centos linux\",\n            \"cloudlinux\",\n            \"ol\", \"oracle\",\n            \"rhel\", \"redhatenterpriseserver\", \"red hat enterprise linux server\",\n            \"scientific\", \"scientific linux\",\n        ]\n        # It is important that the loose version comparison below is not made\n        # if the OS is not RHEL derived. See\n        # https://github.com/certbot/certbot/issues/9481.\n        if not rhel_derived:\n            return False\n        at_least_v9 = util.parse_loose_version(os_version) >= util.parse_loose_version('9')\n        return at_least_v9\n\n    def _override_cmds(self) -> None:\n        super()._override_cmds()\n\n        # As of RHEL 9, apachectl can't be passed flags like \"-v\" or \"-t -D\", so\n        # instead use options.bin (i.e. httpd) for version_cmd and the various\n        # get_X commands\n        if self._rhel9_or_newer():\n            if not self.options.bin:\n                raise ValueError(\"OS option apache_bin must be set for CentOS\") # pragma: no cover\n\n            self.options.version_cmd[0] = self.options.bin\n            self.options.get_modules_cmd[0] = self.options.bin\n            self.options.get_includes_cmd[0] = self.options.bin\n            self.options.get_defines_cmd[0] = self.options.bin\n\n        if not self.options.restart_cmd_alt:  # pragma: no cover\n            raise ValueError(\"OS option restart_cmd_alt must be set for CentOS.\")\n        self.options.restart_cmd_alt[0] = self.options.ctl\n\n    def _try_restart_fedora(self) -> None:\n        \"\"\"\n        Tries to restart httpd using systemctl to generate the self signed key pair.\n        \"\"\"\n\n        try:\n            util.run_script(['systemctl', 'restart', 'httpd'])\n        except errors.SubprocessError as err:\n            raise errors.MisconfigurationError(str(err))\n\n        # Finish with actual config check to see if systemctl restart helped\n        super().config_test()\n\n    def get_parser(self) -> \"CentOSParser\":\n        \"\"\"Initializes the ApacheParser\"\"\"\n        return CentOSParser(\n            self.options.server_root, self, self.options.vhost_root, self.version)\n\n\nclass CentOSParser(parser.ApacheParser):\n    \"\"\"CentOS specific ApacheParser override class\"\"\"\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        # CentOS specific configuration file for Apache\n        self.sysconfig_filep: str = \"/etc/sysconfig/httpd\"\n        super().__init__(*args, **kwargs)\n\n    def update_runtime_variables(self) -> None:\n        \"\"\" Override for update_runtime_variables for custom parsing \"\"\"\n        # Opportunistic, works if SELinux not enforced\n        super().update_runtime_variables()\n        self.parse_sysconfig_var()\n\n    def parse_sysconfig_var(self) -> None:\n        \"\"\" Parses Apache CLI options from CentOS configuration file \"\"\"\n        defines = apache_util.parse_define_file(self.sysconfig_filep, \"OPTIONS\")\n        for k, v in defines.items():\n            self.variables[k] = v\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_darwin.py",
    "content": "\"\"\" Distribution specific override class for macOS \"\"\"\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass DarwinConfigurator(configurator.ApacheConfigurator):\n    \"\"\"macOS specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        vhost_root=\"/etc/apache2/other\",\n        vhost_files=\"*.conf\",\n        ctl=\"apachectl\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/apache2/other\",\n    )\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_debian.py",
    "content": "\"\"\" Distribution specific override class for Debian family (Ubuntu/Debian) \"\"\"\nimport logging\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\nfrom certbot_apache._internal.obj import VirtualHost\n\nlogger = logging.getLogger(__name__)\n\n\nclass DebianConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Debian specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        enmod=\"a2enmod\",\n        dismod=\"a2dismod\",\n        handle_modules=True,\n        handle_sites=True,\n    )\n\n    def enable_site(self, vhost: VirtualHost) -> None:\n        \"\"\"Enables an available site, Apache reload required.\n\n        .. note:: Does not make sure that the site correctly works or that all\n                  modules are enabled appropriately.\n\n        :param vhost: vhost to enable\n        :type vhost: :class:`~certbot_apache._internal.obj.VirtualHost`\n\n        :raises .errors.NotSupportedError: If filesystem layout is not\n            supported.\n\n        \"\"\"\n        if vhost.enabled:\n            return None\n\n        enabled_path = (\"%s/sites-enabled/%s\" %\n                        (self.parser.root,\n                         os.path.basename(vhost.filep)))\n        if not os.path.isdir(os.path.dirname(enabled_path)):\n            # For some reason, sites-enabled / sites-available do not exist\n            # Call the parent method\n            return super().enable_site(vhost)\n        self.reverter.register_file_creation(False, enabled_path)\n        try:\n            os.symlink(vhost.filep, enabled_path)\n        except OSError as err:\n            if os.path.islink(enabled_path) and filesystem.realpath(\n               enabled_path) == vhost.filep:\n                # Already in shape\n                vhost.enabled = True\n                return None\n            logger.error(\n                \"Could not symlink %s to %s, got error: %s\", enabled_path,\n                vhost.filep, err.strerror)\n            errstring = (\"Encountered error while trying to enable a \" +\n                         \"newly created VirtualHost located at {0} by \" +\n                         \"linking to it from {1}\")\n            raise errors.NotSupportedError(errstring.format(vhost.filep,\n                                                            enabled_path))\n        vhost.enabled = True\n        logger.info(\"Enabling available site: %s\", vhost.filep)\n        self.save_notes += \"Enabled site %s\\n\" % vhost.filep\n        return None\n\n    def enable_mod(self, mod_name: str, temp: bool = False) -> None:\n        \"\"\"Enables module in Apache.\n\n        Both enables and reloads Apache so module is active.\n\n        :param str mod_name: Name of the module to enable. (e.g. 'ssl')\n        :param bool temp: Whether or not this is a temporary action.\n\n        :raises .errors.NotSupportedError: If the filesystem layout is not\n            supported.\n        :raises .errors.MisconfigurationError: If a2enmod or a2dismod cannot be\n            run.\n\n        \"\"\"\n        avail_path = os.path.join(self.parser.root, \"mods-available\")\n        enabled_path = os.path.join(self.parser.root, \"mods-enabled\")\n        if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path):\n            raise errors.NotSupportedError(\n                \"Unsupported directory layout. You may try to enable mod %s \"\n                \"and try again.\" % mod_name)\n\n        deps = apache_util.get_mod_deps(mod_name)\n\n        # Enable all dependencies\n        for dep in deps:\n            if (dep + \"_module\") not in self.parser.modules:\n                self._enable_mod_debian(dep, temp)\n                self.parser.add_mod(dep)\n                note = \"Enabled dependency of %s module - %s\" % (mod_name, dep)\n                if not temp:\n                    self.save_notes += note + os.linesep\n                logger.debug(note)\n\n        # Enable actual module\n        self._enable_mod_debian(mod_name, temp)\n        self.parser.add_mod(mod_name)\n\n        if not temp:\n            self.save_notes += \"Enabled %s module in Apache\\n\" % mod_name\n        logger.info(\"Enabled Apache %s module\", mod_name)\n\n        # Modules can enable additional config files. Variables may be defined\n        # within these new configuration sections.\n        # Reload is not necessary as DUMP_RUN_CFG uses latest config.\n        self.parser.update_runtime_variables()\n\n    def _enable_mod_debian(self, mod_name: str, temp: bool) -> None:\n        \"\"\"Assumes mods-available, mods-enabled layout.\"\"\"\n        # Generate reversal command.\n        # Try to be safe here... check that we can probably reverse before\n        # applying enmod command\n        if (self.options.dismod is None or self.options.enmod is None\n                or not util.exe_exists(self.options.dismod)):\n            raise errors.MisconfigurationError(\n                \"Unable to find a2dismod, please make sure a2enmod and \"\n                \"a2dismod are configured correctly for certbot.\")\n\n        self.reverter.register_undo_command(temp, [self.options.dismod, \"-f\", mod_name])\n        util.run_script([self.options.enmod, mod_name])\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_fedora.py",
    "content": "\"\"\" Distribution specific override class for Fedora 29+ \"\"\"\nfrom typing import Any\n\nfrom certbot import errors\nfrom certbot import util\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass FedoraConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Fedora 29+ specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/httpd\",\n        vhost_root=\"/etc/httpd/conf.d\",\n        vhost_files=\"*.conf\",\n        logs_root=\"/var/log/httpd\",\n        ctl=\"httpd\",\n        version_cmd=['httpd', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        restart_cmd_alt=['apachectl', 'restart'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/httpd/conf.d\",\n    )\n\n    def config_test(self) -> None:\n        \"\"\"\n        Override config_test to mitigate configtest error in vanilla installation\n        of mod_ssl in Fedora. The error is caused by non-existent self-signed\n        certificates referenced by the configuration, that would be autogenerated\n        during the first (re)start of httpd.\n        \"\"\"\n        try:\n            super().config_test()\n        except errors.MisconfigurationError:\n            self._try_restart_fedora()\n\n    def get_parser(self) -> \"FedoraParser\":\n        \"\"\"Initializes the ApacheParser\"\"\"\n        return FedoraParser(\n            self.options.server_root, self, self.options.vhost_root, self.version)\n\n    def _try_restart_fedora(self) -> None:\n        \"\"\"\n        Tries to restart httpd using systemctl to generate the self signed key pair.\n        \"\"\"\n        try:\n            util.run_script(['systemctl', 'restart', 'httpd'])\n        except errors.SubprocessError as err:\n            raise errors.MisconfigurationError(str(err))\n\n        # Finish with actual config check to see if systemctl restart helped\n        super().config_test()\n\n    def _prepare_options(self) -> None:\n        \"\"\"\n        Override the options dictionary initialization to keep using apachectl\n        instead of httpd and so take advantages of this new bash script in newer versions\n        of Fedora to restart httpd.\n        \"\"\"\n        super()._prepare_options()\n        self.options.restart_cmd[0] = 'apachectl'\n        if not self.options.restart_cmd_alt:  # pragma: no cover\n            raise ValueError(\"OS option restart_cmd_alt must be set for Fedora.\")\n        self.options.restart_cmd_alt[0] = 'apachectl'\n        self.options.conftest_cmd[0] = 'apachectl'\n\n\nclass FedoraParser(parser.ApacheParser):\n    \"\"\"Fedora 29+ specific ApacheParser override class\"\"\"\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        # Fedora 29+ specific configuration file for Apache\n        self.sysconfig_filep = \"/etc/sysconfig/httpd\"\n        super().__init__(*args, **kwargs)\n\n    def update_runtime_variables(self) -> None:\n        \"\"\" Override for update_runtime_variables for custom parsing \"\"\"\n        # Opportunistic, works if SELinux not enforced\n        super().update_runtime_variables()\n        self._parse_sysconfig_var()\n\n    def _parse_sysconfig_var(self) -> None:\n        \"\"\" Parses Apache CLI options from Fedora configuration file \"\"\"\n        defines = apache_util.parse_define_file(self.sysconfig_filep, \"OPTIONS\")\n        for k, v in defines.items():\n            self.variables[k] = v\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_gentoo.py",
    "content": "\"\"\" Distribution specific override class for Gentoo Linux \"\"\"\nfrom typing import Any\n\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass GentooConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Gentoo specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/apache2\",\n        vhost_root=\"/etc/apache2/vhosts.d\",\n        vhost_files=\"*.conf\",\n        restart_cmd_alt=['apache2ctl', 'restart'],\n        challenge_location=\"/etc/apache2/vhosts.d\",\n    )\n\n    def _prepare_options(self) -> None:\n        \"\"\"\n        Override the options dictionary initialization in order to support\n        alternative restart cmd used in Gentoo.\n        \"\"\"\n        super()._prepare_options()\n        if not self.options.restart_cmd_alt:  # pragma: no cover\n            raise ValueError(\"OS option restart_cmd_alt must be set for Gentoo.\")\n        self.options.restart_cmd_alt[0] = self.options.ctl\n\n    def get_parser(self) -> \"GentooParser\":\n        \"\"\"Initializes the ApacheParser\"\"\"\n        return GentooParser(\n            self.options.server_root, self, self.options.vhost_root, self.version)\n\n\nclass GentooParser(parser.ApacheParser):\n    \"\"\"Gentoo specific ApacheParser override class\"\"\"\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        # Gentoo specific configuration file for Apache2\n        self.apacheconfig_filep = \"/etc/conf.d/apache2\"\n        super().__init__(*args, **kwargs)\n\n    def update_runtime_variables(self) -> None:\n        \"\"\" Override for update_runtime_variables for custom parsing \"\"\"\n        self.parse_sysconfig_var()\n        self.update_modules()\n\n    def parse_sysconfig_var(self) -> None:\n        \"\"\" Parses Apache CLI options from Gentoo configuration file \"\"\"\n        defines = apache_util.parse_define_file(self.apacheconfig_filep,\n                                                \"APACHE2_OPTS\")\n        for k, v in defines.items():\n            self.variables[k] = v\n\n    def update_modules(self) -> None:\n        \"\"\"Get loaded modules from httpd process, and add them to DOM\"\"\"\n        mod_cmd = [self.configurator.options.ctl, \"modules\"]\n        matches = apache_util.parse_from_subprocess(mod_cmd, r\"(.*)_module\")\n        for mod in matches:\n            self.add_mod(mod.strip())\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_suse.py",
    "content": "\"\"\" Distribution specific override class for OpenSUSE \"\"\"\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass OpenSUSEConfigurator(configurator.ApacheConfigurator):\n    \"\"\"OpenSUSE specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        vhost_root=\"/etc/apache2/vhosts.d\",\n        vhost_files=\"*.conf\",\n        ctl=\"apachectl\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        conftest_cmd=['apachectl', 'configtest'],\n        enmod=\"a2enmod\",\n        dismod=\"a2dismod\",\n        challenge_location=\"/etc/apache2/vhosts.d\",\n    )\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/override_void.py",
    "content": "\"\"\" Distribution specific override class for Void Linux \"\"\"\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal.configurator import OsOptions\n\n\nclass VoidConfigurator(configurator.ApacheConfigurator):\n    \"\"\"Void Linux specific ApacheConfigurator override class\"\"\"\n\n    OS_DEFAULTS = OsOptions(\n        server_root=\"/etc/apache\",\n        vhost_root=\"/etc/apache/extra\",\n        vhost_files=\"*.conf\",\n        logs_root=\"/var/log/httpd\",\n        ctl=\"apachectl\",\n        version_cmd=['apachectl', '-v'],\n        restart_cmd=['apachectl', 'graceful'],\n        conftest_cmd=['apachectl', 'configtest'],\n        challenge_location=\"/etc/apache/extra\",\n    )\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/parser.py",
    "content": "\"\"\"ApacheParser is a member object of the ApacheConfigurator class.\"\"\"\nimport copy\nimport fnmatch\nimport logging\nimport re\nfrom typing import Collection\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\nfrom typing import Union\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import constants\n\nif TYPE_CHECKING:\n    from certbot_apache._internal.configurator import ApacheConfigurator  # pragma: no cover\n\ntry:\n    from augeas import Augeas\nexcept ImportError:  # pragma: no cover\n    Augeas = None\n\nlogger = logging.getLogger(__name__)\n\n\nclass ApacheParser:\n    \"\"\"Class handles the fine details of parsing the Apache Configuration.\n\n    .. todo:: Make parsing general... remove sites-available etc...\n\n    :ivar str root: Normalized absolute path to the server root\n        directory. Without trailing slash.\n    :ivar set modules: All module names that are currently enabled.\n    :ivar dict loc: Location to place directives, root - configuration origin,\n        default - user config file, name - NameVirtualHost,\n\n    \"\"\"\n    arg_var_interpreter: re.Pattern[str] = re.compile(r\"\\$\\{[^ \\}]*}\")\n    fnmatch_chars: set[str] = {\"*\", \"?\", \"\\\\\", \"[\", \"]\"}\n\n    # pylint: disable=unused-argument\n    def __init__(self, root: str, configurator: \"ApacheConfigurator\",\n                 vhostroot: str, version: tuple[int, ...] = (2, 4)) -> None:\n        # Note: Order is important here.\n\n        # Needed for calling save() with reverter functionality that resides in\n        # AugeasConfigurator superclass of ApacheConfigurator. This resolves\n        # issues with aug.load() after adding new files / defines to parse tree\n        self.configurator = configurator\n\n        # Initialize augeas\n        self.aug: Augeas = init_augeas()\n\n        if not self.check_aug_version():\n            raise errors.NotSupportedError(\n                \"Apache plugin support requires libaugeas0 and augeas-lenses \"\n                \"version 1.2.0 or higher, please make sure you have you have \"\n                \"those installed.\")\n\n        self.modules: dict[str, Optional[str]] = {}\n        self.parser_paths: dict[str, list[str]] = {}\n        self.variables: dict[str, str] = {}\n\n        # Find configuration root and make sure augeas can parse it.\n        self.root: str = os.path.abspath(root)\n        self.loc: dict[str, str] = {\"root\": self._find_config_root()}\n        self.parse_file(self.loc[\"root\"])\n\n        # Look up variables from httpd and add to DOM if not already parsed\n        self.update_runtime_variables()\n\n        # This problem has been fixed in Augeas 1.0\n        self.standardize_excl()\n\n        # Parse LoadModule directives from configuration files\n        self.parse_modules()\n\n        # Set up rest of locations\n        self.loc.update(self._set_locations())\n\n        # list of the active include paths, before modifications\n        self.existing_paths = copy.deepcopy(self.parser_paths)\n\n        # Must also attempt to parse additional virtual host root\n        if vhostroot:\n            self.parse_file(os.path.abspath(vhostroot) + \"/\" +\n                            self.configurator.options.vhost_files)\n\n    def check_parsing_errors(self, lens: str) -> None:\n        \"\"\"Verify Augeas can parse all of the lens files.\n\n        :param str lens: lens to check for errors\n\n        :raises .errors.PluginError: If there has been an error in parsing with\n            the specified lens.\n\n        \"\"\"\n        error_files = self.aug.match(\"/augeas//error\")\n\n        for path in error_files:\n            # Check to see if it was an error resulting from the use of\n            # the httpd lens\n            lens_path = self.aug.get(path + \"/lens\")\n            # As aug.get may return null\n            if lens_path and lens in lens_path:\n                msg = (\n                    \"There has been an error in parsing the file {0} on line {1}: \"\n                    \"{2}\".format(\n                    # Strip off /augeas/files and /error\n                    path[13:len(path) - 6],\n                    self.aug.get(path + \"/line\"),\n                    self.aug.get(path + \"/message\")))\n                raise errors.PluginError(msg)\n\n    def check_aug_version(self) -> Union[bool, list[str]]:\n        \"\"\" Checks that we have recent enough version of libaugeas.\n        If augeas version is recent enough, it will support case insensitive\n        regexp matching\"\"\"\n\n        self.aug.set(\"/test/path/testing/arg\", \"aRgUMeNT\")\n        try:\n            matches: list[str] = self.aug.match(\n                \"/test//*[self::arg=~regexp('argument', 'i')]\")\n        except RuntimeError:\n            self.aug.remove(\"/test/path\")\n            return False\n        self.aug.remove(\"/test/path\")\n        return matches\n\n    def unsaved_files(self) -> set[str]:\n        \"\"\"Lists files that have modified Augeas DOM but the changes have not\n        been written to the filesystem yet, used by `self.save()` and\n        ApacheConfigurator to check the file state.\n\n        :raises .errors.PluginError: If there was an error in Augeas, in\n            an attempt to save the configuration, or an error creating a\n            checkpoint\n\n        :returns: `set` of unsaved files\n        \"\"\"\n        save_state = self.aug.get(\"/augeas/save\")\n        self.aug.set(\"/augeas/save\", \"noop\")\n        # Existing Errors\n        ex_errs = self.aug.match(\"/augeas//error\")\n        try:\n            # This is a noop save\n            self.aug.save()\n        except (OSError, RuntimeError):\n            self._log_save_errors(ex_errs)\n            # Erase Save Notes\n            self.configurator.save_notes = \"\"\n            raise errors.PluginError(\n                \"Error saving files, check logs for more info.\")\n\n        # Return the original save method\n        self.aug.set(\"/augeas/save\", save_state)\n\n        # Retrieve list of modified files\n        # Note: Noop saves can cause the file to be listed twice, I used a\n        # set to remove this possibility. This is a known augeas 0.10 error.\n        save_paths = self.aug.match(\"/augeas/events/saved\")\n\n        save_files = set()\n        if save_paths:\n            for path in save_paths:\n                save_files.add(self.aug.get(path)[6:])\n        return save_files\n\n    def ensure_augeas_state(self) -> None:\n        \"\"\"Makes sure that all Augeas dom changes are written to files to avoid\n        loss of configuration directives when doing additional augeas parsing,\n        causing a possible augeas.load() resulting dom reset\n        \"\"\"\n\n        if self.unsaved_files():\n            self.configurator.save_notes += \"(autosave)\"\n            self.configurator.save()\n\n    def save(self, save_files: Iterable[str]) -> None:\n        \"\"\"Saves all changes to the configuration files.\n\n        save() is called from ApacheConfigurator to handle the parser specific\n        tasks of saving.\n\n        :param list save_files: list of strings of file paths that we need to save.\n\n        \"\"\"\n        self.configurator.save_notes = \"\"\n\n        ex_errs = self.aug.match(\"/augeas//error\")\n        try:\n            self.aug.save()\n        except OSError:\n            self._log_save_errors(ex_errs)\n            raise\n\n        # Force reload if files were modified\n        # This is needed to recalculate augeas directive span\n        if save_files:\n            for sf in save_files:\n                self.aug.remove(\"/files/\"+sf)\n            self.aug.load()\n\n    def _log_save_errors(self, ex_errs: Iterable[str]) -> None:\n        \"\"\"Log errors due to bad Augeas save.\n\n        :param list ex_errs: Existing errors before save\n\n        \"\"\"\n        # Check for the root of save problems\n        new_errs = [e for e in self.aug.match(\"/augeas//error\") if e not in ex_errs]\n\n        for err in new_errs:\n            logger.debug(\n                \"Error %s saving %s: %s\", self.aug.get(err), err[13:len(err) - 6],\n                self.aug.get(f\"{err}/message\"))\n        logger.error(\n            \"Unable to save files: %s.%s\", \", \".join(err[13:len(err) - 6] for err in new_errs),\n            f\" Save Notes: {self.configurator.save_notes}\" if self.configurator.save_notes else \"\")\n\n    def add_include(self, main_config: str, inc_path: str) -> None:\n        \"\"\"Add Include for a new configuration file if one does not exist\n\n        :param str main_config: file path to main Apache config file\n        :param str inc_path: path of file to include\n\n        \"\"\"\n        if not self.find_dir(case_i(\"Include\"), inc_path):\n            logger.debug(\"Adding Include %s to %s\",\n                         inc_path, get_aug_path(main_config))\n            self.add_dir(\n                get_aug_path(main_config),\n                \"Include\", inc_path)\n\n            # Add new path to parser paths\n            new_dir = os.path.dirname(inc_path)\n            new_file = os.path.basename(inc_path)\n            self.existing_paths.setdefault(new_dir, []).append(new_file)\n\n    def add_mod(self, mod_name: str) -> None:\n        \"\"\"Shortcut for updating parser modules.\"\"\"\n        if mod_name + \"_module\" not in self.modules:\n            self.modules[mod_name + \"_module\"] = None\n        if \"mod_\" + mod_name + \".c\" not in self.modules:\n            self.modules[\"mod_\" + mod_name + \".c\"] = None\n\n    def reset_modules(self) -> None:\n        \"\"\"Reset the loaded modules list. This is called from cleanup to clear\n        temporarily loaded modules.\"\"\"\n        self.modules = {}\n        self.update_modules()\n        self.parse_modules()\n\n    def parse_modules(self) -> None:\n        \"\"\"Iterates on the configuration until no new modules are loaded.\n\n        ..todo:: This should be attempted to be done with a binary to avoid\n            the iteration issue.  Else... parse and enable mods at same time.\n\n        \"\"\"\n        mods: dict[str, str] = {}\n        matches = self.find_dir(\"LoadModule\")\n        iterator = iter(matches)\n        # Make sure prev_size != cur_size for do: while: iteration\n        prev_size = -1\n\n        while len(mods) != prev_size:\n            prev_size = len(mods)\n\n            for match_name, match_filename in zip(\n                    iterator, iterator):\n                mod_name = self.get_arg(match_name)\n                mod_filename = self.get_arg(match_filename)\n                if mod_name and mod_filename:\n                    mods[mod_name] = mod_filename\n                    mods[os.path.basename(mod_filename)[:-2] + \"c\"] = mod_filename\n                else:\n                    logger.debug(\"Could not read LoadModule directive from Augeas path: %s\",\n                                 match_name[6:])\n        self.modules.update(mods)\n\n    def update_runtime_variables(self) -> None:\n        \"\"\"Update Includes, Defines and Includes from httpd config dump data\"\"\"\n\n        self.update_defines()\n        self.update_includes()\n        self.update_modules()\n\n    def update_defines(self) -> None:\n        \"\"\"Updates the dictionary of known variables in the configuration\"\"\"\n        self.variables = apache_util.parse_defines(self.configurator.options.get_defines_cmd)\n\n    def update_includes(self) -> None:\n        \"\"\"Get includes from httpd process, and add them to DOM if needed\"\"\"\n\n        # Find_dir iterates over configuration for Include and IncludeOptional\n        # directives to make sure we see the full include tree present in the\n        # configuration files\n        _ = self.find_dir(\"Include\")\n\n        matches = apache_util.parse_includes(self.configurator.options.get_includes_cmd)\n        if matches:\n            for i in matches:\n                if not self.parsed_in_current(i):\n                    self.parse_file(i)\n\n    def update_modules(self) -> None:\n        \"\"\"Get loaded modules from httpd process, and add them to DOM\"\"\"\n\n        matches = apache_util.parse_modules(self.configurator.options.get_modules_cmd)\n        for mod in matches:\n            self.add_mod(mod.strip())\n\n    def filter_args_num(self, matches: str, args: int) -> list[str]:\n        \"\"\"Filter out directives with specific number of arguments.\n\n        This function makes the assumption that all related arguments are given\n        in order.  Thus /files/apache/directive[5]/arg[2] must come immediately\n        after /files/apache/directive[5]/arg[1]. Runs in 1 linear pass.\n\n        :param str matches: Matches of all directives with arg nodes\n        :param int args: Number of args you would like to filter\n\n        :returns: List of directives that contain # of arguments.\n            (arg is stripped off)\n\n        \"\"\"\n        filtered: list[str] = []\n        if args == 1:\n            for i, match in enumerate(matches):\n                if match.endswith(\"/arg\"):\n                    filtered.append(matches[i][:-4])\n        else:\n            for i, match in enumerate(matches):\n                if match.endswith(\"/arg[%d]\" % args):\n                    # Make sure we don't cause an IndexError (end of list)\n                    # Check to make sure arg + 1 doesn't exist\n                    if (i == (len(matches) - 1) or\n                            not matches[i + 1].endswith(\"/arg[%d]\" %\n                                                        (args + 1))):\n                        filtered.append(matches[i][:-len(\"/arg[%d]\" % args)])\n\n        return filtered\n\n    def add_dir_to_ifmodssl(self, aug_conf_path: str, directive: str, args: list[str]) -> None:\n        \"\"\"Adds directive and value to IfMod ssl block.\n\n        Adds given directive and value along configuration path within\n        an IfMod mod_ssl.c block.  If the IfMod block does not exist in\n        the file, it is created.\n\n        :param str aug_conf_path: Desired Augeas config path to add directive\n        :param str directive: Directive you would like to add, e.g. Listen\n        :param args: Values of the directive; list of str (eg. [\"443\"])\n        :type args: list\n\n        \"\"\"\n        # TODO: Add error checking code... does the path given even exist?\n        #       Does it throw exceptions?\n        if_mod_path = self.get_ifmod(aug_conf_path, \"mod_ssl.c\")\n        # IfModule can have only one valid argument, so append after\n        self.aug.insert(if_mod_path + \"arg\", \"directive\", False)\n        nvh_path = if_mod_path + \"directive[1]\"\n        self.aug.set(nvh_path, directive)\n        if len(args) == 1:\n            self.aug.set(nvh_path + \"/arg\", args[0])\n        else:\n            for i, arg in enumerate(args):\n                self.aug.set(\"%s/arg[%d]\" % (nvh_path, i + 1), arg)\n\n    def get_ifmod(self, aug_conf_path: str, mod: str) -> str:\n        \"\"\"Returns the path to <IfMod mod> and creates one if it doesn't exist.\n\n        :param str aug_conf_path: Augeas configuration path\n        :param str mod: module ie. mod_ssl.c\n        :param bool beginning: If the IfModule should be created to the beginning\n            of augeas path DOM tree.\n\n        :returns: Augeas path of the requested IfModule directive that pre-existed\n            or was created during the process. The path may be dynamic,\n            i.e. .../IfModule[last()]\n        :rtype: str\n\n        \"\"\"\n        if_mods: list[str] = self.aug.match((\"%s/IfModule/*[self::arg='%s']\" %\n                                  (aug_conf_path, mod)))\n        if not if_mods:\n            return self.create_ifmod(aug_conf_path, mod)\n\n        # Strip off \"arg\" at end of first ifmod path\n        return if_mods[0].rpartition(\"arg\")[0]\n\n    def create_ifmod(self, aug_conf_path: str, mod: str) -> str:\n        \"\"\"Creates a new <IfMod mod> and returns its path.\n\n        :param str aug_conf_path: Augeas configuration path\n        :param str mod: module ie. mod_ssl.c\n\n        :returns: Augeas path of the newly created IfModule directive.\n            The path may be dynamic, i.e. .../IfModule[last()]\n        :rtype: str\n\n        \"\"\"\n        c_path = \"{}/IfModule[last() + 1]\".format(aug_conf_path)\n        c_path_arg = \"{}/IfModule[last()]/arg\".format(aug_conf_path)\n        self.aug.set(c_path, \"\")\n        retpath = \"{}/IfModule[last()]/\".format(aug_conf_path)\n        self.aug.set(c_path_arg, mod)\n        return retpath\n\n    def add_dir(\n        self, aug_conf_path: Optional[str], directive: Optional[str], args: Union[list[str], str]\n    ) -> None:\n        \"\"\"Appends directive to the end of the file given by aug_conf_path.\n\n        .. note:: Not added to AugeasConfigurator because it may depend\n            on the lens\n\n        :param str aug_conf_path: Augeas configuration path to add directive\n        :param str directive: Directive to add\n        :param args: Value of the directive. ie. Listen 443, 443 is arg\n        :type args: list or str\n\n        \"\"\"\n        aug_conf_path = aug_conf_path if aug_conf_path else \"\"\n        self.aug.set(aug_conf_path + \"/directive[last() + 1]\", directive)\n        if isinstance(args, list):\n            for i, value in enumerate(args, 1):\n                self.aug.set(\n                    \"%s/directive[last()]/arg[%d]\" % (aug_conf_path, i), value)\n        else:\n            self.aug.set(aug_conf_path + \"/directive[last()]/arg\", args)\n\n    def add_dir_beginning(self, aug_conf_path: Optional[str], dirname: str,\n                          args: Union[list[str], str]) -> None:\n        \"\"\"Adds the directive to the beginning of defined aug_conf_path.\n\n        :param str aug_conf_path: Augeas configuration path to add directive\n        :param str dirname: Directive to add\n        :param args: Value of the directive. ie. Listen 443, 443 is arg\n        :type args: list or str\n        \"\"\"\n        aug_conf_path = aug_conf_path if aug_conf_path else \"\"\n        first_dir = aug_conf_path + \"/directive[1]\"\n        if self.aug.get(first_dir):\n            self.aug.insert(first_dir, \"directive\", True)\n        else:\n            self.aug.set(first_dir, \"directive\")\n\n        self.aug.set(first_dir, dirname)\n        if isinstance(args, list):\n            for i, value in enumerate(args, 1):\n                self.aug.set(first_dir + \"/arg[%d]\" % (i), value)\n        else:\n            self.aug.set(first_dir + \"/arg\", args)\n\n    def add_comment(self, aug_conf_path: str, comment: str) -> None:\n        \"\"\"Adds the comment to the augeas path\n\n        :param str aug_conf_path: Augeas configuration path to add directive\n        :param str comment: Comment content\n\n        \"\"\"\n        self.aug.set(aug_conf_path + \"/#comment[last() + 1]\", comment)\n\n    def find_comments(self, arg: str, start: Optional[str] = None) -> list[str]:\n        \"\"\"Finds a comment with specified content from the provided DOM path\n\n        :param str arg: Comment content to search\n        :param str start: Beginning Augeas path to begin looking\n\n        :returns: List of augeas paths containing the comment content\n        :rtype: list\n\n        \"\"\"\n        if not start:\n            start = get_aug_path(self.root)\n\n        comments = self.aug.match(\"%s//*[label() = '#comment']\" % start)\n\n        results = []\n        for comment in comments:\n            c_content = self.aug.get(comment)\n            if c_content and arg in c_content:\n                results.append(comment)\n        return results\n\n    def find_dir(self, directive: str, arg: Optional[str] = None,\n                 start: Optional[str] = None, exclude: bool = True) -> list[str]:\n        \"\"\"Finds directive in the configuration.\n\n        Recursively searches through config files to find directives\n        Directives should be in the form of a case insensitive regex currently\n\n        .. todo:: arg should probably be a list\n        .. todo:: arg search currently only supports direct matching. It does\n            not handle the case of variables or quoted arguments. This should\n            be adapted to use a generic search for the directive and then do a\n            case-insensitive self.get_arg filter\n\n        Note: Augeas is inherently case sensitive while Apache is case\n        insensitive.  Augeas 1.0 allows case insensitive regexes like\n        regexp(/Listen/, \"i\"), however the version currently supported\n        by Ubuntu 0.10 does not.  Thus I have included my own case insensitive\n        transformation by calling case_i() on everything to maintain\n        compatibility.\n\n        :param str directive: Directive to look for\n        :param arg: Specific value directive must have, None if all should\n                    be considered\n        :type arg: str or None\n\n        :param str start: Beginning Augeas path to begin looking\n        :param bool exclude: Whether or not to exclude directives based on\n            variables and enabled modules\n\n        :rtype list\n\n        \"\"\"\n        # Cannot place member variable in the definition of the function so...\n        if not start:\n            start = get_aug_path(self.loc[\"root\"])\n\n        # No regexp code\n        # if arg is None:\n        #     matches = self.aug.match(start +\n        # \"//*[self::directive='\" + directive + \"']/arg\")\n        # else:\n        #     matches = self.aug.match(start +\n        # \"//*[self::directive='\" + directive +\n        #   \"']/* [self::arg='\" + arg + \"']\")\n\n        # includes = self.aug.match(start +\n        # \"//* [self::directive='Include']/* [label()='arg']\")\n\n        regex = \"(%s)|(%s)|(%s)\" % (case_i(directive),\n                                    case_i(\"Include\"),\n                                    case_i(\"IncludeOptional\"))\n        matches = self.aug.match(\n            \"%s//*[self::directive=~regexp('%s')]\" % (start, regex))\n\n        if exclude:\n            matches = self.exclude_dirs(matches)\n\n        if arg is None:\n            arg_suffix = \"/arg\"\n        else:\n            arg_suffix = \"/*[self::arg=~regexp('%s')]\" % case_i(arg)\n\n        ordered_matches: list[str] = []\n\n        # TODO: Wildcards should be included in alphabetical order\n        # https://httpd.apache.org/docs/2.4/mod/core.html#include\n        for match in matches:\n            dir_ = self.aug.get(match).lower()\n            if dir_ in (\"include\", \"includeoptional\"):\n                ordered_matches.extend(self.find_dir(\n                    directive, arg,\n                    self._get_include_path(self.get_arg(match + \"/arg\")),\n                    exclude))\n            # This additionally allows Include\n            if dir_ == directive.lower():\n                ordered_matches.extend(self.aug.match(match + arg_suffix))\n\n        return ordered_matches\n\n    def get_arg(self, match: str) -> Optional[str]:\n        \"\"\"Uses augeas.get to get argument value and interprets result.\n\n        This also converts all variables and parameters appropriately.\n\n        \"\"\"\n        value: str = self.aug.get(match)\n\n        # No need to strip quotes for variables, as apache2ctl already does\n        # this, but we do need to strip quotes for all normal arguments.\n\n        # Note: normal argument may be a quoted variable\n        # e.g. strip now, not later\n        if not value:\n            return None\n\n        value = value.strip(\"'\\\"\")\n\n        variables = ApacheParser.arg_var_interpreter.findall(value)\n\n        for var in variables:\n            # Strip off ${ and }\n            try:\n                value = value.replace(var, self.variables[var[2:-1]])\n            except KeyError:\n                raise errors.PluginError(\"Error Parsing variable: %s\" % var)\n\n        return value\n\n    def get_root_augpath(self) -> str:\n        \"\"\"\n        Returns the Augeas path of root configuration.\n        \"\"\"\n        return get_aug_path(self.loc[\"root\"])\n\n    def exclude_dirs(self, matches: Iterable[str]) -> list[str]:\n        \"\"\"Exclude directives that are not loaded into the configuration.\"\"\"\n        filters = [(\"ifmodule\", self.modules.keys()), (\"ifdefine\", self.variables)]\n\n        valid_matches = []\n\n        for match in matches:\n            for filter_ in filters:\n                if not self._pass_filter(match, filter_):\n                    break\n            else:\n                valid_matches.append(match)\n        return valid_matches\n\n    def _pass_filter(self, match: str, filter_: tuple[str, Collection[str]]) -> bool:\n        \"\"\"Determine if directive passes a filter.\n\n        :param str match: Augeas path\n        :param list filter: list of tuples of form\n            [(\"lowercase if directive\", set of relevant parameters)]\n\n        \"\"\"\n        match_l = match.lower()\n        last_match_idx = match_l.find(filter_[0])\n\n        while last_match_idx != -1:\n            # Check args\n            end_of_if = match_l.find(\"/\", last_match_idx)\n            # This should be aug.get (vars are not used e.g. parser.aug_get)\n            expression = self.aug.get(match[:end_of_if] + \"/arg\")\n\n            if expression.startswith(\"!\"):\n                # Strip off \"!\"\n                if expression[1:] in filter_[1]:\n                    return False\n            else:\n                if expression not in filter_[1]:\n                    return False\n\n            last_match_idx = match_l.find(filter_[0], end_of_if)\n\n        return True\n\n    def standard_path_from_server_root(self, arg: str) -> str:\n        \"\"\"Ensure paths are consistent and absolute\n\n        :param str arg: Argument of directive\n\n        :returns: Standardized argument path\n        :rtype: str\n        \"\"\"\n        # Remove beginning and ending quotes\n        arg = arg.strip(\"'\\\"\")\n\n        # Standardize the include argument based on server root\n        if not arg.startswith(\"/\"):\n            # Normpath will condense ../\n            arg = os.path.normpath(os.path.join(self.root, arg))\n        else:\n            arg = os.path.normpath(arg)\n        return arg\n\n    def _get_include_path(self, arg: Optional[str]) -> Optional[str]:\n        \"\"\"Converts an Apache Include directive into Augeas path.\n\n        Converts an Apache Include directive argument into an Augeas\n        searchable path\n\n        .. todo:: convert to use os.path.join()\n\n        :param str arg: Argument of Include directive\n\n        :returns: Augeas path string\n        :rtype: str\n\n        \"\"\"\n        # Check to make sure only expected characters are used <- maybe remove\n        # validChars = re.compile(\"[a-zA-Z0-9.*?_-/]*\")\n        # matchObj = validChars.match(arg)\n        # if matchObj.group() != arg:\n        #     logger.error(\"Error: Invalid regexp characters in %s\", arg)\n        #     return []\n        if arg is None:\n            return None  # pragma: no cover\n        arg = self.standard_path_from_server_root(arg)\n\n        # Attempts to add a transform to the file if one does not already exist\n        if os.path.isdir(arg):\n            self.parse_file(os.path.join(arg, \"*\"))\n        else:\n            self.parse_file(arg)\n\n        # Argument represents an fnmatch regular expression, convert it\n        # Split up the path and convert each into an Augeas accepted regex\n        # then reassemble\n        split_arg = arg.split(\"/\")\n        for idx, split in enumerate(split_arg):\n            if any(char in ApacheParser.fnmatch_chars for char in split):\n                # Turn it into an augeas regex\n                # TODO: Can this instead be an augeas glob instead of regex\n                split_arg[idx] = (\"* [label()=~regexp('%s')]\" %\n                                  self.fnmatch_to_re(split))\n        # Reassemble the argument\n        # Note: This also normalizes the argument /serverroot/ -> /serverroot\n        arg = \"/\".join(split_arg)\n\n        return get_aug_path(arg)\n\n    def fnmatch_to_re(self, clean_fn_match: str) -> str:\n        \"\"\"Method converts Apache's basic fnmatch to regular expression.\n\n        Assumption - Configs are assumed to be well-formed and only writable by\n        privileged users.\n\n        https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html\n\n        :param str clean_fn_match: Apache style filename match, like globs\n\n        :returns: regex suitable for augeas\n        :rtype: str\n\n        \"\"\"\n        # Since Python 3.6, it returns a different pattern like (?s:.*\\.load)\\Z\n        return fnmatch.translate(clean_fn_match)[4:-3]  # pragma: no cover\n\n    def parse_file(self, filepath: str) -> None:\n        \"\"\"Parse file with Augeas\n\n        Checks to see if file_path is parsed by Augeas\n        If filepath isn't parsed, the file is added and Augeas is reloaded\n\n        :param str filepath: Apache config file path\n\n        \"\"\"\n        use_new, remove_old = self._check_path_actions(filepath)\n        # Ensure that we have the latest Augeas DOM state on disk before\n        # calling aug.load() which reloads the state from disk\n        self.ensure_augeas_state()\n        # Test if augeas included file for Httpd.lens\n        # Note: This works for augeas globs, ie. *.conf\n        if use_new:\n            inc_test = self.aug.match(\n                \"/augeas/load/Httpd['%s' =~ glob(incl)]\" % filepath)\n            if not inc_test:\n                # Load up files\n                # This doesn't seem to work on TravisCI\n                # self.aug.add_transform(\"Httpd.lns\", [filepath])\n                if remove_old:\n                    self._remove_httpd_transform(filepath)\n                self._add_httpd_transform(filepath)\n                self.aug.load()\n\n    def parsed_in_current(self, filep: Optional[str]) -> bool:\n        \"\"\"Checks if the file path is parsed by current Augeas parser config\n        ie. returns True if the file is found on a path that's found in live\n        Augeas configuration.\n\n        :param str filep: Path to match\n\n        :returns: True if file is parsed in existing configuration tree\n        :rtype: bool\n        \"\"\"\n        if not filep:\n            return False  # pragma: no cover\n        return self._parsed_by_parser_paths(filep, self.parser_paths)\n\n    def parsed_in_original(self, filep: Optional[str]) -> bool:\n        \"\"\"Checks if the file path is parsed by existing Apache config.\n        ie. returns True if the file is found on a path that matches Include or\n        IncludeOptional statement in the Apache configuration.\n\n        :param str filep: Path to match\n\n        :returns: True if file is parsed in existing configuration tree\n        :rtype: bool\n        \"\"\"\n        if not filep:\n            return False  # pragma: no cover\n        return self._parsed_by_parser_paths(filep, self.existing_paths)\n\n    def _parsed_by_parser_paths(self, filep: str, paths: Mapping[str, list[str]]) -> bool:\n        \"\"\"Helper function that searches through provided paths and returns\n        True if file path is found in the set\"\"\"\n        for directory in paths:\n            for filename in paths[directory]:\n                if fnmatch.fnmatch(filep, os.path.join(directory, filename)):\n                    return True\n        return False\n\n    def _check_path_actions(self, filepath: str) -> tuple[bool, bool]:\n        \"\"\"Determine actions to take with a new augeas path\n\n        This helper function will return a tuple that defines\n        if we should try to append the new filepath to augeas\n        parser paths, and / or remove the old one with more\n        narrow matching.\n\n        :param str filepath: filepath to check the actions for\n\n        \"\"\"\n\n        try:\n            new_file_match = os.path.basename(filepath)\n            existing_matches = self.parser_paths[os.path.dirname(filepath)]\n            if \"*\" in existing_matches:\n                use_new = False\n            else:\n                use_new = True\n            remove_old = new_file_match == \"*\"\n        except KeyError:\n            use_new = True\n            remove_old = False\n        return use_new, remove_old\n\n    def _remove_httpd_transform(self, filepath: str) -> None:\n        \"\"\"Remove path from Augeas transform\n\n        :param str filepath: filepath to remove\n        \"\"\"\n\n        remove_basenames = self.parser_paths[os.path.dirname(filepath)]\n        remove_dirname = os.path.dirname(filepath)\n        for name in remove_basenames:\n            remove_path = remove_dirname + \"/\" + name\n            remove_inc = self.aug.match(\n                \"/augeas/load/Httpd/incl [. ='%s']\" % remove_path)\n            self.aug.remove(remove_inc[0])\n        self.parser_paths.pop(remove_dirname)\n\n    def _add_httpd_transform(self, incl: str) -> None:\n        \"\"\"Add a transform to Augeas.\n\n        This function will correctly add a transform to augeas\n        The existing augeas.add_transform in python doesn't seem to work for\n        Travis CI as it loads in libaugeas.so.0.10.0\n\n        :param str incl: filepath to include for transform\n\n        \"\"\"\n        last_include: str = self.aug.match(\"/augeas/load/Httpd/incl [last()]\")\n        if last_include:\n            # Insert a new node immediately after the last incl\n            self.aug.insert(last_include[0], \"incl\", False)\n            self.aug.set(\"/augeas/load/Httpd/incl[last()]\", incl)\n        # On first use... must load lens and add file to incl\n        else:\n            # Augeas uses base 1 indexing... insert at beginning...\n            self.aug.set(\"/augeas/load/Httpd/lens\", \"Httpd.lns\")\n            self.aug.set(\"/augeas/load/Httpd/incl\", incl)\n        # Add included path to paths dictionary\n        try:\n            self.parser_paths[os.path.dirname(incl)].append(\n                os.path.basename(incl))\n        except KeyError:\n            self.parser_paths[os.path.dirname(incl)] = [\n                os.path.basename(incl)]\n\n    def standardize_excl(self) -> None:\n        \"\"\"Standardize the excl arguments for the Httpd lens in Augeas.\n\n        Note: Hack!\n        Standardize the excl arguments for the Httpd lens in Augeas\n        Servers sometimes give incorrect defaults\n        Note: This problem should be fixed in Augeas 1.0.  Unfortunately,\n        Augeas 0.10 appears to be the most popular version currently.\n\n        \"\"\"\n        # attempt to protect against augeas error in 0.10.0 - ubuntu\n        # *.augsave -> /*.augsave upon augeas.load()\n        # Try to avoid bad httpd files\n        # There has to be a better way... but after a day and a half of testing\n        # I had no luck\n        # This is a hack... work around... submit to augeas if still not fixed\n\n        excl = [\"*.augnew\", \"*.augsave\", \"*.dpkg-dist\", \"*.dpkg-bak\",\n                \"*.dpkg-new\", \"*.dpkg-old\", \"*.rpmsave\", \"*.rpmnew\",\n                \"*~\",\n                self.root + \"/*.augsave\",\n                self.root + \"/*~\",\n                self.root + \"/*/*augsave\",\n                self.root + \"/*/*~\",\n                self.root + \"/*/*/*.augsave\",\n                self.root + \"/*/*/*~\"]\n\n        for i, excluded in enumerate(excl, 1):\n            self.aug.set(\"/augeas/load/Httpd/excl[%d]\" % i, excluded)\n\n        self.aug.load()\n\n    def _set_locations(self) -> dict[str, str]:\n        \"\"\"Set default location for directives.\n\n        Locations are given as file_paths\n        .. todo:: Make sure that files are included\n\n        \"\"\"\n        default: str = self.loc[\"root\"]\n\n        temp: str = os.path.join(self.root, \"ports.conf\")\n        if os.path.isfile(temp):\n            listen = temp\n            name = temp\n        else:\n            listen = default\n            name = default\n\n        return {\"default\": default, \"listen\": listen, \"name\": name}\n\n    def _find_config_root(self) -> str:\n        \"\"\"Find the Apache Configuration Root file.\"\"\"\n        location = [\"apache2.conf\", \"httpd.conf\", \"conf/httpd.conf\"]\n        for name in location:\n            if os.path.isfile(os.path.join(self.root, name)):\n                return os.path.join(self.root, name)\n        raise errors.NoInstallationError(\"Could not find configuration root\")\n\n\ndef case_i(string: str) -> str:\n    \"\"\"Returns case insensitive regex.\n\n    Returns a sloppy, but necessary version of a case insensitive regex.\n    Any string should be able to be submitted and the string is\n    escaped and then made case insensitive.\n    May be replaced by a more proper /i once augeas 1.0 is widely\n    supported.\n\n    :param str string: string to make case i regex\n\n    \"\"\"\n    return \"\".join(\"[\" + c.upper() + c.lower() + \"]\"\n                    if c.isalpha() else c for c in re.escape(string))\n\n\ndef get_aug_path(file_path: str) -> str:\n    \"\"\"Return augeas path for full filepath.\n\n    :param str file_path: Full filepath\n\n    \"\"\"\n    return \"/files%s\" % file_path\n\n\ndef init_augeas() -> Augeas:\n    \"\"\" Initialize the actual Augeas instance \"\"\"\n\n    if not Augeas:  # pragma: no cover\n        raise errors.NoInstallationError(\"Problem in Augeas installation\")\n\n    return Augeas(\n        # specify a directory to load our preferred lens from\n        loadpath=constants.AUGEAS_LENS_DIR,\n        # Do not save backup (we do it ourselves), do not load\n        # anything by default\n        flags=(Augeas.NONE |\n               Augeas.NO_MODL_AUTOLOAD |\n               Augeas.ENABLE_SPAN))\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/parsernode_util.py",
    "content": "\"\"\"ParserNode utils\"\"\"\nfrom typing import Any\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot_apache._internal.interfaces import ParserNode\n\n\ndef validate_kwargs(kwargs: dict[str, Any], required_names: Iterable[str]) -> dict[str, Any]:\n    \"\"\"\n    Ensures that the kwargs dict has all the expected values. This function modifies\n    the kwargs dictionary, and hence the returned dictionary should be used instead\n    in the caller function instead of the original kwargs.\n\n    :param dict kwargs: Dictionary of keyword arguments to validate.\n    :param list required_names: List of required parameter names.\n    \"\"\"\n\n    validated_kwargs: dict[str, Any] = {}\n    for name in required_names:\n        try:\n            validated_kwargs[name] = kwargs.pop(name)\n        except KeyError:\n            raise TypeError(\"Required keyword argument: {} undefined.\".format(name))\n\n    # Raise exception if unknown key word arguments are found.\n    if kwargs:\n        unknown = \", \".join(kwargs.keys())\n        raise TypeError(\"Unknown keyword argument(s): {}\".format(unknown))\n    return validated_kwargs\n\n\ndef parsernode_kwargs(kwargs: dict[str, Any]\n                      ) -> tuple[Optional[ParserNode], bool, Optional[str], dict[str, Any]]:\n    \"\"\"\n    Validates keyword arguments for ParserNode. This function modifies the kwargs\n    dictionary, and hence the returned dictionary should be used instead in the\n    caller function instead of the original kwargs.\n\n    If metadata is provided, the otherwise required argument \"filepath\" may be\n    omitted if the implementation is able to extract its value from the metadata.\n    This usecase is handled within this function. Filepath defaults to None.\n\n    :param dict kwargs: Keyword argument dictionary to validate.\n\n    :returns: Tuple of validated and prepared arguments.\n    \"\"\"\n\n    # As many values of ParserNode instances can be derived from the metadata,\n    # (ancestor being a common exception here) make sure we permit it here as well.\n    if \"metadata\" in kwargs:\n        # Filepath can be derived from the metadata in Augeas implementation.\n        # Default is None, as in this case the responsibility of populating this\n        # variable lies on the implementation.\n        kwargs.setdefault(\"filepath\", None)\n\n    kwargs.setdefault(\"dirty\", False)\n    kwargs.setdefault(\"metadata\", {})\n\n    kwargs = validate_kwargs(kwargs, [\"ancestor\", \"dirty\", \"filepath\", \"metadata\"])\n    return kwargs[\"ancestor\"], kwargs[\"dirty\"], kwargs[\"filepath\"], kwargs[\"metadata\"]\n\n\ndef commentnode_kwargs(kwargs: dict[str, Any]) -> tuple[Optional[str], dict[str, str]]:\n    \"\"\"\n    Validates keyword arguments for CommentNode and sets the default values for\n    optional kwargs. This function modifies the kwargs dictionary, and hence the\n    returned dictionary should be used instead in the caller function instead of\n    the original kwargs.\n\n    If metadata is provided, the otherwise required argument \"comment\" may be\n    omitted if the implementation is able to extract its value from the metadata.\n    This usecase is handled within this function.\n\n    :param dict kwargs: Keyword argument dictionary to validate.\n\n    :returns: Tuple of validated and prepared arguments and ParserNode kwargs.\n    \"\"\"\n\n    # As many values of ParserNode instances can be derived from the metadata,\n    # (ancestor being a common exception here) make sure we permit it here as well.\n    if \"metadata\" in kwargs:\n        kwargs.setdefault(\"comment\", None)\n        # Filepath can be derived from the metadata in Augeas implementation.\n        # Default is None, as in this case the responsibility of populating this\n        # variable lies on the implementation.\n        kwargs.setdefault(\"filepath\", None)\n\n    kwargs.setdefault(\"dirty\", False)\n    kwargs.setdefault(\"metadata\", {})\n\n    kwargs = validate_kwargs(kwargs, [\"ancestor\", \"dirty\", \"filepath\", \"comment\",\n                                      \"metadata\"])\n\n    comment = kwargs.pop(\"comment\")\n    return comment, kwargs\n\n\ndef directivenode_kwargs(kwargs: dict[str, Any]\n                         ) -> tuple[Optional[str], tuple[str, ...], bool, dict[str, Any]]:\n    \"\"\"\n    Validates keyword arguments for DirectiveNode and BlockNode and sets the\n    default values for optional kwargs. This function modifies the kwargs\n    dictionary, and hence the returned dictionary should be used instead in the\n    caller function instead of the original kwargs.\n\n    If metadata is provided, the otherwise required argument \"name\" may be\n    omitted if the implementation is able to extract its value from the metadata.\n    This usecase is handled within this function.\n\n    :param dict kwargs: Keyword argument dictionary to validate.\n\n    :returns: Tuple of validated and prepared arguments and ParserNode kwargs.\n    \"\"\"\n\n    # As many values of ParserNode instances can be derived from the metadata,\n    # (ancestor being a common exception here) make sure we permit it here as well.\n    if \"metadata\" in kwargs:\n        kwargs.setdefault(\"name\", None)\n        # Filepath can be derived from the metadata in Augeas implementation.\n        # Default is None, as in this case the responsibility of populating this\n        # variable lies on the implementation.\n        kwargs.setdefault(\"filepath\", None)\n\n    kwargs.setdefault(\"dirty\", False)\n    kwargs.setdefault(\"enabled\", True)\n    kwargs.setdefault(\"parameters\", ())\n    kwargs.setdefault(\"metadata\", {})\n\n    kwargs = validate_kwargs(kwargs, [\"ancestor\", \"dirty\", \"filepath\", \"name\",\n                                      \"parameters\", \"enabled\", \"metadata\"])\n\n    name = kwargs.pop(\"name\")\n    parameters = kwargs.pop(\"parameters\")\n    enabled = kwargs.pop(\"enabled\")\n    return name, parameters, enabled, kwargs\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-apache tests\"\"\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/NEEDED.txt",
    "content": "Issues for which some kind of test case should be constructable, but we do not\ncurrently have one:\n\nhttps://github.com/certbot/certbot/issues/1213\nhttps://github.com/certbot/certbot/issues/1602\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test",
    "content": "#!/bin/bash\n\n# A hackish script to see if the client is behaving as expected \n# with each of the \"passing\" conf files.\n\nif [ -z \"$SERVER\" ]; then\n    echo \"Please set SERVER to the ACME server's directory URL.\"\n    exit 1\nfi\n\nexport EA=/etc/apache2/ \nTESTDIR=\"`dirname $0`\"\ncd $TESTDIR/passing\n\nfunction CleanupExit() {\n    echo control c, exiting tests...\n    if [ \"$f\" != \"\" ] ; then\n        Cleanup\n    fi\n    exit 1\n}\n\nfunction Setup() {\n    if [ \"$APPEND_APACHECONF\" = \"\" ] ; then\n        sudo cp \"$f\" \"$EA\"/sites-available/\n        sudo ln -sf \"$EA/sites-available/$f\" \"$EA/sites-enabled/$f\"\n        echo \"\n<VirtualHost *:80>\n    ServerName example.com\n    DocumentRoot /tmp/\n    ErrorLog /tmp/error.log\n    CustomLog /tmp/requests.log combined\n</VirtualHost>\" | sudo tee $EA/sites-available/throwaway-example.conf >/dev/null\n        sudo ln -sf $EA/sites-available/throwaway-example.conf $EA/sites-enabled/throwaway-example.conf\n    else\n        TMP=\"/tmp/`basename \\\"$APPEND_APACHECONF\\\"`.$$\"\n        sudo cp -a \"$APPEND_APACHECONF\" \"$TMP\"\n        sudo bash -c \"cat \\\"$f\\\" >> \\\"$APPEND_APACHECONF\\\"\"\n    fi\n}\n\nfunction Cleanup() {\n    if [ \"$APPEND_APACHECONF\" = \"\" ] ; then\n        sudo rm /etc/apache2/sites-{enabled,available}/\"$f\"\n        sudo rm $EA/sites-available/throwaway-example.conf\n        sudo rm $EA/sites-enabled/throwaway-example.conf\n    else\n        sudo mv \"$TMP\" \"$APPEND_APACHECONF\"\n    fi\n}\n\n# if our environment asks us to enable modules, do our best!\nif [ \"$1\" = --debian-modules ] ; then\n    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y apache2\n    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libapache2-mod-wsgi-py3\n    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libapache2-mod-macro\n\n    for mod in  ssl rewrite macro wsgi deflate userdir version mime setenvif ; do\n        echo -n enabling $mod\n        sudo a2enmod $mod\n    done\nfi\n\nCERTBOT_CMD=\"sudo $(command -v certbot) --server $SERVER -vvvv\"\nCERTBOT_CMD=\"$CERTBOT_CMD --debug --apache --register-unsafely-without-email\"\nCERTBOT_CMD=\"$CERTBOT_CMD --agree-tos certonly -t --no-verify-ssl\"\n\nFAILS=0\ntrap CleanupExit INT\nfor f in *.conf ; do \n    echo -n  testing \"$f\"...\n    Setup\n    RESULT=`echo c | $CERTBOT_CMD 2>&1`\n    if echo $RESULT | grep -Eq \\(\"Which names would you like\"\\|\"mod_macro is not yet\"\\) ; then\n        echo passed\n    else\n        echo failed\n        echo $RESULT\n        echo\n        echo\n        FAILS=`expr $FAILS + 1`\n    fi\n    Cleanup\ndone\nif [ \"$FAILS\" -ne 0 ] ; then\n    exit 1\nfi\nexit 0\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test-pebble.py",
    "content": "#!/usr/bin/env python\n\"\"\"\nThis executable script wraps the apache-conf-test bash script, in order to setup a pebble instance\nbefore its execution. Directory URL is passed through the SERVER environment variable.\n\"\"\"\nimport os\nimport subprocess\nimport sys\n\nfrom certbot_integration_tests.utils import acme_server\n\nSCRIPT_DIRNAME = os.path.dirname(__file__)\n\n\ndef main() -> int:\n    args = sys.argv[1:]\n    with acme_server.ACMEServer([], False) as acme_xdist:\n        environ = os.environ.copy()\n        environ['SERVER'] = acme_xdist['directory_url']\n        command = [os.path.join(SCRIPT_DIRNAME, 'apache-conf-test')]\n        command.extend(args)\n        return subprocess.call(command, env=environ)\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/failing/missing-double-quote-1724.conf",
    "content": "<VirtualHost *:443>\n        ServerAdmin webmaster@localhost\n        ServerAlias www.example.com\n        ServerName example.com\n        DocumentRoot /var/www/example.com/www/\n\tSSLEngine on\n\t\n\tSSLProtocol all -SSLv2 -SSLv3\n        SSLCipherSuite \"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$\n        SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n        SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n\n        <Directory />\n                Options FollowSymLinks\n                AllowOverride All\n        </Directory>\n        <Directory /var/www/example.com/www>\n                Options Indexes FollowSymLinks MultiViews\n                AllowOverride All\n                Order allow,deny\n                allow from all\n                # This directive allows us to have apache2's default start page\n                # in /apache2-default/, but still have / go to the right place\n        </Directory>\n\n        ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n        <Directory \"/usr/lib/cgi-bin\">\n                AllowOverride None\n                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n                Order allow,deny\n                Allow from all\n        </Directory>\n\n        ErrorLog /var/log/apache2/error.log\n\n        # Possible values include: debug, info, notice, warn, error, crit,\n        # alert, emerg.\n        LogLevel warn\n\n        CustomLog /var/log/apache2/access.log combined\n        ServerSignature On\n\n    Alias /apache_doc/ \"/usr/share/doc/\"\n    <Directory \"/usr/share/doc/\">\n        Options Indexes MultiViews FollowSymLinks\n        AllowOverride None\n        Order deny,allow\n        Deny from all\n        Allow from 127.0.0.0/255.0.0.0 ::1/128\n    </Directory>\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/failing/multivhost-1093.conf",
    "content": "<Directory /var/www/sjau.ch>\n                AllowOverride None\n                                Require all denied\n                </Directory>\n\n<VirtualHost *:80>\n                                        DocumentRoot /var/www/sjau.ch/web\n\n                ServerName sjau.ch\n                ServerAlias www.sjau.ch\n                ServerAdmin webmaster@sjau.ch\n\n                ErrorLog /var/log/ispconfig/httpd/sjau.ch/error.log\n\n                Alias /error/ \"/var/www/sjau.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                </IfModule>\n\n                <Directory /var/www/sjau.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client1/web2/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/sjau.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/sjau.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client1/web2/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web2 client1\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/sjau.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client1/web2/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web2 client1\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client1/web2/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client1/web2/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n<VirtualHost [2a01:4f8:160:13a2::1002]:80>\n                                        DocumentRoot /var/www/sjau.ch/web\n\n                ServerName sjau.ch\n                ServerAlias www.sjau.ch\n                ServerAdmin webmaster@sjau.ch\n\n                ErrorLog /var/log/ispconfig/httpd/sjau.ch/error.log\n\n                Alias /error/ \"/var/www/sjau.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                </IfModule>\n\n                <Directory /var/www/sjau.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client1/web2/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/sjau.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/sjau.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client1/web2/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client1/web2/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web2 client1\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/sjau.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client1/web2/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web2/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web2 client1\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client1/web2/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client1/web2/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/failing/multivhost-1093b.conf",
    "content": "<Directory /var/www/ensemen.ch>\n                AllowOverride None\n                                Require all denied\n                </Directory>\n\n<VirtualHost *:80>\n                                        DocumentRoot /var/www/ensemen.ch/web\n\n                ServerName ensemen.ch\n                ServerAlias www.ensemen.ch\n                ServerAdmin webmaster@ensemen.ch\n\n                ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log\n\n                Alias /error/ \"/var/www/ensemen.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                </IfModule>\n\n                <Directory /var/www/ensemen.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client4/web17/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web17 client4\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web17 client4\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client4/web17/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client4/web17/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n<VirtualHost *:443>\n                                        DocumentRoot /var/www/ensemen.ch/web\n\n                ServerName ensemen.ch\n                ServerAlias www.ensemen.ch\n                ServerAdmin webmaster@ensemen.ch\n\n                ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log\n\n                Alias /error/ \"/var/www/ensemen.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                SSLEngine on\n                SSLProtocol All -SSLv2 -SSLv3\n                SSLCertificateFile /var/www/clients/client4/web17/ssl/ensemen.ch.crt\n                SSLCertificateKeyFile /var/www/clients/client4/web17/ssl/ensemen.ch.key\n                </IfModule>\n\n                <Directory /var/www/ensemen.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client4/web17/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web17 client4\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web17 client4\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client4/web17/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client4/web17/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n<VirtualHost [2a01:4f8:160:13a2::1017]:80>\n                                        DocumentRoot /var/www/ensemen.ch/web\n\n                ServerName ensemen.ch\n                ServerAlias www.ensemen.ch\n                ServerAdmin webmaster@ensemen.ch\n\n                ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log\n\n                Alias /error/ \"/var/www/ensemen.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                </IfModule>\n\n                <Directory /var/www/ensemen.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client4/web17/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web17 client4\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web17 client4\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client4/web17/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client4/web17/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n<VirtualHost [2a01:4f8:160:13a2::1017]:443>\n                                        DocumentRoot /var/www/ensemen.ch/web\n\n                ServerName ensemen.ch\n                ServerAlias www.ensemen.ch\n                ServerAdmin webmaster@ensemen.ch\n\n                ErrorLog /var/log/ispconfig/httpd/ensemen.ch/error.log\n\n                Alias /error/ \"/var/www/ensemen.ch/web/error/\"\n                ErrorDocument 400 /error/400.html\n                ErrorDocument 401 /error/401.html\n                ErrorDocument 403 /error/403.html\n                ErrorDocument 404 /error/404.html\n                ErrorDocument 405 /error/405.html\n                ErrorDocument 500 /error/500.html\n                ErrorDocument 502 /error/502.html\n                ErrorDocument 503 /error/503.html\n\n                <IfModule mod_ssl.c>\n                SSLEngine on\n                SSLProtocol All -SSLv2 -SSLv3\n                SSLCertificateFile /var/www/clients/client4/web17/ssl/ensemen.ch.crt\n                SSLCertificateKeyFile /var/www/clients/client4/web17/ssl/ensemen.ch.key\n                </IfModule>\n\n                <Directory /var/www/ensemen.ch/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                # Clear PHP settings of this website\n                                <FilesMatch \".+\\.ph(p[345]?|t|tml)$\">\n                                                SetHandler None\n                                </FilesMatch>\n                                Options +FollowSymLinks\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n                <IfModule mod_ruby.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                Options +ExecCGI\n                        </Directory>\n                        RubyRequire apache/ruby-run\n                        #RubySafeLevel 0\n                        AddType text/html .rb\n                        AddType text/html .rbx\n                        <Files *.rb>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                        <Files *.rbx>\n                                SetHandler ruby-object\n                                RubyHandler Apache::RubyRun.instance\n                        </Files>\n                </IfModule>\n\n\n                <IfModule mod_python.c>\n                        <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.py$\">\n                                        SetHandler mod_python\n                                </FilesMatch>\n                                PythonHandler mod_python.publisher\n                                PythonDebug On\n                        </Directory>\n                </IfModule>\n\n                # cgi enabled\n        <Directory /var/www/clients/client4/web17/cgi-bin>\n                                                Require all granted\n                                        </Directory>\n                ScriptAlias  /cgi-bin/ /var/www/clients/client4/web17/cgi-bin/\n                <FilesMatch \"\\.(cgi|pl)$\">\n                        SetHandler cgi-script\n                </FilesMatch>\n                # suexec enabled\n                <IfModule mod_suexec.c>\n                        SuexecUserGroup web17 client4\n                </IfModule>\n                # php as fast-cgi enabled\n        # For config options see: http://httpd.apache.org/mod_fcgid/mod/mod_fcgid.html\n                <IfModule mod_fcgid.c>\n                                IdleTimeout 300\n                                ProcessLifeTime 3600\n                                # MaxProcessCount 1000\n                                DefaultMinClassProcessCount 0\n                                DefaultMaxClassProcessCount 100\n                                IPCConnectTimeout 3\n                                IPCCommTimeout 600\n                                BusyTimeout 3600\n                </IfModule>\n                <Directory /var/www/ensemen.ch/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n                <Directory /var/www/clients/client4/web17/web>\n                                <FilesMatch \"\\.php[345]?$\">\n                                        SetHandler fcgid-script\n                                </FilesMatch>\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php3\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php4\n                                FCGIWrapper /var/www/php-fcgi-scripts/web17/.php-fcgi-starter .php5\n                                Options +ExecCGI\n                                AllowOverride All\n                                                                Require all granted\n                                                </Directory>\n\n\n                # add support for apache mpm_itk\n                <IfModule mpm_itk_module>\n                        AssignUserId web17 client4\n                </IfModule>\n\n                <IfModule mod_dav_fs.c>\n                # Do not execute PHP files in webdav directory\n                        <Directory /var/www/clients/client4/web17/webdav>\n                                <ifModule mod_security2.c>\n                                        SecRuleRemoveById 960015\n                                        SecRuleRemoveById 960032\n                                </ifModule>\n                                <FilesMatch \"\\.ph(p3?|tml)$\">\n                                        SetHandler None\n                                </FilesMatch>\n                        </Directory>\n                        DavLockDB /var/www/clients/client4/web17/tmp/DavLock\n                        # DO NOT REMOVE THE COMMENTS!\n                        # IF YOU REMOVE THEM, WEBDAV WILL NOT WORK ANYMORE!\n      # WEBDAV BEGIN\n                        # WEBDAV END\n                </IfModule>\n\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/1626-1531.conf",
    "content": "<VirtualHost *:80>\n        ServerAdmin denver@ossguy.com\n        ServerName  c-beta.ossguy.com\n\n        Alias /robots.txt /home/denver/www/c-beta.ossguy.com/static/robots.txt\n        Alias /favicon.ico /home/denver/www/c-beta.ossguy.com/static/favicon.ico\n\n        AliasMatch /(.*\\.css) /home/denver/www/c-beta.ossguy.com/static/$1\n        AliasMatch /(.*\\.js) /home/denver/www/c-beta.ossguy.com/static/$1\n        AliasMatch /(.*\\.png) /home/denver/www/c-beta.ossguy.com/static/$1\n        AliasMatch /(.*\\.gif) /home/denver/www/c-beta.ossguy.com/static/$1\n        AliasMatch /(.*\\.jpg) /home/denver/www/c-beta.ossguy.com/static/$1\n\n        WSGIScriptAlias / /home/denver/www/c-beta.ossguy.com/django.wsgi\n        WSGIDaemonProcess c-beta-ossguy user=www-data group=www-data home=/var/www processes=5 threads=10 maximum-requests=1000 umask=0007 display-name=c-beta-ossguy\n        WSGIProcessGroup c-beta-ossguy\n        WSGIApplicationGroup %{GLOBAL}\n\n        DocumentRoot /home/denver/www/c-beta.ossguy.com/static\n\n        <Directory /home/denver/www/c-beta.ossguy.com/static>\n                Options -Indexes +FollowSymLinks -MultiViews\n                Require all granted\n                AllowOverride None\n        </Directory>\n\n        <Directory /home/denver/www/c-beta.ossguy.com/static/source>\n                Options +Indexes +FollowSymLinks -MultiViews\n                Require all granted\n                AllowOverride None\n        </Directory>\n\n        # Custom log file locations\n        LogLevel warn\n        ErrorLog  /tmp/error.log\n        CustomLog /tmp/access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/README.modules",
    "content": "# Modules required to parse these conf files:\nssl\nrewrite\nmacro\nwsgi\ndeflate\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/anarcat-1531.conf",
    "content": "<VirtualHost *:80>\n        ServerAdmin root@localhost\n        ServerName anarcat.wiki.orangeseeds.org:80\n\n\n        UserDir disabled\n\n        RewriteEngine On\n        RewriteRule ^/(.*) http\\:\\/\\/anarc\\.at\\/$1 [L,R,NE]\n\n        ErrorLog /var/log/apache2/1531error.log\n        LogLevel warn\n        CustomLog /var/log/apache2/1531access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/comment-continuations-2050.conf",
    "content": "# ---------------------------------------------------------------\n# Core ModSecurity Rule Set ver.2.2.6\n# Copyright (C) 2006-2012 Trustwave All rights reserved.\n#\n# The OWASP ModSecurity Core Rule Set is distributed under \n# Apache Software License (ASL) version 2\n# Please see the enclosed LICENCE file for full details.\n# ---------------------------------------------------------------\n\n\n#\n# -- [[ Recommended Base Configuration ]] ------------------------------------------------- \n#\n# The configuration directives/settings in this file are used to control\n# the OWASP ModSecurity CRS. These settings do **NOT** configure the main\n# ModSecurity settings such as:\n# \n# - SecRuleEngine\n# - SecRequestBodyAccess\n# - SecAuditEngine\n# - SecDebugLog\n#\n# You should use the modsecurity.conf-recommended file that comes with the\n# ModSecurity source code archive.\n#\n# Ref: http://mod-security.svn.sourceforge.net/viewvc/mod-security/m2/trunk/modsecurity.conf-recommended\n#\n\n\n#\n# -- [[ Rule Version ]] -------------------------------------------------------------------\n#\n# Rule version data is added to the \"Producer\" line of Section H of the Audit log:\n#\n# - Producer: ModSecurity for Apache/2.7.0-rc1 (http://www.modsecurity.org/); OWASP_CRS/2.2.4.\n#\n# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecComponentSignature\n#\n#SecComponentSignature \"OWASP_CRS/2.2.6\"\n\n\n#\n# -- [[ Modes of Operation: Self-Contained vs. Collaborative Detection ]] -----------------\n#\n# Each detection rule uses the \"block\" action which will inherit the SecDefaultAction\n# specified below.  Your settings here will determine which mode of operation you use.\n#\n# -- [[ Self-Contained Mode ]] --\n# Rules inherit the \"deny\" disruptive action.  The first rule that matches will block.\n#\n# -- [[ Collaborative Detection Mode ]] --\n# This is a \"delayed blocking\" mode of operation where each matching rule will inherit\n# the \"pass\" action and will only contribute to anomaly scores.  Transactional blocking\n# can be applied \n#\n# -- [[ Alert Logging Control ]] --\n# You have three options -\n#\n# - To log to both the Apache error_log and ModSecurity audit_log file use: \"log\"\n# - To log *only* to the ModSecurity audit_log file use: \"nolog,auditlog\"\n# - To log *only* to the Apache error_log file use: \"log,noauditlog\"\n#\n# Ref: http://blog.spiderlabs.com/2010/11/advanced-topic-of-the-week-traditional-vs-anomaly-scoring-detection-modes.html\n# Ref: https://sourceforge.net/apps/mediawiki/mod-security/index.php?title=Reference_Manual#SecDefaultAction\n#\n#SecDefaultAction \"phase:1,deny,log\"\n\n\n#\n# -- [[ Collaborative Detection Severity Levels ]] ----------------------------------------\n#\n# These are the default scoring points for each severity level.  You may\n# adjust these to you liking.  These settings will be used in macro expansion\n# in the rules to increment the anomaly scores when rules match.\n#\n# These are the default Severity ratings (with anomaly scores) of the individual rules -\n#\n#    - 2: Critical - Anomaly Score of 5.\n#         Is the highest severity level possible without correlation.  It is\n#         normally generated by the web attack rules (40 level files).\n#    - 3: Error - Anomaly Score of 4.\n#         Is generated mostly from outbound leakage rules (50 level files).\n#    - 4: Warning - Anomaly Score of 3.\n#         Is generated by malicious client rules (35 level files).\n#    - 5: Notice - Anomaly Score of 2.\n#         Is generated by the Protocol policy and anomaly files.\n#\n#SecAction \\\n  \"id:'900001', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.critical_anomaly_score=5, \\\n  setvar:tx.error_anomaly_score=4, \\\n  setvar:tx.warning_anomaly_score=3, \\\n  setvar:tx.notice_anomaly_score=2, \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ Collaborative Detection Scoring Threshold Levels ]] ------------------------------\n#\n# These variables are used in macro expansion in the 49 inbound blocking and 59\n# outbound blocking files.\n#\n# **MUST HAVE** ModSecurity v2.5.12 or higher to use macro expansion in numeric\n# operators.  If you have an earlier version, edit the 49/59 files directly to\n# set the appropriate anomaly score levels.\n#\n# You should set the score to the proper threshold you would prefer. If set to \"5\"\n# it will work similarly to previous Mod CRS rules and will create an event in the error_log\n# file if there are any rules that match.  If you would like to lessen the number of events\n# generated in the error_log file, you should increase the anomaly score threshold to\n# something like \"20\".  This would only generate an event in the error_log file if\n# there are multiple lower severity rule matches or if any 1 higher severity item matches.\n#\n#SecAction \\\n  \"id:'900002', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.inbound_anomaly_score_level=5, \\\n  nolog, \\\n  pass\"\n\n\n#SecAction \\\n  \"id:'900003', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.outbound_anomaly_score_level=4, \\\n  nolog, \\\n  pass\"\n\n\n# \n# -- [[ Collaborative Detection Blocking ]] -----------------------------------------------\n#\n# This is a collaborative detection mode where each rule will increment an overall\n# anomaly score for the transaction. The scores are then evaluated in the following files:\n#\n# Inbound anomaly score - checked in the modsecurity_crs_49_inbound_blocking.conf file\n# Outbound anomaly score - checked in the modsecurity_crs_59_outbound_blocking.conf file\n#\n# If you want to use anomaly scoring mode, then uncomment this line.\n#\n#SecAction \\\n  \"id:'900004', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.anomaly_score_blocking=on, \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ GeoIP Database ]] -----------------------------------------------------------------\n#\n# There are some rulesets that need to inspect the GEO data of the REMOTE_ADDR data.\n# \n# You must first download the MaxMind GeoIP Lite City DB -\n#\n#       http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz\n#\n# You then need to define the proper path for the SecGeoLookupDb directive\n#\n# Ref: http://blog.spiderlabs.com/2010/10/detecting-malice-with-modsecurity-geolocation-data.html\n# Ref: http://blog.spiderlabs.com/2010/11/detecting-malice-with-modsecurity-ip-forensics.html\n#\n#SecGeoLookupDb /opt/modsecurity/lib/GeoLiteCity.dat\n\n#\n# -- [[ Regression Testing Mode ]] --------------------------------------------------------\n#\n# If you are going to run the regression testing mode, you should uncomment the\n# following rule. It will enable DetectionOnly mode for the SecRuleEngine and\n# will enable Response Header tagging so that the client testing script can see\n# which rule IDs have matched.\n#\n# You must specify the your source IP address where you will be running the tests\n# from.\n#\n#SecRule REMOTE_ADDR \"@ipMatch 192.168.1.100\" \\\n  \"id:'900005', \\\n  phase:1, \\\n  t:none, \\\n  ctl:ruleEngine=DetectionOnly, \\\n  setvar:tx.regression_testing=1, \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ HTTP Policy Settings ]] ----------------------------------------------------------\n#\n# Set the following policy settings here and they will be propagated to the 23 rules\n# file (modsecurity_common_23_request_limits.conf) by using macro expansion.  \n# If you run into false positives, you can adjust the settings here.\n#\n# Only the max number of args is uncommented by default as there are a high rate\n# of false positives.  Uncomment the items you wish to set.\n# \n#\n# -- Maximum number of arguments in request limited\n#SecAction \\\n  \"id:'900006', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.max_num_args=255, \\\n  nolog, \\\n  pass\"\n\n#\n# -- Limit argument name length\n#SecAction \\\n  \"id:'900007', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.arg_name_length=100, \\\n  nolog, \\\n  pass\"\n\n#\n# -- Limit value name length\n#SecAction \\\n  \"id:'900008', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.arg_length=400, \\\n  nolog, \\\n  pass\"\n\n#\n# -- Limit arguments total length\n#SecAction \\\n  \"id:'900009', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.total_arg_length=64000, \\\n  nolog, \\\n  pass\"\n\n#\n# -- Individual file size is limited\n#SecAction \\\n  \"id:'900010', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.max_file_size=1048576, \\\n  nolog, \\\n  pass\"\n\n#\n# -- Combined file size is limited\n#SecAction \\\n  \"id:'900011', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.combined_file_sizes=1048576, \\\n  nolog, \\\n  pass\"\n\n\n#\n# Set the following policy settings here and they will be propagated to the 30 rules\n# file (modsecurity_crs_30_http_policy.conf) by using macro expansion.  \n# If you run into false positives, you can adjust the settings here.\n#\n#SecAction \\\n  \"id:'900012', \\\n  phase:1, \\\n  t:none, \\\n  setvar:'tx.allowed_methods=GET HEAD POST OPTIONS', \\\n  setvar:'tx.allowed_request_content_type=application/x-www-form-urlencoded|multipart/form-data|text/xml|application/xml|application/x-amf', \\\n  setvar:'tx.allowed_http_versions=HTTP/0.9 HTTP/1.0 HTTP/1.1', \\\n  setvar:'tx.restricted_extensions=.asa/ .asax/ .ascx/ .axd/ .backup/ .bak/ .bat/ .cdx/ .cer/ .cfg/ .cmd/ .com/ .config/ .conf/ .cs/ .csproj/ .csr/ .dat/ .db/ .dbf/ .dll/ .dos/ .htr/ .htw/ .ida/ .idc/ .idq/ .inc/ .ini/ .key/ .licx/ .lnk/ .log/ .mdb/ .old/ .pass/ .pdb/ .pol/ .printer/ .pwd/ .resources/ .resx/ .sql/ .sys/ .vb/ .vbs/ .vbproj/ .vsdisco/ .webinfo/ .xsd/ .xsx/', \\\n  setvar:'tx.restricted_headers=/Proxy-Connection/ /Lock-Token/ /Content-Range/ /Translate/ /via/ /if/', \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ Content Security Policy (CSP) Settings ]] -----------------------------------------\n#\n# The purpose of these settings is to send CSP response headers to\n# Mozilla FireFox users so that you can enforce how dynamic content\n# is used. CSP usage helps to prevent XSS attacks against your users.\n#  \n# Reference Link:\n#\n#\thttps://developer.mozilla.org/en/Security/CSP\n#\n# Uncomment this SecAction line if you want use CSP enforcement.\n# You need to set the appropriate directives and settings for your site/domain and\n# and activate the CSP file in the experimental_rules directory.\n# \n# Ref: http://blog.spiderlabs.com/2011/04/modsecurity-advanced-topic-of-the-week-integrating-content-security-policy-csp.html\n#\n#SecAction \\\n  \"id:'900013', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.csp_report_only=1, \\\n  setvar:tx.csp_report_uri=/csp_violation_report, \\\n  setenv:'csp_policy=allow \\'self\\'; img-src *.yoursite.com; media-src *.yoursite.com; style-src *.yoursite.com; frame-ancestors *.yoursite.com; script-src *.yoursite.com; report-uri %{tx.csp_report_uri}', \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ Brute Force Protection ]] ---------------------------------------------------------\n#\n# If you are using the Brute Force Protection rule set, then uncomment the following\n# lines and set the following variables:\n# - Protected URLs: resources to protect (e.g. login pages) - set to your login page\n# - Burst Time Slice Interval: time interval window to monitor for bursts\n# - Request Threshold: request # threshold to trigger a burst\n# - Block Period: temporary block timeout\n#\n#SecAction \\\n  \"id:'900014', \\\n  phase:1, \\\n  t:none, \\\n  setvar:'tx.brute_force_protected_urls=/login.jsp /partner_login.php', \\\n  setvar:'tx.brute_force_burst_time_slice=60', \\\n  setvar:'tx.brute_force_counter_threshold=10', \\\n  setvar:'tx.brute_force_block_timeout=300', \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ DoS Protection ]] ----------------------------------------------------------------\n#\n# If you are using the DoS Protection rule set, then uncomment the following\n# lines and set the following variables:\n# - Burst Time Slice Interval: time interval window to monitor for bursts\n# - Request Threshold: request # threshold to trigger a burst\n# - Block Period: temporary block timeout\n#\n#SecAction \\\n  \"id:'900015', \\\n  phase:1, \\\n  t:none, \\\n  setvar:'tx.dos_burst_time_slice=60', \\\n  setvar:'tx.dos_counter_threshold=100', \\\n  setvar:'tx.dos_block_timeout=600', \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ Check UTF encoding ]] -----------------------------------------------------------\n#\n# We only want to apply this check if UTF-8 encoding is actually used by the site, otherwise\n# it will result in false positives.\n#\n# Uncomment this line if your site uses UTF8 encoding\n#SecAction \\\n  \"id:'900016', \\\n  phase:1, \\\n  t:none, \\\n  setvar:tx.crs_validate_utf8_encoding=1, \\\n  nolog, \\\n  pass\"\n\n\n#\n# -- [[ Enable XML Body Parsing ]] -------------------------------------------------------\n#\n# The rules in this file will trigger the XML parser upon an XML request\n#\n# Initiate XML Processor in case of xml content-type\n#\n#SecRule REQUEST_HEADERS:Content-Type \"text/xml\" \\\n  \"id:'900017', \\\n  phase:1, \\\n  t:none,t:lowercase, \\\n  nolog, \\\n  pass, \\\n  chain\"\n\t#SecRule REQBODY_PROCESSOR \"!@streq XML\" \\\n\t  \"ctl:requestBodyProcessor=XML\"\n\n\n#\n# -- [[ Global and IP Collections ]] -----------------------------------------------------\n#\n# Create both Global and IP collections for rules to use\n# There are some CRS rules that assume that these two collections\n# have already been initiated.\n#\n#SecRule REQUEST_HEADERS:User-Agent \"^(.*)$\" \\\n  \"id:'900018', \\\n  phase:1, \\\n  t:none,t:sha1,t:hexEncode, \\\n  setvar:tx.ua_hash=%{matched_var}, \\\n  nolog, \\\n  pass\"\n\n\n#SecRule REQUEST_HEADERS:x-forwarded-for \"^\\b(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})\\b\" \\\n  \"id:'900019', \\\n  phase:1, \\\n  t:none, \\\n  capture, \\\n  setvar:tx.real_ip=%{tx.1}, \\\n  nolog, \\\n  pass\"\n\n\n#SecRule &TX:REAL_IP \"!@eq 0\" \\\n  \"id:'900020', \\\n  phase:1, \\\n  t:none, \\\n  initcol:global=global, \\\n  initcol:ip=%{tx.real_ip}_%{tx.ua_hash}, \\\n  nolog, \\\n  pass\"\n\n\n#SecRule &TX:REAL_IP \"@eq 0\" \\\n  \"id:'900021', \\\n  phase:1, \\\n  t:none, \\\n  initcol:global=global, \\\n  initcol:ip=%{remote_addr}_%{tx.ua_hash}, \\\n  nolog, \\\n  pass\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/drupal-errordocument-arg-1724.conf",
    "content": "#\n# Apache/PHP/Drupal settings:\n#\n\n# Protect files and directories from prying eyes.\n<FilesMatch \"\\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\\.php)?|xtmpl|svn-base)$|^(code-style\\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format)$\">\n  Order allow,deny\n</FilesMatch>\n\n# Don't show directory listings for URLs which map to a directory.\nOptions -Indexes\n\n# Follow symbolic links in this directory.\nOptions +FollowSymLinks\n\n# Make Drupal handle any 404 errors.\nErrorDocument 404 /index.php\n\n# Force simple error message for requests for non-existent favicon.ico.\n<Files favicon.ico>\n  # There is no end quote below, for compatibility with Apache 1.3.\n  ErrorDocument 404 \"The requested file favicon.ico was not found.\n</Files>\n\n# Set the default handler.\nDirectoryIndex index.php\n\n# Override PHP settings. More in sites/default/settings.php\n# but the following cannot be changed at runtime.\n\n# PHP 4, Apache 1.\n<IfModule mod_php4.c>\n  php_value magic_quotes_gpc                0\n  php_value register_globals                0\n  php_value session.auto_start              0\n  php_value mbstring.http_input             pass\n  php_value mbstring.http_output            pass\n  php_value mbstring.encoding_translation   0\n</IfModule>\n\n# PHP 4, Apache 2.\n<IfModule sapi_apache2.c>\n  php_value magic_quotes_gpc                0\n  php_value register_globals                0\n  php_value session.auto_start              0\n  php_value mbstring.http_input             pass\n  php_value mbstring.http_output            pass\n  php_value mbstring.encoding_translation   0\n</IfModule>\n\n# PHP 5, Apache 1 and 2.\n<IfModule mod_php5.c>\n  php_value magic_quotes_gpc                0\n  php_value register_globals                0\n  php_value session.auto_start              0\n  php_value mbstring.http_input             pass\n  php_value mbstring.http_output            pass\n  php_value mbstring.encoding_translation   0\n</IfModule>\n\n# Requires mod_expires to be enabled.\n<IfModule mod_expires.c>\n  # Enable expirations.\n  ExpiresActive On\n\n  # Cache all files for 2 weeks after access (A).\n  ExpiresDefault A1209600\n\n  <FilesMatch \\.php$>\n    # Do not allow PHP scripts to be cached unless they explicitly send cache\n    # headers themselves. Otherwise all scripts would have to overwrite the\n    # headers set by mod_expires if they want another caching behavior. This may\n    # fail if an error occurs early in the bootstrap process, and it may cause\n    # problems if a non-Drupal PHP file is installed in a subdirectory.\n    ExpiresActive Off\n  </FilesMatch>\n</IfModule>\n\n# Various rewrite rules.\n<IfModule mod_rewrite.c>\n  RewriteEngine on\n\n  # If your site can be accessed both with and without the 'www.' prefix, you\n  # can use one of the following settings to redirect users to your preferred\n  # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:\n  #\n  # To redirect all users to access the site WITH the 'www.' prefix,\n  # (http://example.com/... will be redirected to http://www.example.com/...)\n  # adapt and uncomment the following:\n  # RewriteCond %{HTTP_HOST} ^example\\.com$ [NC]\n  # RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]\n  #\n  # To redirect all users to access the site WITHOUT the 'www.' prefix,\n  # (http://www.example.com/... will be redirected to http://example.com/...)\n  # uncomment and adapt the following:\n  # RewriteCond %{HTTP_HOST} ^www\\.example\\.com$ [NC]\n  # RewriteRule ^(.*)$ http://example.com/$1 [L,R=301]\n\n  # Modify the RewriteBase if you are using Drupal in a subdirectory or in a\n  # VirtualDocumentRoot and the rewrite rules are not working properly.\n  # For example if your site is at http://example.com/drupal uncomment and\n  # modify the following line:\n  # RewriteBase /drupal\n  #\n  # If your site is running in a VirtualDocumentRoot at http://example.com/,\n  # uncomment the following line:\n  # RewriteBase /\n\n  # Rewrite URLs of the form 'x' to the form 'index.php?q=x'.\n  RewriteCond %{REQUEST_FILENAME} !-f\n  RewriteCond %{REQUEST_FILENAME} !-d\n  RewriteCond %{REQUEST_URI} !=/favicon.ico\n  RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]\n</IfModule>\n\n# $Id$\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/drupal-htaccess-1531.conf",
    "content": "#\n# Apache/PHP/Drupal settings:\n#\n\n# Protect files and directories from prying eyes.\n<FilesMatch \"\\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\\.php)?|xtmpl)(~|\\.sw[op]|\\.bak|\\.orig|\\.save)?$|^(\\..*|Entries.*|Repository|Root|Tag|Template)$|^#.*#$|\\.php(~|\\.sw[op]|\\.bak|\\.orig\\.save)$\">\n  Order allow,deny\n</FilesMatch>\n\n# Don't show directory listings for URLs which map to a directory.\nOptions -Indexes\n\n# Follow symbolic links in this directory.\nOptions +FollowSymLinks\n\n# Make Drupal handle any 404 errors.\nErrorDocument 404 /index.php\n\n# Set the default handler.\nDirectoryIndex index.php index.html index.htm\n\n# Override PHP settings that cannot be changed at runtime. See\n# sites/default/default.settings.php and drupal_environment_initialize() in\n# includes/bootstrap.inc for settings that can be changed at runtime.\n\n# PHP 5, Apache 1 and 2.\n<IfModule mod_php5.c>\n  php_flag magic_quotes_gpc                 off\n  php_flag magic_quotes_sybase              off\n  php_flag register_globals                 off\n  php_flag session.auto_start               off\n  php_value mbstring.http_input             pass\n  php_value mbstring.http_output            pass\n  php_flag mbstring.encoding_translation    off\n</IfModule>\n\n# Requires mod_expires to be enabled.\n<IfModule mod_expires.c>\n  # Enable expirations.\n  ExpiresActive On\n\n  # Cache all files for 2 weeks after access (A).\n  ExpiresDefault A1209600\n\n  <FilesMatch \\.php$>\n    # Do not allow PHP scripts to be cached unless they explicitly send cache\n    # headers themselves. Otherwise all scripts would have to overwrite the\n    # headers set by mod_expires if they want another caching behavior. This may\n    # fail if an error occurs early in the bootstrap process, and it may cause\n    # problems if a non-Drupal PHP file is installed in a subdirectory.\n    ExpiresActive Off\n  </FilesMatch>\n</IfModule>\n\n# Various rewrite rules.\n<IfModule mod_rewrite.c>\n  RewriteEngine on\n\n  # Set \"protossl\" to \"s\" if we were accessed via https://.  This is used later\n  # if you enable \"www.\" stripping or enforcement, in order to ensure that\n  # you don't bounce between http and https.\n  RewriteRule ^ - [E=protossl]\n  RewriteCond %{HTTPS} on\n  RewriteRule ^ - [E=protossl:s]\n\n  # Make sure Authorization HTTP header is available to PHP\n  # even when running as CGI or FastCGI.\n  RewriteRule ^ - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]\n\n  # Block access to \"hidden\" directories whose names begin with a period. This\n  # includes directories used by version control systems such as Subversion or\n  # Git to store control files. Files whose names begin with a period, as well\n  # as the control files used by CVS, are protected by the FilesMatch directive\n  # above.\n  #\n  # NOTE: This only works when mod_rewrite is loaded. Without mod_rewrite, it is\n  # not possible to block access to entire directories from .htaccess, because\n  # <DirectoryMatch> is not allowed here.\n  #\n  # If you do not have mod_rewrite installed, you should remove these\n  # directories from your webroot or otherwise protect them from being\n  # downloaded.\n  RewriteRule \"(^|/)\\.\" - [F]\n\n  # If your site can be accessed both with and without the 'www.' prefix, you\n  # can use one of the following settings to redirect users to your preferred\n  # URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:\n  #\n  # To redirect all users to access the site WITH the 'www.' prefix,\n  # (http://example.com/... will be redirected to http://www.example.com/...)\n  # uncomment the following:\n  # RewriteCond %{HTTP_HOST} .\n  # RewriteCond %{HTTP_HOST} !^www\\. [NC]\n  # RewriteRule ^ http%{ENV:protossl}://www.%{HTTP_HOST}%{REQUEST_URI} [L,R=301]\n  #\n  # To redirect all users to access the site WITHOUT the 'www.' prefix,\n  # (http://www.example.com/... will be redirected to http://example.com/...)\n  # uncomment the following:\n  # RewriteCond %{HTTP_HOST} ^www\\.(.+)$ [NC]\n  # RewriteRule ^ http%{ENV:protossl}://%1%{REQUEST_URI} [L,R=301]\n\n  # Modify the RewriteBase if you are using Drupal in a subdirectory or in a\n  # VirtualDocumentRoot and the rewrite rules are not working properly.\n  # For example if your site is at http://example.com/drupal uncomment and\n  # modify the following line:\n  # RewriteBase /drupal\n  #\n  # If your site is running in a VirtualDocumentRoot at http://example.com/,\n  # uncomment the following line:\n  # RewriteBase /\n\n  # Pass all requests not referring directly to files in the filesystem to\n  # index.php. Clean URLs are handled in drupal_environment_initialize().\n  RewriteCond %{REQUEST_FILENAME} !-f\n  RewriteCond %{REQUEST_FILENAME} !-d\n  RewriteCond %{REQUEST_URI} !=/favicon.ico\n  RewriteRule ^ index.php [L]\n\n  # Rules to correctly serve gzip compressed CSS and JS files.\n  # Requires both mod_rewrite and mod_headers to be enabled.\n  <IfModule mod_headers.c>\n    # Serve gzip compressed CSS files if they exist and the client accepts gzip.\n    RewriteCond %{HTTP:Accept-encoding} gzip\n    RewriteCond %{REQUEST_FILENAME}\\.gz -s\n    RewriteRule ^(.*)\\.css $1\\.css\\.gz [QSA]\n\n    # Serve gzip compressed JS files if they exist and the client accepts gzip.\n    RewriteCond %{HTTP:Accept-encoding} gzip\n    RewriteCond %{REQUEST_FILENAME}\\.gz -s\n    RewriteRule ^(.*)\\.js $1\\.js\\.gz [QSA]\n\n    # Serve correct content types, and prevent mod_deflate double gzip.\n    RewriteRule .css.gz$ - [T=text/css,E=no-gzip:1]\n    RewriteRule .js.gz$ - [T=text/javascript,E=no-gzip:1]\n\n    <FilesMatch \"(\\.js\\.gz|\\.css\\.gz)$\">\n      # Serve correct encoding type.\n      Header set Content-Encoding gzip\n      # Force proxies to cache gzipped & non-gzipped css/js files separately.\n      Header append Vary Accept-Encoding\n    </FilesMatch>\n  </IfModule>\n</IfModule>\n\n# Add headers to all responses.\n<IfModule mod_headers.c>\n  # Disable content sniffing, since it's an attack vector.\n  Header always set X-Content-Type-Options nosniff\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/escaped-space-arguments-2735.conf",
    "content": "RewriteCond %{HTTP:Content-Disposition} \\.php [NC]\nRewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\\ /.+/trackback/?\\ HTTP/ [NC]\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/example-1755.conf",
    "content": "<VirtualHost *:80>\n\t# The ServerName directive sets the request scheme, hostname and port that\n\t# the server uses to identify itself. This is used when creating\n\t# redirection URLs. In the context of virtual hosts, the ServerName\n\t# specifies what hostname must appear in the request's Host: header to\n\t# match this virtual host. For the default virtual host (this file) this\n\t# value is not decisive as it is used as a last resort host regardless.\n\t# However, you must set it for any further virtual host explicitly.\n\tServerName www.example.com\n    ServerAlias example.com\nSetOutputFilter DEFLATE\n# Do not attempt to compress the following extensions\nSetEnvIfNoCase Request_URI \\\n\\.(?:gif|jpe?g|png|swf|flv|zip|gz|tar|mp3|mp4|m4v)$ no-gzip dont-vary\n\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/proof\n\n\t# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n\t# error, crit, alert, emerg.\n\t# It is also possible to configure the loglevel for particular\n\t# modules, e.g.\n\t#LogLevel info ssl:warn\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t# For most configuration files from conf-available/, which are\n\t# enabled or disabled at a global level, it is possible to\n\t# include a line for only one particular virtual host. For example the\n\t# following line enables the CGI configuration for this host only\n\t# after it has been globally disabled with \"a2disconf\".\n\t#Include conf-available/serve-cgi-bin.conf\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/example-ssl.conf",
    "content": "<VirtualHost *:443>\n    ServerName example.com\n    ServerAlias www.example.com\n    ServerAdmin webmaster@localhost\n\n    DocumentRoot /var/www/html\n\n    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n    # error, crit, alert, emerg.\n    # It is also possible to configure the loglevel for particular\n    # modules, e.g.\n    #LogLevel info ssl:warn\n\n    ErrorLog ${APACHE_LOG_DIR}/error.log\n    CustomLog ${APACHE_LOG_DIR}/access.log combined\n\n    # For most configuration files from conf-available/, which are\n    # enabled or disabled at a global level, it is possible to\n    # include a line for only one particular virtual host. For example the\n    # following line enables the CGI configuration for this host only\n    # after it has been globally disabled with \"a2disconf\".\n    #Include conf-available/serve-cgi-bin.conf\n\n    #   SSL Engine Switch:\n    #   Enable/Disable SSL for this virtual host.\n    SSLEngine on\n\n    #   A self-signed (snakeoil) certificate can be created by installing\n    #   the ssl-cert package. See\n    #   /usr/share/doc/apache2/README.Debian.gz for more info.\n    #   If both key and certificate are stored in the same file, only the\n    #   SSLCertificateFile directive is needed.\n    SSLCertificateFile\t/etc/ssl/certs/ssl-cert-snakeoil.pem\n    SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n\n    #   Server Certificate Chain:\n    #   Point SSLCertificateChainFile at a file containing the\n    #   concatenation of PEM encoded CA certificates which form the\n    #   certificate chain for the server certificate. Alternatively\n    #   the referenced file can be the same as SSLCertificateFile\n    #   when the CA certificates are directly appended to the server\n    #   certificate for convenience.\n    #SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt\n\n    #   Certificate Authority (CA):\n    #   Set the CA certificate verification path where to find CA\n    #   certificates for client authentication or alternatively one\n    #   huge file containing all of them (file must be PEM encoded)\n    #   Note: Inside SSLCACertificatePath you need hash symlinks\n    #\t\t to point to the certificate files. Use the provided\n    #\t\t Makefile to update the hash symlinks after changes.\n    #SSLCACertificatePath /etc/ssl/certs/\n    #SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt\n\n    #   Certificate Revocation Lists (CRL):\n    #   Set the CA revocation path where to find CA CRLs for client\n    #   authentication or alternatively one huge file containing all\n    #   of them (file must be PEM encoded)\n    #   Note: Inside SSLCARevocationPath you need hash symlinks\n    #\t\t to point to the certificate files. Use the provided\n    #\t\t Makefile to update the hash symlinks after changes.\n    #SSLCARevocationPath /etc/apache2/ssl.crl/\n    #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl\n\n    #   Client Authentication (Type):\n    #   Client certificate verification type and depth.  Types are\n    #   none, optional, require and optional_no_ca.  Depth is a\n    #   number which specifies how deeply to verify the certificate\n    #   issuer chain before deciding the certificate is not valid.\n    #SSLVerifyClient require\n    #SSLVerifyDepth  10\n\n    #   SSL Engine Options:\n    #   Set various options for the SSL engine.\n    #   o FakeBasicAuth:\n    #\t Translate the client X.509 into a Basic Authorisation.  This means that\n    #\t the standard Auth/DBMAuth methods can be used for access control.  The\n    #\t user name is the `one line' version of the client's X.509 certificate.\n    #\t Note that no password is obtained from the user. Every entry in the user\n    #\t file needs this password: `xxj31ZMTZzkVA'.\n    #   o ExportCertData:\n    #\t This exports two additional environment variables: SSL_CLIENT_CERT and\n    #\t SSL_SERVER_CERT. These contain the PEM-encoded certificates of the\n    #\t server (always existing) and the client (only existing when client\n    #\t authentication is used). This can be used to import the certificates\n    #\t into CGI scripts.\n    #   o StdEnvVars:\n    #\t This exports the standard SSL/TLS related `SSL_*' environment variables.\n    #\t Per default this exportation is switched off for performance reasons,\n    #\t because the extraction step is an expensive operation and is usually\n    #\t useless for serving static content. So one usually enables the\n    #\t exportation for CGI and SSI requests only.\n    #   o OptRenegotiate:\n    #\t This enables optimized SSL connection renegotiation handling when SSL\n    #\t directives are used in per-directory context.\n    #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire\n    <FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n            SSLOptions +StdEnvVars\n    </FilesMatch>\n    <Directory /usr/lib/cgi-bin>\n            SSLOptions +StdEnvVars\n    </Directory>\n\n    #   SSL Protocol Adjustments:\n    #   The safe and default but still SSL/TLS standard compliant shutdown\n    #   approach is that mod_ssl sends the close notify alert but doesn't wait for\n    #   the close notify alert from client. When you need a different shutdown\n    #   approach you can use one of the following variables:\n    #   o ssl-unclean-shutdown:\n    #\t This forces an unclean shutdown when the connection is closed, i.e. no\n    #\t SSL close notify alert is send or allowed to received.  This violates\n    #\t the SSL/TLS standard but is needed for some brain-dead browsers. Use\n    #\t this when you receive I/O errors because of the standard approach where\n    #\t mod_ssl sends the close notify alert.\n    #   o ssl-accurate-shutdown:\n    #\t This forces an accurate shutdown when the connection is closed, i.e. a\n    #\t SSL close notify alert is send and mod_ssl waits for the close notify\n    #\t alert of the client. This is 100% SSL/TLS standard compliant, but in\n    #\t practice often causes hanging connections with brain-dead browsers. Use\n    #\t this only for browsers where you know that their SSL implementation\n    #\t works correctly.\n    #   Notice: Most problems of broken clients are also related to the HTTP\n    #   keep-alive facility, so you usually additionally want to disable\n    #   keep-alive for those clients, too. Use variable \"nokeepalive\" for this.\n    #   Similarly, one has to force some clients to use HTTP/1.0 to workaround\n    #   their broken HTTP/1.1 implementation. Use variables \"downgrade-1.0\" and\n    #   \"force-response-1.0\" for this.\n    BrowserMatch \"MSIE [2-6]\" \\\n            nokeepalive ssl-unclean-shutdown \\\n            downgrade-1.0 force-response-1.0\n    # MSIE 7 and newer should be able to use keepalive\n    BrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/example.conf",
    "content": "<VirtualHost *:80>\n\t# The ServerName directive sets the request scheme, hostname and port that\n\t# the server uses to identify itself. This is used when creating\n\t# redirection URLs. In the context of virtual hosts, the ServerName\n\t# specifies what hostname must appear in the request's Host: header to\n\t# match this virtual host. For the default virtual host (this file) this\n\t# value is not decisive as it is used as a last resort host regardless.\n\t# However, you must set it for any further virtual host explicitly.\n\tServerName www.example.com\n    ServerAlias example.com\n\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\t# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n\t# error, crit, alert, emerg.\n\t# It is also possible to configure the loglevel for particular\n\t# modules, e.g.\n\t#LogLevel info ssl:warn\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t# For most configuration files from conf-available/, which are\n\t# enabled or disabled at a global level, it is possible to\n\t# include a line for only one particular virtual host. For example the\n\t# following line enables the CGI configuration for this host only\n\t# after it has been globally disabled with \"a2disconf\".\n\t#Include conf-available/serve-cgi-bin.conf\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/finalize-1243.apache2.conf.txt",
    "content": "# This is the main Apache server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See http://httpd.apache.org/docs/2.4/ for detailed information about\n# the directives and /usr/share/doc/apache2/README.Debian about Debian specific\n# hints.\n#\n#\n# Summary of how the Apache 2 configuration works in Debian:\n# The Apache 2 web server configuration in Debian is quite different to\n# upstream's suggested way to configure the web server. This is because Debian's\n# default Apache2 installation attempts to make adding and removing modules,\n# virtual hosts, and extra configuration directives as flexible as possible, in\n# order to make automating the changes and administering the server as easy as\n# possible.\n\n# It is split into several files forming the configuration hierarchy outlined\n# below, all located in the /etc/apache2/ directory:\n#\n#\t/etc/apache2/\n#\t|-- apache2.conf\n#\t|\t`--  ports.conf\n#\t|-- mods-enabled\n#\t|\t|-- *.load\n#\t|\t`-- *.conf\n#\t|-- conf-enabled\n#\t|\t`-- *.conf\n# \t`-- sites-enabled\n#\t \t`-- *.conf\n#\n#\n# * apache2.conf is the main configuration file (this file). It puts the pieces\n#   together by including all remaining configuration files when starting up the\n#   web server.\n#\n# * ports.conf is always included from the main configuration file. It is\n#   supposed to determine listening ports for incoming connections which can be\n#   customized anytime.\n#\n# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/\n#   directories contain particular configuration snippets which manage modules,\n#   global configuration fragments, or virtual host configurations,\n#   respectively.\n#\n#   They are activated by symlinking available configuration files from their\n#   respective *-available/ counterparts. These should be managed by using our\n#   helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See\n#   their respective man pages for detailed information.\n#\n# * The binary is called apache2. Due to the use of environment variables, in\n#   the default configuration, apache2 needs to be started/stopped with\n#   /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not\n#   work with the default configuration.\n\n\n# Global configuration\n#\n\n#\n# ServerRoot: The top of the directory tree under which the server's\n# configuration, error, and log files are kept.\n#\n# NOTE!  If you intend to place this on an NFS (or otherwise network)\n# mounted filesystem then please read the Mutex documentation (available\n# at <URL:http://httpd.apache.org/docs/2.4/mod/core.html#mutex>);\n# you will save yourself a lot of trouble.\n#\n# Do NOT add a slash at the end of the directory path.\n#\n#ServerRoot \"/etc/apache2\"\n\n#\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n#\nMutex file:${APACHE_LOCK_DIR} default\n\n#\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n# This needs to be set in /etc/apache2/envvars\n#\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n#\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\n#\nMaxKeepAliveRequests 100\n\n#\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\n#\nKeepAliveTimeout 5\n\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\n#\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\n#\nHostnameLookups Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n#\n# LogLevel: Control the severity of messages logged to the error_log.\n# Available values: trace8, ..., trace1, debug, info, notice, warn,\n# error, crit, alert, emerg.\n# It is also possible to configure the log level for particular modules, e.g.\n# \"LogLevel info ssl:warn\"\n#\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n# Include list of ports to listen on\nInclude ports.conf\n\n\n# Sets the default security model of the Apache2 HTTPD server. It does\n# not allow access to the root filesystem outside of /usr/share and /var/www.\n# The former is used by web applications packaged in Debian,\n# the latter may be used for local directories served by the web server. If\n# your system is serving content from a sub-directory in /srv you must allow\n# access here, or in any related virtual host.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /usr/share>\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n<Directory /var/www/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n#<Directory /srv/>\n#\tOptions Indexes FollowSymLinks\n#\tAllowOverride None\n#\tRequire all granted\n#</Directory>\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\n#\nAccessFileName .htaccess\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n#\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n\n#\n# The following directives define some format nicknames for use with\n# a CustomLog directive.\n#\n# These deviate from the Common Log Format definitions in that they use %O\n# (the actual bytes sent including headers) instead of %b (the size of the\n# requested file), because the latter makes it impossible to detect partial\n# requests.\n#\n# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.\n# Use mod_remoteip instead.\n#\n#LogFormat \"%v:%p %h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" vhost_combined\nLogFormat \"%t \\\"%r\\\" %>s %O  \\\"%{User-Agent}i\\\"\" vhost_combined\n\n#LogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\n#LogFormat \"%h %l %u %t \\\"%r\\\" %>s %O\" common\nLogFormat \"- %t \\\"%r\\\" %>s %b\" noip\n\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-agent}i\" agent\n\n# Include of directories ignores editors' and dpkg's backup files,\n# see README.Debian for details.\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/*.conf\n\n# Include the virtual host configurations:\n#IncludeOptional sites-enabled/*.conf\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/finalize-1243.conf",
    "content": "#LoadModule ssl_module modules/mod_ssl.so\n\nListen 4443\n<VirtualHost *:4443>\n\t# The ServerName directive sets the request scheme, hostname and port that\n\t# the server uses to identify itself. This is used when creating\n\t# redirection URLs. In the context of virtual hosts, the ServerName\n\t# specifies what hostname must appear in the request's Host: header to\n\t# match this virtual host. For the default virtual host (this file) this\n\t# value is not decisive as it is used as a last resort host regardless.\n\t# However, you must set it for any further virtual host explicitly.\n\tServerName www.eiserneketten.de\n\n\tSSLEngine on\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\t# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n\t# error, crit, alert, emerg.\n\t# It is also possible to configure the loglevel for particular\n\t# modules, e.g.\n\t#LogLevel info ssl:warn\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log noip\n\n\t# For most configuration files from conf-available/, which are\n\t# enabled or disabled at a global level, it is possible to\n\t# include a line for only one particular virtual host. For example the\n\t# following line enables the CGI configuration for this host only\n\t# after it has been globally disabled with \"a2disconf\".\n\t#Include conf-available/serve-cgi-bin.conf\n\t<Directory />\n\t\tOptions FollowSymLinks\n\t\tAllowOverride None\n\t\tOrder Deny,Allow\n\t\t#Deny from All\n\t</Directory>\n\n\tAlias / /eiserneketten/pages/eiserneketten.html\nSSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\nSSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\nSSLCertificateChainFile /etc/ssl/certs/ssl-cert-snakeoil.pem\nInclude /etc/letsencrypt/options-ssl-apache.conf\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n\n#\n# Directives to allow use of AWStats as a CGI\n#\nAlias /awstatsclasses \"/usr/local/awstats/wwwroot/classes/\"\nAlias /awstatscss \"/usr/local/awstats/wwwroot/css/\"\nAlias /awstatsicons \"/usr/local/awstats/wwwroot/icon/\"\nScriptAlias /awstats/ \"/usr/local/awstats/wwwroot/cgi-bin/\"\n\n#\n# This is to permit URL access to scripts/files in AWStats directory.\n#\n<Directory \"/usr/local/awstats/wwwroot\">\n    Options None\n    AllowOverride None\n    Order allow,deny\n    Allow from all\n</Directory>\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/graphite-quote-1934.conf",
    "content": "<VirtualHost *:80>\n\n        WSGIDaemonProcess _graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120 user=www-data group=www-data\n        WSGIProcessGroup _graphite\n        WSGIImportScript /usr/share/graphite-web/graphite.wsgi process-group=_graphite application-group=%{GLOBAL}\n        WSGIScriptAlias / /usr/share/graphite-web/graphite.wsgi\n\n        Alias /content/ /usr/share/graphite-web/static/\n        <Location \"/content/\">\n                SetHandler None\n        </Location>\n\n        ErrorLog ${APACHE_LOG_DIR}/graphite-web_error.log\n\n        # Possible values include: debug, info, notice, warn, error, crit,\n        # alert, emerg.\n        LogLevel warn\n\n        CustomLog ${APACHE_LOG_DIR}/graphite-web_access.log combined\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/ipv6-1143.conf",
    "content": "<VirtualHost *:80 [::]:80>\nDocumentRoot /tmp\nServerName example.com\nServerAlias www.example.com\nCustomLog ${APACHE_LOG_DIR}/example.log combined\n<Directory \"/tmp\">\n  AllowOverride All\n</Directory>\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/ipv6-1143b.conf",
    "content": "<VirtualHost *:443 [::]:443>\nDocumentRoot /tmp\nServerName example.com\nServerAlias www.example.com\nCustomLog ${APACHE_LOG_DIR}/example.log combined\n<Directory \"/tmp\">\n  AllowOverride All\n</Directory>\n\n  SSLEngine on\n\n  SSLHonorCipherOrder On\n  SSLProtocol all -SSLv2 -SSLv3\n  SSLCipherSuite \"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS\"\n\n   SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n   SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/ipv6-1143c.conf",
    "content": "<VirtualHost [::]:80 *:80>\nDocumentRoot /tmp\nServerName example.com\nServerAlias www.example.com\nCustomLog ${APACHE_LOG_DIR}/example.log combined\n<Directory \"/tmp\">\n  AllowOverride All\n</Directory>\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/ipv6-1143d.conf",
    "content": "<VirtualHost [::]:443 *:443>\nDocumentRoot /tmp\nServerName example.com\nServerAlias www.example.com\nCustomLog ${APACHE_LOG_DIR}/example.log combined\n<Directory \"/tmp\">\n  AllowOverride All\n</Directory>\n\n  SSLEngine on\n\n  SSLHonorCipherOrder On\n  SSLProtocol all -SSLv2 -SSLv3\n  SSLCipherSuite \"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS\"\n\n   SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n   SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/missing-quote-1724.conf",
    "content": "<VirtualHost *:443>\n        ServerAdmin webmaster@localhost\n        ServerAlias www.example.com\n        ServerName example.com\n        DocumentRoot /var/www/example.com/www/\n\tSSLEngine on\n\t\n\tSSLProtocol all -SSLv2 -SSLv3\n        SSLCipherSuite \"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$\n        SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n        SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n\n        <Directory />\n                Options FollowSymLinks\n                AllowOverride All\n        </Directory>\n        <Directory /var/www/example.com/www>\n                Options Indexes FollowSymLinks MultiViews\n                AllowOverride All\n                Order allow,deny\n                allow from all\n                # This directive allows us to have apache2's default start page\n                # in /apache2-default/, but still have / go to the right place\n        </Directory>\n\n        ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n        <Directory \"/usr/lib/cgi-bin\">\n                AllowOverride None\n                Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n                Order allow,deny\n                Allow from all\n        </Directory>\n\n        ErrorLog /var/log/apache2/error.log\n\n        # Possible values include: debug, info, notice, warn, error, crit,\n        # alert, emerg.\n        LogLevel warn\n\n        CustomLog /var/log/apache2/access.log combined\n        ServerSignature On\n\n    Alias /apache_doc/ \"/usr/share/doc/\"\n    <Directory \"/usr/share/doc/\">\n        Options Indexes MultiViews FollowSymLinks\n        AllowOverride None\n        Order deny,allow\n        Deny from all\n        Allow from 127.0.0.0/255.0.0.0 ::1/128\n    </Directory>\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/modmacro-1385.conf",
    "content": "<Macro Vhost $host $port $dir>\n        <VirtualHost *:$port>\n                # The ServerName directive sets the request scheme, hostname and port that\n                # the server uses to identify itself. This is used when creating\n                # redirection URLs. In the context of virtual hosts, the ServerName\n                # specifies what hostname must appear in the request's Host: header to\n                # match this virtual host. For the default virtual host (this file) this\n                # value is not decisive as it is used as a last resort host regardless.\n                # However, you must set it for any further virtual host explicitly.\n                ServerName $host\n\n                ServerAdmin webmaster@localhost\n                DocumentRoot $dir\n\n                # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n                # error, crit, alert, emerg.\n                # It is also possible to configure the loglevel for particular\n                # modules, e.g.\n                #LogLevel info ssl:warn\n\n                ErrorLog ${APACHE_LOG_DIR}/error.log\n                CustomLog ${APACHE_LOG_DIR}/access.log combined\n\n                # For most configuration files from conf-available/, which are\n                # enabled or disabled at a global level, it is possible to\n                # include a line for only one particular virtual host. For example the\n                # following line enables the CGI configuration for this host only\n                # after it has been globally disabled with \"a2disconf\".\n                #Include conf-available/serve-cgi-bin.conf\n        </VirtualHost>\n</Macro>\nUse Vhost goxogle.com 80 /var/www/goxogle/\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/owncloud-1264.conf",
    "content": "Alias /owncloud /usr/share/owncloud\n\n<Directory /usr/share/owncloud/>\n    Options +FollowSymLinks\n    AllowOverride All\n    <IfVersion < 2.3>\n        order allow,deny\n        allow from all\n    </IfVersion>\n    <IfVersion >= 2.3>\n        Require all granted\n    </IfVersion>\n</Directory>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/rewrite-quote-1960.conf",
    "content": "<IfModule mod_rewrite.c>\n RewriteEngine On\n RewriteCond %{REQUEST_URI} ^.*(,|;|:|<|>|\">|\"<|/|\\\\\\.\\.\\\\).* [NC,OR]\n RewriteCond %{REQUEST_URI} ^.*(\\=|\\@|\\[|\\]|\\^|\\`|\\{|\\}|\\~).* [NC,OR]\n RewriteCond %{REQUEST_URI} ^.*(\\'|%0A|%0D|%27|%3C|%3E|%00).* [NC]\n RewriteRule ^(.*)$ - [F,L]\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/roundcube-1222.conf",
    "content": "# Those aliases do not work properly with several hosts on your apache server\n# Uncomment them to use it or adapt them to your configuration\n#    Alias /roundcube/program/js/tiny_mce/ /usr/share/tinymce/www/\n#    Alias /roundcube /var/lib/roundcube\n\n# Access to tinymce files\n<Directory \"/usr/share/tinymce/www/\">\n      Options Indexes MultiViews FollowSymLinks\n      AllowOverride None\n      <IfVersion >= 2.3>\n        Require all granted\n      </IfVersion> \n      <IfVersion < 2.3>\n        Order allow,deny\n        Allow from all\n      </IfVersion>\n</Directory>\n\n<Directory /var/lib/roundcube/>\n  Options +FollowSymLinks\n  # This is needed to parse /var/lib/roundcube/.htaccess. See its\n  # content before setting AllowOverride to None.\n  AllowOverride All\n  <IfVersion >= 2.3>\n    Require all granted\n  </IfVersion> \n  <IfVersion < 2.3>\n    Order allow,deny\n    Allow from all\n  </IfVersion>\n</Directory>\n\n# Protecting basic directories:\n<Directory /var/lib/roundcube/config>\n        Options -FollowSymLinks\n        AllowOverride None\n</Directory>\n\n<Directory /var/lib/roundcube/temp>\n        Options -FollowSymLinks\n        AllowOverride None\n        <IfVersion >= 2.3>\n          Require all denied\n        </IfVersion> \n        <IfVersion < 2.3>\n          Order allow,deny\n          Deny from all\n        </IfVersion>\n</Directory>\n\n<Directory /var/lib/roundcube/logs>\n        Options -FollowSymLinks\n        AllowOverride None\n        <IfVersion >= 2.3>\n          Require all denied\n        </IfVersion> \n        <IfVersion < 2.3>\n          Order allow,deny\n          Deny from all\n        </IfVersion>\n</Directory>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/section-continuations-2525.conf",
    "content": "\nNameVirtualHost 0.0.0.0:7080\nNameVirtualHost [00000:000:000:000::0]:7080\nNameVirtualHost 0.0.0.0:7080\n\nNameVirtualHost 127.0.0.1:7080\nNameVirtualHost 0.0.0.0:7081\nNameVirtualHost [0000:000:000:000::2]:7081\nNameVirtualHost 0.0.0.0:7081\n\nNameVirtualHost 127.0.0.1:7081\n\nServerName \"example.com\"\nServerAdmin \"srv@example.com\"\n\nDocumentRoot /tmp\n\n<IfModule mod_logio.c>\n\tLogFormat \"%a %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" plesklog\n</IfModule>\n<IfModule !mod_logio.c>\n\tLogFormat \"%a %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" plesklog\n</IfModule>\n\nTraceEnable off\n\nServerTokens ProductOnly\n\n<Directory \"/var/www/vhosts\">\n\tAllowOverride \"All\"\n\tOptions SymLinksIfOwnerMatch\n\tOrder allow,deny\n\tAllow from all\n\n\t<IfModule sapi_apache2.c>\n\t\tphp_admin_flag engine off\n\t</IfModule>\n\n\t<IfModule mod_php5.c>\n\t\tphp_admin_flag engine off\n\t</IfModule>\n\n</Directory>\n\n<Directory \"/usr/lib/mailman\">\n\tAllowOverride \"All\"\n\tOptions SymLinksIfOwnerMatch\n\tOrder allow,deny\n\tAllow from all\n\t<IfModule sapi_apache2.c>\n\t\tphp_admin_flag engine off\n\t</IfModule>\n\t<IfModule mod_php5.c>\n\t\tphp_admin_flag engine off\n\t</IfModule>\n</Directory>\n\n<IfModule mod_headers.c>\n\tHeader add X-Powered-By PleskLin\n</IfModule>\n\n<IfModule mod_security2.c>\n\tSecRuleEngine DetectionOnly\n\tSecRequestBodyAccess On\n\tSecRequestBodyLimit 134217728\n\tSecResponseBodyAccess Off\n\tSecResponseBodyLimit 524288\n\tSecAuditEngine On\n\tSecAuditLog \"/var/log/modsec_audit.log\"\n\tSecAuditLogType serial\n</IfModule>\n\n#Include \"/etc/httpd/conf/plesk.conf.d/ip_default/*.conf\"\n\n<VirtualHost \\\n\t0.0.0.0:7080 \\\n\t[00000:000:000:0000::2]:7080 \\\n\t0.0.0.0:7080 \\\n\t127.0.0.1:7080 \\\n\t>\n\tServerName \"default\"\n\tUseCanonicalName Off\n\tDocumentRoot /tmp\n\tScriptAlias \"/cgi-bin/\" \"/var/www/vhosts/default/cgi-bin\"\n\n\t<IfModule mod_ssl.c>\n\t\tSSLEngine off\n\t</IfModule>\n\n\t<Directory \"/var/www/vhosts/default/cgi-bin\">\n\t\tAllowOverride None\n\t\tOptions None\n\t\tOrder allow,deny\n\t\tAllow from all\n\t</Directory>\n\n\t<Directory \"/var/www/vhosts/default/htdocs\">\n\n\t\t<IfModule sapi_apache2.c>\n\t\t\tphp_admin_flag engine on\n\t\t</IfModule>\n\n\t\t<IfModule mod_php5.c>\n\t\t\tphp_admin_flag engine on\n\t\t</IfModule>\n\n\t</Directory>\n\n</VirtualHost>\n\n<IfModule mod_ssl.c>\n\n\t<VirtualHost \\\n\t\t0.0.0.0:7081 \\\n\t\t127.0.0.1:7081 \\\n\t\t>\n\t\tServerName \"default-0_0_0_0\"\n\t\tUseCanonicalName Off\n\t\tDocumentRoot /tmp\n\t\tScriptAlias \"/cgi-bin/\" \"/var/www/vhosts/default/cgi-bin\"\n\n\t\tSSLEngine on\n\t\tSSLVerifyClient none\n\t\tSSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n\n\t\t<Directory \"/var/www/vhosts/default/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions None\n\t\t\tOrder allow,deny\n\t\t\tAllow from all\n\t\t</Directory>\n\n\t\t<Directory \"/var/www/vhosts/default/htdocs\">\n\n\t\t\t<IfModule sapi_apache2.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t\t<IfModule mod_php5.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t</Directory>\n\n\t</VirtualHost>\n\t<VirtualHost \\\n\t\t[00000:000:000:000::2]:7081 \\\n\t\t127.0.0.1:7081 \\\n\t\t>\n\t\tServerName \"default-0000_000_000_00000__2\"\n\t\tUseCanonicalName Off\n\t\tDocumentRoot /tmp\n\t\tScriptAlias \"/cgi-bin/\" \"/var/www/vhosts/default/cgi-bin\"\n\n\t\tSSLEngine on\n\t\tSSLVerifyClient none\n\t\tSSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n\n\t\t<Directory \"/var/www/vhosts/default/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions None\n\t\t\tOrder allow,deny\n\t\t\tAllow from all\n\t\t</Directory>\n\n\t\t<Directory \"/var/www/vhosts/default/htdocs\">\n\n\t\t\t<IfModule sapi_apache2.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t\t<IfModule mod_php5.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t</Directory>\n\n\t</VirtualHost>\n\t<VirtualHost \\\n\t\t0.0.0.0:7081 \\\n\t\t127.0.0.1:7081 \\\n\t\t>\n\t\tServerName \"default-0_0_0_0\"\n\t\tUseCanonicalName Off\n\t\tDocumentRoot /tmp\n\t\tScriptAlias \"/cgi-bin/\" \"/var/www/vhosts/default/cgi-bin\"\n\n\t\tSSLEngine on\n\t\tSSLVerifyClient none\n\t\tSSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n\n\t\t#SSLCACertificateFile \"/usr/local/psa/var/certificates/cert-nLy6Z1\"\n\n\t\t<Directory \"/var/www/vhosts/default/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions None\n\t\t\tOrder allow,deny\n\t\t\tAllow from all\n\t\t</Directory>\n\n\t\t<Directory \"/var/www/vhosts/default/htdocs\">\n\n\t\t\t<IfModule sapi_apache2.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t\t<IfModule mod_php5.c>\n\t\t\t\tphp_admin_flag engine on\n\t\t\t</IfModule>\n\n\t\t</Directory>\n\n\t</VirtualHost>\n\n</IfModule>\n\n<VirtualHost \\\n\t0.0.0.0:7080 \\\n\t[0000:000:000:000::2]:7080 \\\n\t0.0.0.0:7080 \\\n\t127.0.0.1:7080 \\\n\t>\n\tDocumentRoot /tmp\n\tServerName lists\n\tServerAlias lists.*\n\tUseCanonicalName Off\n\n\tScriptAlias \"/mailman/\" \"/usr/lib/mailman/cgi-bin/\"\n\n\tAlias \"/icons/\" \"/var/www/icons/\"\n\tAlias \"/pipermail/\" \"/var/lib/mailman/archives/public/\"\n\n\t<IfModule mod_ssl.c>\n\t\tSSLEngine off\n\t</IfModule>\n\n\t<Directory \"/var/lib/mailman/archives/\">\n\t\tOptions FollowSymLinks\n\t\tOrder allow,deny\n\t\tAllow from all\n\t</Directory>\n\n</VirtualHost>\n\n<IfModule mod_ssl.c>\n\t<VirtualHost \\\n\t\t0.0.0.0:7081 \\\n\t\t[00000:000:000:0000::2]:7081 \\\n\t\t0.0.0.0:7081 \\\n\t\t127.0.0.1:7081 \\\n\t\t>\n\t\tDocumentRoot /tmp\n\t\tServerName lists\n\t\tServerAlias lists.*\n\t\tUseCanonicalName Off\n\n\t\tScriptAlias \"/mailman/\" \"/usr/lib/mailman/cgi-bin/\"\n\n\t\tAlias \"/icons/\" \"/var/www/icons/\"\n\t\tAlias \"/pipermail/\" \"/var/lib/mailman/archives/public/\"\n\n\t\tSSLEngine on\n\t\tSSLVerifyClient none\n\t\tSSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n\n\t\t<Directory \"/var/lib/mailman/archives/\">\n\t\t\tOptions FollowSymLinks\n\t\t\tOrder allow,deny\n\t\t\tAllow from all\n\t\t</Directory>\n\n\t</VirtualHost>\n</IfModule>\n\n<IfModule mod_rpaf.c>\n\tRPAFproxy_ips 0.0.0.0 [00000:000:000:00000::2] 0.0.0.0\n</IfModule>\n<IfModule mod_rpaf-2.0.c>\n\tRPAFproxy_ips 0.0.0.0 [0000:000:000:0000::2] 0.0.0.0\n</IfModule>\n<IfModule mod_remoteip.c>\n\tRemoteIPInternalProxy 0.0.0.0 [0000:000:000:0000::2] 0.0.0.0\n\tRemoteIPHeader X-Forwarded-For\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/section-empty-continuations-2731.conf",
    "content": "#ATTENTION!\n#\n#DO NOT MODIFY THIS FILE BECAUSE IT WAS GENERATED AUTOMATICALLY,\n#SO ALL YOUR CHANGES WILL BE LOST THE NEXT TIME THE FILE IS GENERATED.\n\nNameVirtualHost 192.168.100.218:80\nNameVirtualHost 10.128.178.192:80\n\nNameVirtualHost 192.168.100.218:443\nNameVirtualHost 10.128.178.192:443\n\n\nServerName \"254020-web1.example.com\"\nServerAdmin \"name@example.com\"\n\nDocumentRoot \"/tmp\"\n\n<IfModule mod_logio.c>\n    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" plesklog\n</IfModule>\n<IfModule !mod_logio.c>\n    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" plesklog\n</IfModule>\n\n\tTraceEnable off\n\nServerTokens ProductOnly\n\n<Directory \"/var/www/vhosts\">\n    AllowOverride \"All\"\n    Options SymLinksIfOwnerMatch\n    Order allow,deny\n    Allow from all\n\n<IfModule sapi_apache2.c>\nphp_admin_flag engine off\n</IfModule>\n\n<IfModule mod_php5.c>\nphp_admin_flag engine off\n</IfModule>\n\n</Directory>\n\n<Directory \"/usr/lib/mailman\">\n    AllowOverride All\n    Options SymLinksIfOwnerMatch\n    Order allow,deny\n    Allow from all\n    <IfModule sapi_apache2.c>\n        php_admin_flag engine off\n    </IfModule>\n    <IfModule mod_php5.c>\n        php_admin_flag engine off\n    </IfModule>\n</Directory>\n\n<IfModule mod_headers.c>\n    Header add X-Powered-By PleskLin\n</IfModule>\n\n<IfModule mod_jk.c>\n   JkWorkersFile \"/etc/httpd/conf/workers.properties\"\n      JkLogFile /var/log/httpd/mod_jk.log\n   JkLogLevel info\n</IfModule>\n\n#Include \"/etc/httpd/conf/plesk.conf.d/ip_default/*.conf\"\n\n\n    <VirtualHost \\\n        192.168.100.218:80 \\\n        10.128.178.192:80 \\\n         \\\n    >\n     \tServerName \"default\"\n        UseCanonicalName Off\n        DocumentRoot \"/tmp\"\n        ScriptAlias /cgi-bin/ \"/var/www/vhosts/default/cgi-bin\"\n\n\n        <IfModule mod_ssl.c>\n            SSLEngine off\n        </IfModule>\n\n        <Directory \"/var/www/vhosts/default/cgi-bin\">\n            AllowOverride None\n            Options None\n            Order allow,deny\n            Allow from all\n        </Directory>\n\n        <Directory /var/www/vhosts/default/htdocs>\n\n<IfModule sapi_apache2.c>\nphp_admin_flag engine on\n</IfModule>\n\n<IfModule mod_php5.c>\nphp_admin_flag engine on\n</IfModule>\n\n\t</Directory>\n\n    </VirtualHost>\n\n<IfModule mod_ssl.c>\n\n    <VirtualHost \\\n        192.168.100.218:443 \\\n         \\\n    >\n     \tServerName \"default-192_168_100_218\"\n        UseCanonicalName Off\n        DocumentRoot \"/tmp\"\n        ScriptAlias /cgi-bin/ \"/var/www/vhosts/default/cgi-bin\"\n\n\n        SSLEngine on\n        SSLVerifyClient none\n        #SSLCertificateFile \"/usr/local/psa/var/certificates/cert-9MgutN\"\n\n        #SSLCACertificateFile \"/usr/local/psa/var/certificates/cert-s6Wx3P\"\n\n        <Directory \"/var/www/vhosts/default/cgi-bin\">\n            AllowOverride None\n            Options None\n            Order allow,deny\n            Allow from all\n        </Directory>\n\n        <Directory /var/www/vhosts/default/htdocs>\n\n<IfModule sapi_apache2.c>\nphp_admin_flag engine on\n</IfModule>\n\n<IfModule mod_php5.c>\nphp_admin_flag engine on\n</IfModule>\n\n\t</Directory>\n\n    </VirtualHost>\n    <VirtualHost \\\n        10.128.178.192:443 \\\n         \\\n    >\n     \tServerName \"default-10_128_178_192\"\n        UseCanonicalName Off\n        DocumentRoot \"/tmp\"\n        ScriptAlias /cgi-bin/ \"/var/www/vhosts/default/cgi-bin\"\n\n\n        SSLEngine on\n        SSLVerifyClient none\n        #SSLCertificateFile \"/usr/local/psa/var/certificates/certxfb6025\"\n\n\n        <Directory \"/var/www/vhosts/default/cgi-bin\">\n            AllowOverride None\n            Options None\n            Order allow,deny\n            Allow from all\n        </Directory>\n\n        <Directory /var/www/vhosts/default/htdocs>\n\n<IfModule sapi_apache2.c>\nphp_admin_flag engine on\n</IfModule>\n\n<IfModule mod_php5.c>\nphp_admin_flag engine on\n</IfModule>\n\n\t</Directory>\n\n    </VirtualHost>\n\n</IfModule>\n\n\n<VirtualHost \\\n\t192.168.100.218:80 \\\n                10.128.178.192:80 \\\n             \\\n>\n        DocumentRoot \"/tmp\"\n        ServerName lists\n        ServerAlias lists.*\n        UseCanonicalName Off\n\n        ScriptAlias \"/mailman/\" \"/usr/lib/mailman/cgi-bin/\"\n\n        Alias \"/icons/\" \"/var/www/icons/\"\n        Alias \"/pipermail/\" \"/var/lib/mailman/archives/public/\"\n\n        <IfModule mod_ssl.c>\n            SSLEngine off\n        </IfModule>\n\n\n        <Directory /var/lib/mailman/archives/>\n            Options FollowSymLinks\n            Order allow,deny\n            Allow from all\n        </Directory>\n\n    </VirtualHost>\n\n<IfModule mod_ssl.c>\n<VirtualHost \\\n        192.168.100.218:443 \\\n                10.128.178.192:443 \\\n             \\\n>\n        DocumentRoot \"/tmp\"\n        ServerName lists\n        ServerAlias lists.*\n        UseCanonicalName Off\n\n        ScriptAlias \"/mailman/\" \"/usr/lib/mailman/cgi-bin/\"\n\n        Alias \"/icons/\" \"/var/www/icons/\"\n        Alias \"/pipermail/\" \"/var/lib/mailman/archives/public/\"\n\n        SSLEngine on\n        SSLVerifyClient none\n        #SSLCertificateFile \"/usr/local/psa/var/certificates/certxfb6025\"\n\n\n        <Directory /var/lib/mailman/archives/>\n            Options FollowSymLinks\n            Order allow,deny\n            Allow from all\n        </Directory>\n\n    </VirtualHost>\n</IfModule>\n\n<IfModule mod_rpaf.c>\n    RPAFproxy_ips  192.168.100.218 10.128.178.192\n</IfModule>\n<IfModule mod_rpaf-2.0.c>\n    RPAFproxy_ips  192.168.100.218 10.128.178.192\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/semacode-1598.conf",
    "content": "<VirtualHost *:80>\n  ServerName      semacode.com\n  ServerAlias     www.semacode.com\n  DocumentRoot    /tmp/\n  TransferLog     /tmp/access\n  ErrorLog        /tmp/error\n  Redirect        /posts/rss              http://semacode.com/feed\n  Redirect        permanent   /weblog                 http://semacode.com/blog\n\n#ProxyPreserveHost On\n#  ProxyPass /past http://old.semacode.com\n  #ProxyPassReverse /past http://old.semacode.com\n#<proxy>\n # Order allow,deny\n  #Allow from all\n#</proxy>\n\n  Redirect /stylesheets/inside.css http://old.semacode.com/stylesheets/inside.css\n  RedirectMatch /images/portal/(.*) http://old.semacode.com/images/portal/$1\n  Redirect /images/invisible.gif http://old.semacode.com/images/invisible.gif\n  RedirectMatch /javascripts/(.*) http://old.semacode.com/javascripts/$1\n\n  RewriteEngine on\n  RewriteRule ^/past/(.*)                 http://old.semacode.com/past/$1 [L,P]\n  RewriteCond %{HTTP_HOST}                !^semacode\\.com$ [NC]\n  RewriteCond %{HTTP_HOST}                !^$\n  RewriteRule ^/(.*)                      http://semacode.com/$1 [L,R]\n\n</VirtualHost>\n\n\n<VirtualHost *:80>\n  ServerName old.semacode.com\n  ServerAlias www.old.semacode.com\n  DocumentRoot /home/simon/semacode-server/semacode/website/trunk/public\n  TransferLog     /tmp/access-old\n  ErrorLog        /tmp/error-old\n  <Directory \"/home/simon/semacode-server/semacode/website/trunk/public\">\n    Options FollowSymLinks\n    AllowOverride None\n    Order allow,deny\n    Allow from all\n  </Directory>\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/sslrequire-wordlist-1827.htaccess",
    "content": "SSLRequire %{SSL_CLIENT_S_DN_CN} in {\"foo@bar.com\", \"bar@foo.com\"}\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/passing/two-blocks-one-line-1693.conf",
    "content": "<IfModule mod_ssl.c>\n        <VirtualHost *:443>\n                ServerAdmin info@somethingnewentertainment.com\n                ServerName somethingnewentertainment.com\n                DocumentRoot /var/www/html\n \n                ErrorLog /var/log/apache2/error.log\n                CustomLog /var/log/apache2/access.log combined\n \n                SSLEngine on\n                SSLProtocol all -SSLv2 -SSLv3\n                SSLHonorCipherOrder on\n                SSLCipherSuite \"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EEC DH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRS A RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4\"\n \n                SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem\n                SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key\n \n                <FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n                                SSLOptions +StdEnvVars\n                </FilesMatch>\n                <Directory /usr/lib/cgi-bin>\n                                SSLOptions +StdEnvVars\n                </Directory>\n                BrowserMatch \"MSIE [2-6]\" \\\n                                nokeepalive ssl-unclean-shutdown \\\n                                downgrade-1.0 force-response-1.0\n                BrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n        </VirtualHost> </IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/augeasnode_test.py",
    "content": "\"\"\"Tests for AugeasParserNode classes\"\"\"\nimport os\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import augeasparser\nfrom certbot_apache._internal.tests import util\n\n\ndef _get_augeasnode_mock(filepath):\n    \"\"\" Helper function for mocking out DualNode instance with an AugeasNode \"\"\"\n    def augeasnode_mock(metadata):\n        return augeasparser.AugeasBlockNode(\n            name=assertions.PASS,\n            ancestor=None,\n            filepath=filepath,\n            metadata=metadata)\n    return augeasnode_mock\n\n\nclass AugeasParserNodeTest(util.ApacheTest):  # pylint: disable=too-many-public-methods\n    \"\"\"Test AugeasParserNode using available test configurations\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n\n        with mock.patch(\n            \"certbot_apache._internal.configurator.ApacheConfigurator.get_parsernode_root\"\n        ) as mock_parsernode:\n            mock_parsernode.side_effect = _get_augeasnode_mock(\n                                              os.path.join(self.config_path, \"apache2.conf\"))\n            self.config = util.get_apache_configurator(\n                self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n                use_parsernode=True,\n            )\n        self.vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def test_save(self):\n        with mock.patch('certbot_apache._internal.parser.ApacheParser.save') as mock_save:\n            self.config.parser_root.save(\"A save message\")\n        assert mock_save.called is True\n        assert mock_save.call_args[0][0] == \"A save message\"\n\n    def test_unsaved_files(self):\n        with mock.patch('certbot_apache._internal.parser.ApacheParser.unsaved_files') as mock_uf:\n            mock_uf.return_value = [\"first\", \"second\"]\n            files = self.config.parser_root.unsaved_files()\n        assert files == [\"first\", \"second\"]\n\n    def test_get_block_node_name(self):\n        from certbot_apache._internal.augeasparser import AugeasBlockNode\n        block = AugeasBlockNode(\n            name=assertions.PASS,\n            ancestor=None,\n            filepath=assertions.PASS,\n            metadata={\"augeasparser\": mock.Mock(), \"augeaspath\": \"/files/anything\"}\n        )\n        testcases = {\n            \"/some/path/FirstNode/SecondNode\": \"SecondNode\",\n            \"/some/path/FirstNode/SecondNode/\": \"SecondNode\",\n            \"OnlyPathItem\": \"OnlyPathItem\",\n            \"/files/etc/apache2/apache2.conf/VirtualHost\": \"VirtualHost\",\n            \"/Anything\": \"Anything\",\n        }\n        for test in testcases:\n            # pylint: disable=protected-access\n            assert block._aug_get_name(test) == testcases[test]\n\n    def test_find_blocks(self):\n        blocks = self.config.parser_root.find_blocks(\"VirtualHost\", exclude=False)\n        assert len(blocks) == 12\n\n    def test_find_blocks_case_insensitive(self):\n        vhs = self.config.parser_root.find_blocks(\"VirtualHost\")\n        vhs2 = self.config.parser_root.find_blocks(\"viRtuAlHoST\")\n        assert len(vhs) == len(vhs2)\n\n    def test_find_directive_found(self):\n        directives = self.config.parser_root.find_directives(\"Listen\")\n        assert len(directives) == 1\n        assert directives[0].filepath.endswith(\"/apache2/ports.conf\") is True\n        assert directives[0].parameters == (u'80',)\n\n    def test_find_directive_notfound(self):\n        directives = self.config.parser_root.find_directives(\"Nonexistent\")\n        assert len(directives) == 0\n\n    def test_find_directive_from_block(self):\n        blocks = self.config.parser_root.find_blocks(\"virtualhost\")\n        found = False\n        for vh in blocks:\n            if vh.filepath.endswith(\"sites-enabled/certbot.conf\"):\n                servername = vh.find_directives(\"servername\")\n                assert servername[0].parameters[0] == \"certbot.demo\"\n                found = True\n        assert found is True\n\n    def test_find_comments(self):\n        rootcomment = self.config.parser_root.find_comments(\n            \"This is the main Apache server configuration file. \"\n        )\n        assert len(rootcomment) == 1\n        assert rootcomment[0].filepath.endswith(\n            \"debian_apache_2_4/multiple_vhosts/apache2/apache2.conf\"\n        ) is True\n\n    def test_set_parameters(self):\n        servernames = self.config.parser_root.find_directives(\"servername\")\n        names: list[str] = []\n        for servername in servernames:\n            names += servername.parameters\n        assert \"going_to_set_this\" not in names\n        servernames[0].set_parameters([\"something\", \"going_to_set_this\"])\n        servernames = self.config.parser_root.find_directives(\"servername\")\n        names = []\n        for servername in servernames:\n            names += servername.parameters\n        assert \"going_to_set_this\" in names\n\n    def test_set_parameters_atinit(self):\n        from certbot_apache._internal.augeasparser import AugeasDirectiveNode\n        servernames = self.config.parser_root.find_directives(\"servername\")\n        setparam = \"certbot_apache._internal.augeasparser.AugeasDirectiveNode.set_parameters\"\n        with mock.patch(setparam) as mock_set:\n            AugeasDirectiveNode(\n                name=servernames[0].name,\n                parameters=[\"test\", \"setting\", \"these\"],\n                ancestor=assertions.PASS,\n                metadata=servernames[0].metadata\n            )\n            assert mock_set.called is True\n            assert mock_set.call_args_list[0][0][0] == \\\n                [\"test\", \"setting\", \"these\"]\n\n    def test_set_parameters_delete(self):\n        # Set params\n        servername = self.config.parser_root.find_directives(\"servername\")[0]\n        servername.set_parameters([\"thisshouldnotexistpreviously\", \"another\",\n                                   \"third\"])\n\n        # Delete params\n        servernames = self.config.parser_root.find_directives(\"servername\")\n        found = False\n        for servername in servernames:\n            if \"thisshouldnotexistpreviously\" in servername.parameters:\n                assert len(servername.parameters) == 3\n                servername.set_parameters([\"thisshouldnotexistpreviously\"])\n                found = True\n        assert found is True\n\n        # Verify params\n        servernames = self.config.parser_root.find_directives(\"servername\")\n        found = False\n        for servername in servernames:\n            if \"thisshouldnotexistpreviously\" in servername.parameters:\n                assert len(servername.parameters) == 1\n                servername.set_parameters([\"thisshouldnotexistpreviously\"])\n                found = True\n        assert found is True\n\n    def test_add_child_comment(self):\n        newc = self.config.parser_root.add_child_comment(\"The content\")\n        comments = self.config.parser_root.find_comments(\"The content\")\n        assert len(comments) == 1\n        assert newc.metadata[\"augeaspath\"] == \\\n            comments[0].metadata[\"augeaspath\"]\n        assert newc.comment == comments[0].comment\n\n    def test_delete_child(self):\n        listens = self.config.parser_root.find_directives(\"Listen\")\n        assert len(listens) == 1\n        self.config.parser_root.delete_child(listens[0])\n\n        listens = self.config.parser_root.find_directives(\"Listen\")\n        assert len(listens) == 0\n\n    def test_delete_child_not_found(self):\n        listen = self.config.parser_root.find_directives(\"Listen\")[0]\n        listen.metadata[\"augeaspath\"] = \"/files/something/nonexistent\"\n\n        with pytest.raises(errors.PluginError):\n            self.config.parser_root.delete_child(listen)\n\n    def test_add_child_block(self):\n        nb = self.config.parser_root.add_child_block(\n            \"NewBlock\",\n            [\"first\", \"second\"]\n        )\n        rpath, _, directive = nb.metadata[\"augeaspath\"].rpartition(\"/\")\n        assert rpath == \\\n            self.config.parser_root.metadata[\"augeaspath\"]\n        assert directive.startswith(\"NewBlock\") is True\n\n    def test_add_child_block_beginning(self):\n        self.config.parser_root.add_child_block(\n            \"Beginning\",\n            position=0\n        )\n        parser = self.config.parser_root.parser\n        root_path = self.config.parser_root.metadata[\"augeaspath\"]\n        # Get first child\n        first = parser.aug.match(\"{}/*[1]\".format(root_path))\n        assert first[0].endswith(\"Beginning\") is True\n\n    def test_add_child_block_append(self):\n        self.config.parser_root.add_child_block(\n            \"VeryLast\",\n        )\n        parser = self.config.parser_root.parser\n        root_path = self.config.parser_root.metadata[\"augeaspath\"]\n        # Get last child\n        last = parser.aug.match(\"{}/*[last()]\".format(root_path))\n        assert last[0].endswith(\"VeryLast\") is True\n\n    def test_add_child_block_append_alt(self):\n        self.config.parser_root.add_child_block(\n            \"VeryLastAlt\",\n            position=99999\n        )\n        parser = self.config.parser_root.parser\n        root_path = self.config.parser_root.metadata[\"augeaspath\"]\n        # Get last child\n        last = parser.aug.match(\"{}/*[last()]\".format(root_path))\n        assert last[0].endswith(\"VeryLastAlt\") is True\n\n    def test_add_child_block_middle(self):\n        self.config.parser_root.add_child_block(\n            \"Middle\",\n            position=5\n        )\n        parser = self.config.parser_root.parser\n        root_path = self.config.parser_root.metadata[\"augeaspath\"]\n        # Augeas indices start at 1 :(\n        middle = parser.aug.match(\"{}/*[6]\".format(root_path))\n        assert middle[0].endswith(\"Middle\") is True\n\n    def test_add_child_block_existing_name(self):\n        parser = self.config.parser_root.parser\n        root_path = self.config.parser_root.metadata[\"augeaspath\"]\n        # There already exists a single VirtualHost in the base config\n        new_block = parser.aug.match(\"{}/VirtualHost[2]\".format(root_path))\n        assert len(new_block) == 0\n        vh = self.config.parser_root.add_child_block(\n            \"VirtualHost\",\n        )\n        new_block = parser.aug.match(\"{}/VirtualHost[2]\".format(root_path))\n        assert len(new_block) == 1\n        assert vh.metadata[\"augeaspath\"].endswith(\"VirtualHost[2]\") is True\n\n    def test_node_init_error_bad_augeaspath(self):\n        from certbot_apache._internal.augeasparser import AugeasBlockNode\n        parameters = {\n            \"name\": assertions.PASS,\n            \"ancestor\": None,\n            \"filepath\": assertions.PASS,\n            \"metadata\": {\n                \"augeasparser\": mock.Mock(),\n                \"augeaspath\": \"/files/path/endswith/slash/\"\n            }\n        }\n        with pytest.raises(errors.PluginError):\n            AugeasBlockNode(**parameters)\n\n    def test_node_init_error_missing_augeaspath(self):\n        from certbot_apache._internal.augeasparser import AugeasBlockNode\n        parameters = {\n            \"name\": assertions.PASS,\n            \"ancestor\": None,\n            \"filepath\": assertions.PASS,\n            \"metadata\": {\n                \"augeasparser\": mock.Mock(),\n            }\n        }\n        with pytest.raises(errors.PluginError):\n            AugeasBlockNode(**parameters)\n\n    def test_add_child_directive(self):\n        self.config.parser_root.add_child_directive(\n            \"ThisWasAdded\",\n            [\"with\", \"parameters\"],\n            position=0\n        )\n        dirs = self.config.parser_root.find_directives(\"ThisWasAdded\")\n        assert len(dirs) == 1\n        assert dirs[0].parameters == (\"with\", \"parameters\")\n        # The new directive was added to the very first line of the config\n        assert dirs[0].metadata[\"augeaspath\"].endswith(\"[1]\") is True\n\n    def test_add_child_directive_exception(self):\n        with pytest.raises(errors.PluginError):\n            self.config.parser_root.add_child_directive(\"ThisRaisesErrorBecauseMissingParameters\")\n\n    def test_parsed_paths(self):\n        paths = self.config.parser_root.parsed_paths()\n        assert len(paths) == 6\n\n    def test_find_ancestors(self):\n        vhsblocks = self.config.parser_root.find_blocks(\"VirtualHost\")\n        macro_test = False\n        nonmacro_test = False\n        for vh in vhsblocks:\n            if \"/macro/\" in vh.metadata[\"augeaspath\"].lower():\n                ancs = vh.find_ancestors(\"Macro\")\n                assert len(ancs) == 1\n                macro_test = True\n            else:\n                ancs = vh.find_ancestors(\"Macro\")\n                assert len(ancs) == 0\n                nonmacro_test = True\n        assert macro_test is True\n        assert nonmacro_test is True\n\n    def test_find_ancestors_bad_path(self):\n        self.config.parser_root.metadata[\"augeaspath\"] = \"\"\n        ancs = self.config.parser_root.find_ancestors(\"Anything\")\n        assert len(ancs) == 0\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/autohsts_test.py",
    "content": "# pylint: disable=too-many-lines\n\"\"\"Test for certbot_apache._internal.configurator AutoHSTS functionality\"\"\"\nimport re\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot_apache._internal import constants\nfrom certbot_apache._internal.tests import util\n\n\nclass AutoHSTSTest(util.ApacheTest):\n    \"\"\"Tests for AutoHSTS feature\"\"\"\n    # pylint: disable=protected-access\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir)\n        self.config.parser.modules[\"headers_module\"] = None\n        self.config.parser.modules[\"mod_headers.c\"] = None\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n\n        self.vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def get_autohsts_value(self, vh_path):\n        \"\"\" Get value from Strict-Transport-Security header \"\"\"\n        header_path = self.config.parser.find_dir(\"Header\", None, vh_path)\n        if header_path:\n            pat = '(?:[ \"]|^)(strict-transport-security)(?:[ \"]|$)'\n            for head in header_path:\n                if re.search(pat, self.config.parser.aug.get(head).lower()):\n                    return self.config.parser.aug.get(\n                        head.replace(\"arg[3]\", \"arg[4]\"))\n        return None  # pragma: no cover\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.enable_mod\")\n    def test_autohsts_enable_headers_mod(self, mock_enable, _restart):\n        self.config.parser.modules.pop(\"headers_module\", None)\n        self.config.parser.modules.pop(\"mod_header.c\", None)\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        assert mock_enable.called is True\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    def test_autohsts_deploy_already_exists(self, _restart):\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        with pytest.raises(errors.PluginEnhancementAlreadyPresent):\n            self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n\n    @mock.patch(\"certbot_apache._internal.constants.AUTOHSTS_FREQ\", 0)\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.prepare\")\n    def test_autohsts_increase(self, mock_prepare, _mock_restart):\n        self.config._prepared = False\n        maxage = \"\\\"max-age={0}\\\"\"\n        initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])\n        inc_val = maxage.format(constants.AUTOHSTS_STEPS[1])\n\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        # Verify initial value\n        assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                          initial_val\n        # Increase\n        self.config.update_autohsts(mock.MagicMock())\n        # Verify increased value\n        assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                          inc_val\n        assert mock_prepare.called is True\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._autohsts_increase\")\n    def test_autohsts_increase_noop(self, mock_increase, _restart):\n        maxage = \"\\\"max-age={0}\\\"\"\n        initial_val = maxage.format(constants.AUTOHSTS_STEPS[0])\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        # Verify initial value\n        assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                          initial_val\n\n        self.config.update_autohsts(mock.MagicMock())\n        # Freq not patched, so value shouldn't increase\n        assert mock_increase.called is False\n\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.constants.AUTOHSTS_FREQ\", 0)\n    def test_autohsts_increase_no_header(self, _restart):\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        # Remove the header\n        dir_locs = self.config.parser.find_dir(\"Header\", None,\n                                              self.vh_truth[7].path)\n        dir_loc = \"/\".join(dir_locs[0].split(\"/\")[:-1])\n        self.config.parser.aug.remove(dir_loc)\n        with pytest.raises(errors.PluginError):\n            self.config.update_autohsts(mock.MagicMock())\n\n    @mock.patch(\"certbot_apache._internal.constants.AUTOHSTS_FREQ\", 0)\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    def test_autohsts_increase_and_make_permanent(self, _mock_restart):\n        maxage = \"\\\"max-age={0}\\\"\"\n        max_val = maxage.format(constants.AUTOHSTS_PERMANENT)\n        mock_lineage = mock.MagicMock()\n        mock_lineage.key_path = \"/etc/apache2/ssl/key-certbot_15.pem\"\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\"])\n        for i in range(len(constants.AUTOHSTS_STEPS)-1):\n            # Ensure that value is not made permanent prematurely\n            self.config.deploy_autohsts(mock_lineage)\n            assert self.get_autohsts_value(self.vh_truth[7].path) != \\\n                                 max_val\n            self.config.update_autohsts(mock.MagicMock())\n            # Value should match pre-permanent increment step\n            cur_val = maxage.format(constants.AUTOHSTS_STEPS[i+1])\n            assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                              cur_val\n        # Ensure that the value is raised to max\n        assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                          maxage.format(constants.AUTOHSTS_STEPS[-1])\n        # Make permanent\n        self.config.deploy_autohsts(mock_lineage)\n        assert self.get_autohsts_value(self.vh_truth[7].path) == \\\n                          max_val\n\n    def test_autohsts_update_noop(self):\n        with mock.patch(\"certbot_apache._internal.configurator.time\") as mock_time_module:\n            # Time mock is used to make sure that the execution does not\n            # continue when no autohsts entries exist in pluginstorage\n            self.config.update_autohsts(mock.MagicMock())\n            assert not mock_time_module.time.called\n\n    def test_autohsts_make_permanent_noop(self):\n        self.config.storage.put = mock.MagicMock()\n        self.config.deploy_autohsts(mock.MagicMock())\n        # Make sure that the execution does not continue when no entries in store\n        assert self.config.storage.put.called is False\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    def test_autohsts_no_ssl_vhost(self, mock_select):\n        mock_select.return_value = self.vh_truth[0]\n        with mock.patch(\"certbot_apache._internal.configurator.logger.error\") as mock_log:\n            with pytest.raises(errors.PluginError):\n                self.config.enable_autohsts(mock.MagicMock(), \"invalid.example.com\")\n            assert \"Certbot was not able to find SSL\" in mock_log.call_args[0][0]\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.add_vhost_id\")\n    def test_autohsts_dont_enhance_twice(self, mock_id, _restart):\n        mock_id.return_value = \"1234567\"\n        self.config.enable_autohsts(mock.MagicMock(), [\"ocspvhost.com\", \"ocspvhost.com\"])\n        assert mock_id.call_count == 1\n\n    def test_autohsts_remove_orphaned(self):\n        # pylint: disable=protected-access\n        self.config._autohsts_fetch_state()\n        self.config._autohsts[\"orphan_id\"] = {\"laststep\": 0, \"timestamp\": 0}\n\n        self.config._autohsts_save_state()\n        self.config.update_autohsts(mock.MagicMock())\n        assert \"orphan_id\" not in self.config._autohsts\n        # Make sure it's removed from the pluginstorage file as well\n        self.config._autohsts = None\n        self.config._autohsts_fetch_state()\n        assert not self.config._autohsts\n\n    def test_autohsts_make_permanent_vhost_not_found(self):\n        # pylint: disable=protected-access\n        self.config._autohsts_fetch_state()\n        self.config._autohsts[\"orphan_id\"] = {\"laststep\": 999, \"timestamp\": 0}\n        self.config._autohsts_save_state()\n        with mock.patch(\"certbot_apache._internal.configurator.logger.error\") as mock_log:\n            self.config.deploy_autohsts(mock.MagicMock())\n            assert mock_log.called is True\n            assert \"VirtualHost with id orphan_id was not\" in mock_log.call_args[0][0]\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/centos_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.configurator for Centos overrides\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal import override_centos\nfrom certbot_apache._internal.tests import util\n\n\ndef get_vh_truth(temp_dir, config_name):\n    \"\"\"Return the ground truth for the specified directory.\"\"\"\n    prefix = os.path.join(\n        temp_dir, config_name, \"httpd/conf.d\")\n\n    aug_pre = \"/files\" + prefix\n    vh_truth = [\n        obj.VirtualHost(\n            os.path.join(prefix, \"centos.example.com.conf\"),\n            os.path.join(aug_pre, \"centos.example.com.conf/VirtualHost\"),\n            {obj.Addr.fromstring(\"*:80\")},\n            False, True, \"centos.example.com\"),\n        obj.VirtualHost(\n            os.path.join(prefix, \"ssl.conf\"),\n            os.path.join(aug_pre, \"ssl.conf/VirtualHost\"),\n            {obj.Addr.fromstring(\"_default_:443\")},\n            True, True, None)\n    ]\n    return vh_truth\n\n\nclass FedoraRestartTest(util.ApacheTest):\n    \"\"\"Tests for Fedora specific self-signed certificate override\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        test_dir = \"centos7_apache/apache\"\n        config_root = \"centos7_apache/apache/httpd\"\n        vhost_root = \"centos7_apache/apache/httpd/conf.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n            os_info=\"fedora_old\")\n        self.vh_truth = get_vh_truth(\n            self.temp_dir, \"centos7_apache/apache\")\n\n    def _run_fedora_test(self):\n        assert isinstance(self.config, override_centos.CentOSConfigurator)\n        with mock.patch(\"certbot.util.get_os_info\") as mock_info:\n            mock_info.return_value = [\"fedora\", \"28\"]\n            self.config.config_test()\n\n    def test_non_fedora_error(self):\n        c_test = \"certbot_apache._internal.configurator.ApacheConfigurator.config_test\"\n        with mock.patch(c_test) as mock_test:\n            mock_test.side_effect = errors.MisconfigurationError\n            with mock.patch(\"certbot.util.get_os_info\") as mock_info:\n                mock_info.return_value = [\"not_fedora\"]\n                with pytest.raises(errors.MisconfigurationError):\n                    self.config.config_test()\n\n    def test_fedora_restart_error(self):\n        c_test = \"certbot_apache._internal.configurator.ApacheConfigurator.config_test\"\n        with mock.patch(c_test) as mock_test:\n            # First call raises error, second doesn't\n            mock_test.side_effect = [errors.MisconfigurationError, '']\n            with mock.patch(\"certbot.util.run_script\") as mock_run:\n                mock_run.side_effect = errors.SubprocessError\n                with pytest.raises(errors.MisconfigurationError):\n                    self._run_fedora_test()\n\n    def test_fedora_restart(self):\n        c_test = \"certbot_apache._internal.configurator.ApacheConfigurator.config_test\"\n        with mock.patch(c_test) as mock_test:\n            with mock.patch(\"certbot.util.run_script\") as mock_run:\n                # First call raises error, second doesn't\n                mock_test.side_effect = [errors.MisconfigurationError, '']\n                self._run_fedora_test()\n                assert mock_test.call_count == 2\n                assert mock_run.call_args[0][0] == \\\n                                ['systemctl', 'restart', 'httpd']\n\n\nclass UseCorrectApacheExecutableTest(util.ApacheTest):\n    \"\"\"Make sure the various CentOS/RHEL versions use the right httpd executable\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        test_dir = \"centos7_apache/apache\"\n        config_root = \"centos7_apache/apache/httpd\"\n        vhost_root = \"centos7_apache/apache/httpd/conf.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n\n    @mock.patch(\"certbot.util.get_os_info\")\n    def test_old_centos_rhel_and_fedora(self, mock_get_os_info):\n        for os_info in [(\"centos\", \"7\"), (\"rhel\", \"7\"), (\"fedora\", \"28\"), (\"scientific\", \"6\")]:\n            mock_get_os_info.return_value = os_info\n            config = util.get_apache_configurator(\n                self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n                os_info=\"centos\")\n            assert config.options.ctl == \"apachectl\"\n            assert config.options.bin == \"httpd\"\n            assert config.options.version_cmd == [\"apachectl\", \"-v\"]\n            assert config.options.restart_cmd == [\"apachectl\", \"graceful\"]\n            assert config.options.restart_cmd_alt == [\"apachectl\", \"restart\"]\n            assert config.options.conftest_cmd == [\"apachectl\", \"configtest\"]\n            assert config.options.get_defines_cmd == [\"apachectl\", \"-t\", \"-D\", \"DUMP_RUN_CFG\"]\n            assert config.options.get_includes_cmd == [\"apachectl\", \"-t\", \"-D\", \"DUMP_INCLUDES\"]\n            assert config.options.get_modules_cmd == [\"apachectl\", \"-t\", \"-D\", \"DUMP_MODULES\"]\n\n    @mock.patch(\"certbot.util.get_os_info\")\n    def test_new_rhel_derived(self, mock_get_os_info):\n        for os_info in [(\"centos\", \"9\"), (\"rhel\", \"9\"), (\"oracle\", \"9\")]:\n            mock_get_os_info.return_value = os_info\n            config = util.get_apache_configurator(\n                self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n                os_info=os_info[0])\n            assert config.options.ctl == \"apachectl\"\n            assert config.options.bin == \"httpd\"\n            assert config.options.version_cmd == [\"httpd\", \"-v\"]\n            assert config.options.restart_cmd == [\"apachectl\", \"graceful\"]\n            assert config.options.restart_cmd_alt == [\"apachectl\", \"restart\"]\n            assert config.options.conftest_cmd == [\"apachectl\", \"configtest\"]\n            assert config.options.get_defines_cmd == [\"httpd\", \"-t\", \"-D\", \"DUMP_RUN_CFG\"]\n            assert config.options.get_includes_cmd == [\"httpd\", \"-t\", \"-D\", \"DUMP_INCLUDES\"]\n            assert config.options.get_modules_cmd == [\"httpd\", \"-t\", \"-D\", \"DUMP_MODULES\"]\n\n\nclass MultipleVhostsTestCentOS(util.ApacheTest):\n    \"\"\"Multiple vhost tests for CentOS / RHEL family of distros\"\"\"\n\n    @mock.patch(\"certbot.util.get_os_info\")\n    def setUp(self, mock_get_os_info):  # pylint: disable=arguments-differ\n        test_dir = \"centos7_apache/apache\"\n        config_root = \"centos7_apache/apache/httpd\"\n        vhost_root = \"centos7_apache/apache/httpd/conf.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n        mock_get_os_info.return_value = (\"centos\", \"9\")\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n            os_info=\"centos\")\n        self.vh_truth = get_vh_truth(\n            self.temp_dir, \"centos7_apache/apache\")\n\n    def test_get_parser(self):\n        assert isinstance(self.config.parser, override_centos.CentOSParser)\n\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_opportunistic_httpd_runtime_parsing(self, mock_get):\n        define_val = (\n            'Define: TEST1\\n'\n            'Define: TEST2\\n'\n            'Define: DUMP_RUN_CFG\\n'\n        )\n        mod_val = (\n            'Loaded Modules:\\n'\n            ' mock_module (static)\\n'\n            ' another_module (static)\\n'\n        )\n        def mock_get_cfg(command):\n            \"\"\"Mock httpd process stdout\"\"\"\n            if command == ['httpd', '-t', '-D', 'DUMP_RUN_CFG']:\n                return define_val\n            elif command == ['httpd', '-t', '-D', 'DUMP_MODULES']:\n                return mod_val\n            return \"\"\n        mock_get.side_effect = mock_get_cfg\n        self.config.parser.modules = {}\n        self.config.parser.variables = {}\n\n        with mock.patch(\"certbot.util.get_os_info\") as mock_osi:\n            # Make sure we have the have the CentOS httpd constants\n            mock_osi.return_value = (\"centos\", \"9\")\n            self.config.parser.update_runtime_variables()\n\n        assert mock_get.call_count == 3\n        assert len(self.config.parser.modules) == 4\n        assert len(self.config.parser.variables) == 2\n        assert \"TEST2\" in self.config.parser.variables\n        assert \"mod_another.c\" in self.config.parser.modules\n\n    def test_get_virtual_hosts(self):\n        \"\"\"Make sure all vhosts are being properly found.\"\"\"\n        vhs = self.config.get_virtual_hosts()\n        assert len(vhs) == 2\n        found = 0\n\n        for vhost in vhs:\n            for centos_truth in self.vh_truth:\n                if vhost == centos_truth:\n                    found += 1\n                    break\n            else:\n                raise Exception(\"Missed: %s\" % vhost)  # pragma: no cover\n        assert found == 2\n\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_get_sysconfig_vars(self, mock_cfg):\n        \"\"\"Make sure we read the sysconfig OPTIONS variable correctly\"\"\"\n        # Return nothing for the process calls\n        mock_cfg.return_value = \"\"\n        self.config.parser.sysconfig_filep = filesystem.realpath(\n            os.path.join(self.config.parser.root, \"../sysconfig/httpd\"))\n        self.config.parser.variables = {}\n\n        with mock.patch(\"certbot.util.get_os_info\") as mock_osi:\n            # Make sure we have the have the CentOS httpd constants\n            mock_osi.return_value = (\"centos\", \"9\")\n            self.config.parser.update_runtime_variables()\n\n        assert \"mock_define\" in self.config.parser.variables\n        assert \"mock_define_too\" in self.config.parser.variables\n        assert \"mock_value\" in self.config.parser.variables\n        assert \"TRUE\" == self.config.parser.variables[\"mock_value\"]\n        assert \"MOCK_NOSEP\" in self.config.parser.variables\n        assert \"NOSEP_VAL\" == self.config.parser.variables[\"NOSEP_TWO\"]\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_alt_restart_works(self, mock_run_script):\n        mock_run_script.side_effect = [None, errors.SubprocessError, None]\n        self.config.restart()\n        assert mock_run_script.call_count == 3\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_alt_restart_errors(self, mock_run_script):\n        mock_run_script.side_effect = [None,\n                                       errors.SubprocessError,\n                                       errors.SubprocessError]\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.restart()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/complex_parsing_test.py",
    "content": "\"\"\"Tests for certbot_apache._internal.parser.\"\"\"\nimport sys\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_apache._internal.tests import util\n\n\nclass ComplexParserTest(util.ParserTest):\n    \"\"\"Apache Parser Test.\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp(\"complex_parsing\", \"complex_parsing\")\n\n        self.setup_variables()\n        # This needs to happen after due to setup_variables not being run\n        # until after\n        self.parser.parse_modules()  # pylint: disable=protected-access\n\n    def setup_variables(self):\n        \"\"\"Set up variables for parser.\"\"\"\n        self.parser.variables.update(\n            {\n                \"COMPLEX\": \"\",\n                \"tls_port\": \"1234\",\n                \"fnmatch_filename\": \"test_fnmatch.conf\",\n                \"tls_port_str\": \"1234\"\n            }\n        )\n\n    def test_filter_args_num(self):\n        \"\"\"Note: This may also fail do to Include conf-enabled/ syntax.\"\"\"\n        matches = self.parser.find_dir(\"TestArgsDirective\")\n\n        assert len(self.parser.filter_args_num(matches, 1)) == 3\n        assert len(self.parser.filter_args_num(matches, 2)) == 2\n        assert len(self.parser.filter_args_num(matches, 3)) == 1\n\n    def test_basic_variable_parsing(self):\n        matches = self.parser.find_dir(\"TestVariablePort\")\n\n        assert len(matches) == 1\n        assert self.parser.get_arg(matches[0]) == \"1234\"\n\n    def test_basic_variable_parsing_quotes(self):\n        matches = self.parser.find_dir(\"TestVariablePortStr\")\n\n        assert len(matches) == 1\n        assert self.parser.get_arg(matches[0]) == \"1234\"\n\n    def test_invalid_variable_parsing(self):\n        del self.parser.variables[\"tls_port\"]\n\n        matches = self.parser.find_dir(\"TestVariablePort\")\n        with pytest.raises(errors.PluginError):\n            self.parser.get_arg(matches[0])\n\n    def test_basic_ifdefine(self):\n        assert len(self.parser.find_dir(\"VAR_DIRECTIVE\")) == 2\n        assert len(self.parser.find_dir(\"INVALID_VAR_DIRECTIVE\")) == 0\n\n    def test_basic_ifmodule(self):\n        assert len(self.parser.find_dir(\"MOD_DIRECTIVE\")) == 2\n        assert len(self.parser.find_dir(\"INVALID_MOD_DIRECTIVE\")) == 0\n\n    def test_nested(self):\n        assert len(self.parser.find_dir(\"NESTED_DIRECTIVE\")) == 3\n        assert len(self.parser.find_dir(\"INVALID_NESTED_DIRECTIVE\")) == 0\n\n    def test_load_modules(self):\n        \"\"\"If only first is found, there is bad variable parsing.\"\"\"\n        assert \"status_module\" in self.parser.modules\n        assert \"mod_status.c\" in self.parser.modules\n\n        # This is in an IfDefine\n        assert \"ssl_module\" in self.parser.modules\n        assert \"mod_ssl.c\" in self.parser.modules\n\n    def verify_fnmatch(self, arg, hit=True):\n        \"\"\"Test if Include was correctly parsed.\"\"\"\n        from certbot_apache._internal import parser\n        self.parser.add_dir(parser.get_aug_path(self.parser.loc[\"default\"]),\n                            \"Include\", [arg])\n        if hit:\n            assert self.parser.find_dir(\"FNMATCH_DIRECTIVE\")\n        else:\n            assert not self.parser.find_dir(\"FNMATCH_DIRECTIVE\")\n\n    # NOTE: Only run one test per function otherwise you will have\n    # inf recursion\n    def test_include(self):\n        self.verify_fnmatch(\"test_fnmatch.?onf\")\n\n    def test_include_complex(self):\n        self.verify_fnmatch(\"../complex_parsing/[te][te]st_*.?onf\")\n\n    def test_include_fullpath(self):\n        self.verify_fnmatch(os.path.join(self.config_path,\n                                         \"test_fnmatch.conf\"))\n\n    def test_include_fullpath_trailing_slash(self):\n        self.verify_fnmatch(self.config_path + \"//\")\n\n    def test_include_single_quotes(self):\n        self.verify_fnmatch(\"'\" + self.config_path + \"'\")\n\n    def test_include_double_quotes(self):\n        self.verify_fnmatch('\"' + self.config_path + '\"')\n\n    def test_include_variable(self):\n        self.verify_fnmatch(\"../complex_parsing/${fnmatch_filename}\")\n\n    def test_include_missing(self):\n        # This should miss\n        self.verify_fnmatch(\"test_*.onf\", False)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/configurator_reverter_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.configurator implementations of reverter\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot_apache._internal.tests import util\n\n\nclass ConfiguratorReverterTest(util.ApacheTest):\n    \"\"\"Test for ApacheConfigurator reverter methods\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir)\n\n        self.vh_truth = util.get_vh_truth(self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def test_bad_save_checkpoint(self):\n        self.config.reverter.add_to_checkpoint = mock.Mock(side_effect=errors.ReverterError)\n        self.config.parser.add_dir(self.vh_truth[0].path, \"Test\", \"bad_save_ckpt\")\n        with pytest.raises(errors.PluginError):\n            self.config.save()\n\n    def test_bad_save_finalize_checkpoint(self):\n        self.config.reverter.finalize_checkpoint = mock.Mock(side_effect=errors.ReverterError)\n        self.config.parser.add_dir(self.vh_truth[0].path, \"Test\", \"bad_save_ckpt\")\n        with pytest.raises(errors.PluginError):\n            self.config.save(\"Title\")\n\n    def test_finalize_save(self):\n        mock_finalize = mock.Mock()\n        self.config.reverter = mock_finalize\n        self.config.save(\"Example Title\")\n\n        assert mock_finalize.is_called\n\n    def test_revert_challenge_config(self):\n        mock_load = mock.Mock()\n        self.config.parser.aug.load = mock_load\n\n        self.config.revert_challenge_config()\n        assert mock_load.call_count == 1\n\n    def test_revert_challenge_config_error(self):\n        self.config.reverter.revert_temporary_config = mock.Mock(\n            side_effect=errors.ReverterError)\n\n        with pytest.raises(errors.PluginError):\n            self.config.revert_challenge_config()\n\n    def test_rollback_checkpoints(self):\n        mock_load = mock.Mock()\n        self.config.parser.aug.load = mock_load\n\n        self.config.rollback_checkpoints()\n        assert mock_load.call_count == 1\n\n    def test_rollback_error(self):\n        self.config.reverter.rollback_checkpoints = mock.Mock(side_effect=errors.ReverterError)\n        with pytest.raises(errors.PluginError):\n            self.config.rollback_checkpoints()\n\n    def test_recovery_routine_reload(self):\n        mock_load = mock.Mock()\n        self.config.parser.aug.load = mock_load\n        self.config.recovery_routine()\n        assert mock_load.call_count == 1\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/configurator_test.py",
    "content": "# pylint: disable=too-many-lines\n\"\"\"Test for certbot_apache._internal.configurator.\"\"\"\nimport copy\nimport shutil\nimport socket\nimport sys\nimport tempfile\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as certbot_util\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import constants\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal import parser\nfrom certbot_apache._internal.tests import util\n\n\nclass MultipleVhostsTest(util.ApacheTest):\n    \"\"\"Test two standard well-configured HTTP vhosts.\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir)\n        self.config = self.mock_deploy_cert(self.config)\n        self.vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def mock_deploy_cert(self, config):\n        \"\"\"A test for a mock deploy cert\"\"\"\n        config.real_deploy_cert = self.config.deploy_cert\n\n        def mocked_deploy_cert(*args, **kwargs):\n            \"\"\"a helper to mock a deployed cert\"\"\"\n            g_mod = \"certbot_apache._internal.configurator.ApacheConfigurator.enable_mod\"\n            with mock.patch(g_mod):\n                config.real_deploy_cert(*args, **kwargs)\n        self.config.deploy_cert = mocked_deploy_cert\n        return self.config\n\n    @mock.patch(\"certbot_apache._internal.configurator.path_surgery\")\n    def test_prepare_no_install(self, mock_surgery):\n        silly_path = {\"PATH\": \"/tmp/nothingness2342\"}\n        mock_surgery.return_value = False\n        with mock.patch.dict('os.environ', silly_path):\n            with pytest.raises(errors.NoInstallationError):\n                self.config.prepare()\n            assert mock_surgery.call_count == 1\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser\")\n    @mock.patch(\"certbot_apache._internal.configurator.util.exe_exists\")\n    def test_prepare_version(self, mock_exe_exists, _):\n        mock_exe_exists.return_value = True\n        self.config.version = None\n        self.config.config_test = mock.Mock()\n        self.config.get_version = mock.Mock(return_value=(1, 1))\n\n        with pytest.raises(errors.NotSupportedError):\n            self.config.prepare()\n\n    def test_prepare_locked(self):\n        # It is important to test that server_root is locked during the call to\n        # prepare (as opposed to somewhere else during plugin execution) to\n        # ensure that this lock will be acquired after the Certbot package\n        # acquires all of its locks. (Tests that Certbot calls prepare after\n        # acquiring its locks are part of the Certbot package's tests.) Not\n        # doing this could result in deadlock from two versions of Certbot that\n        # acquire its locks in a different order.\n        server_root = self.config.conf(\"server-root\")\n        self.config.config_test = mock.Mock()\n        os.remove(os.path.join(server_root, \".certbot.lock\"))\n        certbot_util.lock_and_call(self._test_prepare_locked, server_root)\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser\")\n    @mock.patch(\"certbot_apache._internal.configurator.util.exe_exists\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.get_parsernode_root\")\n    def _test_prepare_locked(self, _node, _exists, _parser):\n        try:\n            self.config.prepare()\n        except errors.PluginError as err:\n            err_msg = str(err)\n            assert \"lock\" in err_msg\n            assert self.config.conf(\"server-root\") in err_msg\n        else:  # pragma: no cover\n            self.fail(\"Exception wasn't raised!\")\n\n    def test_add_parser_arguments(self):  # pylint: disable=no-self-use\n        from certbot_apache._internal.configurator import ApacheConfigurator\n\n        # Weak test..\n        ApacheConfigurator.add_parser_arguments(mock.MagicMock())\n\n    def test_docs_parser_arguments(self):\n        os.environ[\"CERTBOT_DOCS\"] = \"1\"\n        from certbot_apache._internal.configurator import ApacheConfigurator\n        mock_add = mock.MagicMock()\n        ApacheConfigurator.add_parser_arguments(mock_add)\n        parserargs = [\"server_root\", \"enmod\", \"dismod\", \"le_vhost_ext\",\n                      \"vhost_root\", \"logs_root\", \"challenge_location\",\n                      \"handle_modules\", \"handle_sites\", \"ctl\"]\n        exp = {}\n\n        for k in ApacheConfigurator.OS_DEFAULTS.__dict__.keys():\n            if k in parserargs:\n                exp[k.replace(\"_\", \"-\")] = getattr(ApacheConfigurator.OS_DEFAULTS, k)\n        # Special cases\n        exp[\"vhost-root\"] = None\n\n        found = set()\n        for call in mock_add.call_args_list:\n            found.add(call[0][0])\n\n        # Make sure that all (and only) the expected values exist\n        assert len(mock_add.call_args_list) == len(found)\n        for e in exp:\n            with self.subTest(e=e):\n                assert e in found\n\n        del os.environ[\"CERTBOT_DOCS\"]\n\n    def test_add_parser_arguments_all_configurators(self):  # pylint: disable=no-self-use\n        from certbot_apache._internal.entrypoint import OVERRIDE_CLASSES\n        for cls in OVERRIDE_CLASSES.values():\n            cls.add_parser_arguments(mock.MagicMock())\n\n    def test_all_configurators_defaults_defined(self):\n        from certbot_apache._internal.configurator import ApacheConfigurator\n        from certbot_apache._internal.entrypoint import OVERRIDE_CLASSES\n        parameters = set(ApacheConfigurator.OS_DEFAULTS.__dict__.keys())\n        for cls in OVERRIDE_CLASSES.values():\n            assert parameters.issubset(set(cls.OS_DEFAULTS.__dict__.keys())) is True\n\n    def test_constant(self):\n        assert \"debian_apache_2_4/multiple_vhosts/apache\" in self.config.options.server_root\n\n    @certbot_util.patch_display_util()\n    def test_get_all_names(self, mock_getutility):\n        mock_utility = mock_getutility()\n        mock_utility.notification = mock.MagicMock(return_value=True)\n        names = self.config.get_all_names()\n        assert names == {\"certbot.demo\", \"ocspvhost.com\", \"encryption-example.demo\",\n             \"nonsym.link\", \"vhost.in.rootconf\", \"www.certbot.demo\",\n             \"duplicate.example.com\"}\n\n    @certbot_util.patch_display_util()\n    @mock.patch(\"certbot_apache._internal.configurator.socket.gethostbyaddr\")\n    def test_get_all_names_addrs(self, mock_gethost, mock_getutility):\n        mock_gethost.side_effect = [(\"google.com\", \"\", \"\"), socket.error]\n        mock_utility = mock_getutility()\n        mock_utility.notification.return_value = True\n        vhost = obj.VirtualHost(\n            \"fp\", \"ap\",\n            {obj.Addr((\"8.8.8.8\", \"443\")),\n                 obj.Addr((\"zombo.com\",)),\n                 obj.Addr((\"192.168.1.2\"))},\n            True, False)\n\n        self.config.vhosts.append(vhost)\n\n        names = self.config.get_all_names()\n        assert len(names) == 9\n        assert \"zombo.com\" in names\n        assert \"google.com\" in names\n        assert \"certbot.demo\" in names\n\n    def test_get_bad_path(self):\n        assert apache_util.get_file_path(None) is None\n        assert apache_util.get_file_path(\"nonexistent\") is None\n        assert self.config._create_vhost(\"nonexistent\") is None # pylint: disable=protected-access\n\n    def test_get_aug_internal_path(self):\n        from certbot_apache._internal.apache_util import get_internal_aug_path\n        internal_paths = [\n            \"Virtualhost\", \"IfModule/VirtualHost\", \"VirtualHost\", \"VirtualHost\",\n            \"Macro/VirtualHost\", \"IfModule/VirtualHost\", \"VirtualHost\",\n            \"IfModule/VirtualHost\"]\n\n        for i, internal_path in enumerate(internal_paths):\n            assert get_internal_aug_path(self.vh_truth[i].path) == internal_path\n\n    def test_bad_servername_alias(self):\n        ssl_vh1 = obj.VirtualHost(\n            \"fp1\", \"ap1\", {obj.Addr((\"*\", \"443\"))},\n            True, False)\n        # pylint: disable=protected-access\n        self.config._add_servernames(ssl_vh1)\n        assert self.config._add_servername_alias(\"oy_vey\", ssl_vh1) is None\n\n    def test_add_servernames_alias(self):\n        self.config.parser.add_dir(\n            self.vh_truth[2].path, \"ServerAlias\", [\"*.le.co\"])\n        # pylint: disable=protected-access\n        self.config._add_servernames(self.vh_truth[2])\n        assert self.vh_truth[2].get_names() == {\"*.le.co\", \"ip-172-30-0-17\"}\n\n    def test_get_virtual_hosts(self):\n        \"\"\"Make sure all vhosts are being properly found.\"\"\"\n        vhs = self.config.get_virtual_hosts()\n        assert len(vhs) == 12\n        found = 0\n\n        for vhost in vhs:\n            for truth in self.vh_truth:\n                if vhost == truth:\n                    found += 1\n                    break\n            else:\n                raise Exception(\"Missed: %s\" % vhost)  # pragma: no cover\n\n        assert found == 12\n\n        # Handle case of non-debian layout get_virtual_hosts\n        with mock.patch(\n                \"certbot_apache._internal.configurator.ApacheConfigurator.conf\"\n        ) as mock_conf:\n            mock_conf.return_value = False\n            vhs = self.config.get_virtual_hosts()\n            assert len(vhs) == 12\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    def test_choose_vhost_none_avail(self, mock_select):\n        mock_select.return_value = None\n        with pytest.raises(errors.PluginError):\n            self.config.choose_vhost(\"none.com\")\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    def test_choose_vhost_select_vhost_ssl(self, mock_select):\n        mock_select.return_value = self.vh_truth[1]\n        assert self.vh_truth[1] == self.config.choose_vhost(\"none.com\")\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    @mock.patch(\"certbot_apache._internal.obj.VirtualHost.conflicts\")\n    def test_choose_vhost_select_vhost_non_ssl(self, mock_conf, mock_select):\n        mock_select.return_value = self.vh_truth[0]\n        mock_conf.return_value = False\n        chosen_vhost = self.config.choose_vhost(\"none.com\")\n        self.vh_truth[0].aliases.add(\"none.com\")\n        assert self.vh_truth[0].get_names() == chosen_vhost.get_names()\n\n        # Make sure we go from HTTP -> HTTPS\n        assert self.vh_truth[0].ssl is False\n        assert chosen_vhost.ssl is True\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._find_best_vhost\")\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.add_dir\")\n    def test_choose_vhost_and_servername_addition(self, mock_add, mock_find):\n        ret_vh = self.vh_truth[8]\n        ret_vh.enabled = False\n        mock_find.return_value = self.vh_truth[8]\n        self.config.choose_vhost(\"whatever.com\")\n        assert mock_add.called is True\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    def test_choose_vhost_select_vhost_with_temp(self, mock_select):\n        mock_select.return_value = self.vh_truth[0]\n        chosen_vhost = self.config.choose_vhost(\"none.com\", create_if_no_ssl=False)\n        assert self.vh_truth[0] == chosen_vhost\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    def test_choose_vhost_select_vhost_conflicting_non_ssl(self, mock_select):\n        mock_select.return_value = self.vh_truth[3]\n        conflicting_vhost = obj.VirtualHost(\n            \"path\", \"aug_path\", {obj.Addr.fromstring(\"*:443\")},\n            True, True)\n        self.config.vhosts.append(conflicting_vhost)\n\n        with pytest.raises(errors.PluginError):\n            self.config.choose_vhost(\"none.com\")\n\n    def test_find_best_http_vhost_default(self):\n        vh = obj.VirtualHost(\n            \"fp\", \"ap\", {obj.Addr.fromstring(\"_default_:80\")}, False, True)\n        self.config.vhosts = [vh]\n        assert self.config.find_best_http_vhost(\"foo.bar\", False) == vh\n\n    def test_find_best_http_vhost_port(self):\n        port = \"8080\"\n        vh = obj.VirtualHost(\n            \"fp\", \"ap\", {obj.Addr.fromstring(\"*:\" + port)},\n            False, True, \"encryption-example.demo\")\n        self.config.vhosts.append(vh)\n        assert self.config.find_best_http_vhost(\"foo.bar\", False, port) == vh\n\n    def test_findbest_continues_on_short_domain(self):\n        # pylint: disable=protected-access\n        assert self.config._find_best_vhost(\"purple.com\") is None\n\n    def test_findbest_continues_on_long_domain(self):\n        # pylint: disable=protected-access\n        assert self.config._find_best_vhost(\"green.red.purple.com\") is None\n\n    def test_find_best_vhost(self):\n        # pylint: disable=protected-access\n        assert self.vh_truth[3] == self.config._find_best_vhost(\"certbot.demo\")\n        assert self.vh_truth[0] == self.config._find_best_vhost(\"encryption-example.demo\")\n        assert self.config._find_best_vhost(\"does-not-exist.com\") is None\n\n    def test_find_best_vhost_variety(self):\n        # pylint: disable=protected-access\n        ssl_vh = obj.VirtualHost(\n            \"fp\", \"ap\", {obj.Addr((\"*\", \"443\")),\n                             obj.Addr((\"zombo.com\",))},\n            True, False)\n        self.config.vhosts.append(ssl_vh)\n        assert self.config._find_best_vhost(\"zombo.com\") == ssl_vh\n\n    def test_find_best_vhost_default(self):\n        # pylint: disable=protected-access\n        # Assume only the two default vhosts.\n        self.config.vhosts = [\n            vh for vh in self.config.vhosts\n            if vh.name not in [\"certbot.demo\", \"nonsym.link\",\n                \"encryption-example.demo\", \"duplicate.example.com\",\n                \"ocspvhost.com\", \"vhost.in.rootconf\"]\n            and \"*.blue.purple.com\" not in vh.aliases\n        ]\n        assert self.config._find_best_vhost(\"encryption-example.demo\") == \\\n            self.vh_truth[2]\n\n    def test_non_default_vhosts(self):\n        # pylint: disable=protected-access\n        vhosts = self.config._non_default_vhosts(self.config.vhosts)\n        assert len(vhosts) == 10\n\n    @mock.patch('certbot_apache._internal.configurator.display_util.notify')\n    def test_deploy_cert_enable_new_vhost(self, unused_mock_notify):\n        # Create\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n\n        assert ssl_vhost.enabled is False\n        self.config.deploy_cert(\n            \"encryption-example.demo\", \"example/cert.pem\", \"example/key.pem\",\n            \"example/cert_chain.pem\", \"example/fullchain.pem\")\n        assert ssl_vhost.enabled is True\n\n    def test_no_duplicate_include(self):\n        def mock_find_dir(directive, argument, _):\n            \"\"\"Mock method for parser.find_dir\"\"\"\n            if directive == \"Include\" and argument.endswith(\"options-ssl-apache.conf\"):\n                return [\"/path/to/whatever\"]\n            return None  # pragma: no cover\n\n        mock_add = mock.MagicMock()\n        self.config.parser.add_dir = mock_add\n        self.config._add_dummy_ssl_directives(self.vh_truth[0])  # pylint: disable=protected-access\n        tried_to_add = False\n        for a in mock_add.call_args_list:\n            if a[0][1] == \"Include\" and a[0][2] == self.config.mod_ssl_conf:\n                tried_to_add = True\n        # Include should be added, find_dir is not patched, and returns falsy\n        assert tried_to_add is True\n\n        self.config.parser.find_dir = mock_find_dir\n        mock_add.reset_mock()\n        self.config._add_dummy_ssl_directives(self.vh_truth[0])  # pylint: disable=protected-access\n        for a in mock_add.call_args_list:\n            if a[0][1] == \"Include\" and a[0][2] == self.config.mod_ssl_conf:\n                self.fail(\"Include shouldn't be added, as patched find_dir 'finds' existing one\") \\\n                    # pragma: no cover\n\n    @mock.patch('certbot_apache._internal.configurator.display_util.notify')\n    def test_deploy_cert(self, unused_mock_notify):\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n        # Patch _add_dummy_ssl_directives to make sure we write them correctly\n        # pylint: disable=protected-access\n        orig_add_dummy = self.config._add_dummy_ssl_directives\n        def mock_add_dummy_ssl(vhostpath):\n            \"\"\"Mock method for _add_dummy_ssl_directives\"\"\"\n            def find_args(path, directive):\n                \"\"\"Return list of arguments in requested directive at path\"\"\"\n                f_args = []\n                dirs = self.config.parser.find_dir(directive, None,\n                                                   path)\n                for d in dirs:\n                    f_args.append(self.config.parser.get_arg(d))\n                return f_args\n            # Verify that the dummy directives do not exist\n            assert \"insert_cert_file_path\" not in find_args(vhostpath, \"SSLCertificateFile\")\n            assert \"insert_key_file_path\" not in find_args(vhostpath, \"SSLCertificateKeyFile\")\n            orig_add_dummy(vhostpath)\n            # Verify that the dummy directives exist\n            assert \"insert_cert_file_path\" in find_args(vhostpath, \"SSLCertificateFile\")\n            assert \"insert_key_file_path\" in find_args(vhostpath, \"SSLCertificateKeyFile\")\n        # pylint: disable=protected-access\n        self.config._add_dummy_ssl_directives = mock_add_dummy_ssl\n\n        # Get the default 443 vhost\n        self.config.assoc[\"random.demo\"] = self.vh_truth[1]\n        self.config.deploy_cert(\n            \"random.demo\",\n            \"example/cert.pem\", \"example/key.pem\", \"example/cert_chain.pem\")\n        self.config.save()\n\n        # Verify ssl_module was enabled.\n        assert self.vh_truth[1].enabled is True\n        assert \"ssl_module\" in self.config.parser.modules\n\n        loc_cert = self.config.parser.find_dir(\n            \"sslcertificatefile\", \"example/cert.pem\", self.vh_truth[1].path)\n        loc_key = self.config.parser.find_dir(\n            \"sslcertificateKeyfile\", \"example/key.pem\", self.vh_truth[1].path)\n        loc_chain = self.config.parser.find_dir(\n            \"SSLCertificateChainFile\", \"example/cert_chain.pem\",\n            self.vh_truth[1].path)\n\n        # Verify one directive was found in the correct file\n        assert len(loc_cert) == 1\n        assert apache_util.get_file_path(loc_cert[0]) == \\\n            self.vh_truth[1].filep\n\n        assert len(loc_key) == 1\n        assert apache_util.get_file_path(loc_key[0]) == \\\n            self.vh_truth[1].filep\n\n        assert len(loc_chain) == 1\n        assert apache_util.get_file_path(loc_chain[0]) == \\\n            self.vh_truth[1].filep\n\n        # One more time for chain directive setting\n        self.config.deploy_cert(\n            \"random.demo\",\n            \"two/cert.pem\", \"two/key.pem\", \"two/cert_chain.pem\")\n        assert self.config.parser.find_dir(\n            \"SSLCertificateChainFile\", \"two/cert_chain.pem\",\n            self.vh_truth[1].path)\n\n    def test_add_listen_80(self):\n        mock_find = mock.Mock()\n        mock_add_dir = mock.Mock()\n        mock_find.return_value = []\n        self.config.parser.find_dir = mock_find\n        self.config.parser.add_dir = mock_add_dir\n        self.config.ensure_listen(\"80\")\n        assert mock_add_dir.called is True\n        assert mock_find.called is True\n        assert mock_add_dir.call_args[0][1] == \"Listen\"\n        assert mock_add_dir.call_args[0][2] == \"80\"\n\n    def test_add_listen_80_named(self):\n        mock_find = mock.Mock()\n        mock_find.return_value = [\"test1\", \"test2\", \"test3\"]\n        mock_get = mock.Mock()\n        mock_get.side_effect = [\"1.2.3.4:80\", \"[::1]:80\", \"1.1.1.1:443\"]\n        mock_add_dir = mock.Mock()\n\n        self.config.parser.find_dir = mock_find\n        self.config.parser.get_arg = mock_get\n        self.config.parser.add_dir = mock_add_dir\n\n        self.config.ensure_listen(\"80\")\n        assert mock_add_dir.call_count == 0\n\n        # Reset return lists and inputs\n        mock_add_dir.reset_mock()\n        mock_get.side_effect = [\"1.2.3.4:80\", \"[::1]:80\", \"1.1.1.1:443\"]\n\n        # Test\n        self.config.ensure_listen(\"8080\")\n        assert mock_add_dir.call_count == 3\n        assert mock_add_dir.called is True\n        assert mock_add_dir.call_args[0][1] == \"Listen\"\n        call_found = False\n        for mock_call in mock_add_dir.mock_calls:\n            if mock_call[1][2] == ['1.2.3.4:8080']:\n                call_found = True\n        assert call_found is True\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.reset_modules\")\n    def test_prepare_server_https(self, mock_reset):\n        mock_enable = mock.Mock()\n        self.config.enable_mod = mock_enable\n\n        mock_find = mock.Mock()\n        mock_add_dir = mock.Mock()\n        mock_find.return_value = []\n\n        # This will test the Add listen\n        self.config.parser.find_dir = mock_find\n        self.config.parser.add_dir_to_ifmodssl = mock_add_dir\n        self.config.prepare_server_https(\"443\")\n        # Changing the order these modules are enabled breaks the reverter\n        assert mock_enable.call_args_list[0][0][0] == \"socache_shmcb\"\n        assert mock_enable.call_args[0][0] == \"ssl\"\n        assert mock_enable.call_args[1] == {\"temp\": False}\n\n        self.config.prepare_server_https(\"8080\", temp=True)\n        # Changing the order these modules are enabled breaks the reverter\n        assert mock_enable.call_args_list[2][0][0] == \"socache_shmcb\"\n        assert mock_enable.call_args[0][0] == \"ssl\"\n        # Enable mod is temporary\n        assert mock_enable.call_args[1] == {\"temp\": True}\n\n        assert mock_add_dir.call_count == 2\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.reset_modules\")\n    def test_prepare_server_https_named_listen(self, mock_reset):\n        mock_find = mock.Mock()\n        mock_find.return_value = [\"test1\", \"test2\", \"test3\"]\n        mock_get = mock.Mock()\n        mock_get.side_effect = [\"1.2.3.4:80\", \"[::1]:80\", \"1.1.1.1:443\"]\n        mock_add_dir = mock.Mock()\n        mock_enable = mock.Mock()\n\n        self.config.parser.find_dir = mock_find\n        self.config.parser.get_arg = mock_get\n        self.config.parser.add_dir_to_ifmodssl = mock_add_dir\n        self.config.enable_mod = mock_enable\n\n        # Test Listen statements with specific ip listeed\n        self.config.prepare_server_https(\"443\")\n        # Should be 0 as one interface already listens to 443\n        assert mock_add_dir.call_count == 0\n\n        # Reset return lists and inputs\n        mock_add_dir.reset_mock()\n        mock_get.side_effect = [\"1.2.3.4:80\", \"[::1]:80\", \"1.1.1.1:443\"]\n\n        # Test\n        self.config.prepare_server_https(\"8080\", temp=True)\n        assert mock_add_dir.call_count == 3\n        call_args_list = [mock_add_dir.call_args_list[i][0][2] for i in range(3)]\n        assert sorted(call_args_list) == \\\n            sorted([[\"1.2.3.4:8080\", \"https\"],\n                    [\"[::1]:8080\", \"https\"],\n                    [\"1.1.1.1:8080\", \"https\"]])\n\n        # mock_get.side_effect = [\"1.2.3.4:80\", \"[::1]:80\"]\n        # mock_find.return_value = [\"test1\", \"test2\", \"test3\"]\n        # self.config.parser.get_arg = mock_get\n        # self.config.prepare_server_https(\"8080\", temp=True)\n        # self.assertEqual(self.listens, 0)\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.reset_modules\")\n    def test_prepare_server_https_needed_listen(self, mock_reset):\n        mock_find = mock.Mock()\n        mock_find.return_value = [\"test1\", \"test2\"]\n        mock_get = mock.Mock()\n        mock_get.side_effect = [\"1.2.3.4:8080\", \"80\"]\n        mock_add_dir = mock.Mock()\n        mock_enable = mock.Mock()\n\n        self.config.parser.find_dir = mock_find\n        self.config.parser.get_arg = mock_get\n        self.config.parser.add_dir_to_ifmodssl = mock_add_dir\n        self.config.enable_mod = mock_enable\n\n        self.config.prepare_server_https(\"443\")\n        assert mock_add_dir.call_count == 1\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.reset_modules\")\n    def test_prepare_server_https_mixed_listen(self, mock_reset):\n        mock_find = mock.Mock()\n        mock_find.return_value = [\"test1\", \"test2\"]\n        mock_get = mock.Mock()\n        mock_get.side_effect = [\"1.2.3.4:8080\", \"443\"]\n        mock_add_dir = mock.Mock()\n        mock_enable = mock.Mock()\n\n        self.config.parser.find_dir = mock_find\n        self.config.parser.get_arg = mock_get\n        self.config.parser.add_dir_to_ifmodssl = mock_add_dir\n        self.config.enable_mod = mock_enable\n\n        # Test Listen statements with specific ip listeed\n        self.config.prepare_server_https(\"443\")\n        # Should only be 2 here, as the third interface\n        # already listens to the correct port\n        assert mock_add_dir.call_count == 0\n\n    def test_make_vhost_ssl_with_mock_span(self):\n        # span excludes the closing </VirtualHost> tag in older versions\n        # of Augeas\n        return_value = [self.vh_truth[0].filep, 1, 12, 0, 0, 0, 1142]\n        with mock.patch.object(self.config.parser.aug, 'span') as mock_span:\n            mock_span.return_value = return_value\n            self.test_make_vhost_ssl()\n\n    def test_make_vhost_ssl_with_mock_span2(self):\n        # span includes the closing </VirtualHost> tag in newer versions\n        # of Augeas\n        return_value = [self.vh_truth[0].filep, 1, 12, 0, 0, 0, 1157]\n        with mock.patch.object(self.config.parser.aug, 'span') as mock_span:\n            mock_span.return_value = return_value\n            self.test_make_vhost_ssl()\n\n    def test_make_vhost_ssl_nonsymlink(self):\n        ssl_vhost_slink = self.config.make_vhost_ssl(self.vh_truth[8])\n        assert ssl_vhost_slink.ssl is True\n        assert ssl_vhost_slink.enabled is True\n        assert ssl_vhost_slink.name == \"nonsym.link\"\n\n    def test_make_vhost_ssl_nonexistent_vhost_path(self):\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])\n        assert os.path.dirname(ssl_vhost.filep) == \\\n                         os.path.dirname(filesystem.realpath(self.vh_truth[1].filep))\n\n    def test_make_vhost_ssl(self):\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])\n\n        assert ssl_vhost.filep == \\\n            os.path.join(self.config_path, \"sites-available\",\n                         \"encryption-example-le-ssl.conf\")\n\n        assert ssl_vhost.path == \\\n                         \"/files\" + ssl_vhost.filep + \"/IfModule/Virtualhost\"\n        assert len(ssl_vhost.addrs) == 1\n        assert {obj.Addr.fromstring(\"*:443\")} == ssl_vhost.addrs\n        assert ssl_vhost.name == \"encryption-example.demo\"\n        assert ssl_vhost.ssl is True\n        assert ssl_vhost.enabled is False\n\n        assert len(self.config.vhosts) == 13\n\n    def test_clean_vhost_ssl(self):\n        # pylint: disable=protected-access\n        for directive in [\"SSLCertificateFile\", \"SSLCertificateKeyFile\",\n                          \"SSLCertificateChainFile\", \"SSLCACertificatePath\"]:\n            for _ in range(10):\n                self.config.parser.add_dir(self.vh_truth[1].path,\n                                           directive, [\"bogus\"])\n        self.config.save()\n\n        self.config._clean_vhost(self.vh_truth[1])\n        self.config.save()\n\n        loc_cert = self.config.parser.find_dir(\n            'SSLCertificateFile', None, self.vh_truth[1].path, False)\n        loc_key = self.config.parser.find_dir(\n            'SSLCertificateKeyFile', None, self.vh_truth[1].path, False)\n        loc_chain = self.config.parser.find_dir(\n            'SSLCertificateChainFile', None, self.vh_truth[1].path, False)\n        loc_cacert = self.config.parser.find_dir(\n            'SSLCACertificatePath', None, self.vh_truth[1].path, False)\n\n        assert len(loc_cert) == 1\n        assert len(loc_key) == 1\n\n        assert len(loc_chain) == 0\n\n        assert len(loc_cacert) == 10\n\n    def test_deduplicate_directives(self):\n        # pylint: disable=protected-access\n        DIRECTIVE = \"Foo\"\n        for _ in range(10):\n            self.config.parser.add_dir(self.vh_truth[1].path,\n                                       DIRECTIVE, [\"bar\"])\n        self.config.save()\n\n        self.config._deduplicate_directives(self.vh_truth[1].path, [DIRECTIVE])\n        self.config.save()\n\n        assert len(self.config.parser.find_dir(\n                DIRECTIVE, None, self.vh_truth[1].path, False)) == 1\n\n    def test_remove_directives(self):\n        # pylint: disable=protected-access\n        DIRECTIVES = [\"Foo\", \"Bar\"]\n        for directive in DIRECTIVES:\n            for _ in range(10):\n                self.config.parser.add_dir(self.vh_truth[2].path,\n                                           directive, [\"baz\"])\n        self.config.save()\n\n        self.config._remove_directives(self.vh_truth[2].path, DIRECTIVES)\n        self.config.save()\n\n        for directive in DIRECTIVES:\n            assert len(self.config.parser.find_dir(\n                    directive, None, self.vh_truth[2].path, False)) == 0\n\n    def test_make_vhost_ssl_bad_write(self):\n        mock_open = mock.mock_open()\n        # This calls open\n        self.config.reverter.register_file_creation = mock.Mock()\n        mock_open.side_effect = IOError\n        with mock.patch(\"builtins.open\", mock_open):\n            with pytest.raises(errors.PluginError):\n                self.config.make_vhost_ssl(self.vh_truth[0])\n\n    def test_get_ssl_vhost_path(self):\n        # pylint: disable=protected-access\n        assert self.config._get_ssl_vhost_path(\"example_path\").endswith(\".conf\") is True\n\n    @mock.patch(\"certbot_apache._internal.configurator.http_01.ApacheHttp01.perform\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    def test_perform(self, mock_restart, mock_http_perform):\n        # Only tests functionality specific to configurator.perform\n        # Note: As more challenges are offered this will have to be expanded\n        account_key, achalls = self.get_key_and_achalls()\n\n        expected = [achall.response(account_key) for achall in achalls]\n        mock_http_perform.return_value = expected\n\n        responses = self.config.perform(achalls)\n\n        assert mock_http_perform.call_count == 1\n        assert responses == expected\n\n        assert mock_restart.call_count == 1\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_cleanup(self, mock_cfg, mock_restart):\n        mock_cfg.return_value = \"\"\n        _, achalls = self.get_key_and_achalls()\n\n        for achall in achalls:\n            self.config._chall_out.add(achall)  # pylint: disable=protected-access\n\n        for i, achall in enumerate(achalls):\n            self.config.cleanup([achall])\n            if i == len(achalls) - 1:\n                assert mock_restart.called is True\n            else:\n                assert mock_restart.called is False\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.restart\")\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_cleanup_no_errors(self, mock_cfg, mock_restart):\n        mock_cfg.return_value = \"\"\n        _, achalls = self.get_key_and_achalls()\n        self.config.http_doer = mock.MagicMock()\n\n        for achall in achalls:\n            self.config._chall_out.add(achall)  # pylint: disable=protected-access\n\n        self.config.cleanup([achalls[-1]])\n        assert mock_restart.called is False\n\n        self.config.cleanup(achalls)\n        assert mock_restart.called is True\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_get_version(self, mock_script):\n        mock_script.return_value = (\n            \"Server Version: Apache/2.4.2 (Debian)\", \"\")\n        assert self.config.get_version() == (2, 4, 2)\n\n        mock_script.return_value = (\n            \"Server Version: Apache/2 (Linux)\", \"\")\n        assert self.config.get_version() == (2,)\n\n        mock_script.return_value = (\n            \"Server Version: Apache (Debian)\", \"\")\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n        mock_script.return_value = (\n            \"Server Version: Apache/2.3{0} Apache/2.4.7\".format(\n                os.linesep), \"\")\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n        mock_script.side_effect = errors.SubprocessError(\"Can't find program\")\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_restart(self, _):\n        self.config.restart()\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_restart_bad_process(self, mock_run_script):\n        mock_run_script.side_effect = [None, errors.SubprocessError]\n\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.restart()\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_config_test(self, _):\n        self.config.config_test()\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_config_test_bad_process(self, mock_run_script):\n        mock_run_script.side_effect = errors.SubprocessError\n\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.config_test()\n\n    def test_more_info(self):\n        assert self.config.more_info()\n\n    def test_get_chall_pref(self):\n        assert isinstance(self.config.get_chall_pref(\"\"), list)\n\n    def test_install_ssl_options_conf(self):\n        path = os.path.join(self.work_dir, \"test_it\")\n        other_path = os.path.join(self.work_dir, \"other_test_it\")\n        self.config.install_ssl_options_conf(path, other_path)\n        assert os.path.isfile(path) is True\n        assert os.path.isfile(other_path) is True\n\n    # TEST ENHANCEMENTS\n    def test_supported_enhancements(self):\n        assert isinstance(self.config.supported_enhancements(), list)\n\n    def test_find_http_vhost_without_ancestor(self):\n        # pylint: disable=protected-access\n        vhost = self.vh_truth[0]\n        vhost.ssl = True\n        vhost.ancestor = None\n        res = self.config._get_http_vhost(vhost)\n        assert self.vh_truth[0].name == res.name\n        assert self.vh_truth[0].aliases == res.aliases\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._get_http_vhost\")\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_enhance_unknown_vhost(self, mock_exe, mock_sel_vhost, mock_get):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        mock_exe.return_value = True\n        ssl_vh1 = obj.VirtualHost(\n            \"fp1\", \"ap1\", {obj.Addr((\"*\", \"443\"))},\n            True, False)\n        ssl_vh1.name = \"satoshi.com\"\n        self.config.vhosts.append(ssl_vh1)\n        mock_sel_vhost.return_value = None\n        mock_get.return_value = None\n\n        with pytest.raises(errors.PluginError):\n            self.config.enhance(\"satoshi.com\", \"redirect\")\n\n    def test_enhance_unknown_enhancement(self):\n        with pytest.raises(errors.PluginError):\n            self.config.enhance(\"certbot.demo\", \"unknown_enhancement\")\n\n    def test_enhance_no_ssl_vhost(self):\n        with mock.patch(\"certbot_apache._internal.configurator.logger.error\") as mock_log:\n            with pytest.raises(errors.PluginError):\n                self.config.enhance(\"certbot.demo\", \"redirect\")\n            # Check that correct logger.warning was printed\n            assert \"not able to find\" in mock_log.call_args[0][0]\n            assert \"\\\"redirect\\\"\" in mock_log.call_args[0][0]\n\n            mock_log.reset_mock()\n\n            with pytest.raises(errors.PluginError):\n                self.config.enhance(\"certbot.demo\", \"ensure-http-header\", \"Test\")\n            # Check that correct logger.warning was printed\n            assert \"not able to find\" in mock_log.call_args[0][0]\n            assert \"Test\" in mock_log.call_args[0][0]\n\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_ocsp_stapling(self, mock_exe):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 4, 7))\n        mock_exe.return_value = True\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"staple-ocsp\")\n\n        # Get the ssl vhost for certbot.demo\n        ssl_vhost = self.config.assoc[\"certbot.demo\"]\n\n        ssl_use_stapling_aug_path = self.config.parser.find_dir(\n            \"SSLUseStapling\", \"on\", ssl_vhost.path)\n\n        assert len(ssl_use_stapling_aug_path) == 1\n\n        ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep)\n        stapling_cache_aug_path = self.config.parser.find_dir('SSLStaplingCache',\n                    \"shmcb:/var/run/apache2/stapling_cache(128000)\",\n                    ssl_vhost_aug_path)\n\n        assert len(stapling_cache_aug_path) == 1\n\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_ocsp_stapling_twice(self, mock_exe):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 4, 7))\n        mock_exe.return_value = True\n\n        # Checking the case with already enabled ocsp stapling configuration\n        self.config.choose_vhost(\"ocspvhost.com\")\n        self.config.enhance(\"ocspvhost.com\", \"staple-ocsp\")\n\n        # Get the ssl vhost for letsencrypt.demo\n        ssl_vhost = self.config.assoc[\"ocspvhost.com\"]\n\n        ssl_use_stapling_aug_path = self.config.parser.find_dir(\n            \"SSLUseStapling\", \"on\", ssl_vhost.path)\n\n        assert len(ssl_use_stapling_aug_path) == 1\n        ssl_vhost_aug_path = parser.get_aug_path(ssl_vhost.filep)\n        stapling_cache_aug_path = self.config.parser.find_dir('SSLStaplingCache',\n                    \"shmcb:/var/run/apache2/stapling_cache(128000)\",\n                    ssl_vhost_aug_path)\n\n        assert len(stapling_cache_aug_path) == 1\n\n    def test_get_http_vhost_third_filter(self):\n        ssl_vh = obj.VirtualHost(\n            \"fp\", \"ap\", {obj.Addr((\"*\", \"443\"))},\n            True, False)\n        ssl_vh.name = \"satoshi.com\"\n        self.config.vhosts.append(ssl_vh)\n\n        # pylint: disable=protected-access\n        http_vh = self.config._get_http_vhost(ssl_vh)\n        assert http_vh.ssl is False\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_http_header_hsts(self, mock_exe, _):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"headers_module\"] = None\n        mock_exe.return_value = True\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n\n        # Get the ssl vhost for certbot.demo\n        ssl_vhost = self.config.assoc[\"certbot.demo\"]\n\n        # These are not immediately available in find_dir even with save() and\n        # load(). They must be found in sites-available\n        hsts_header = self.config.parser.find_dir(\n            \"Header\", None, ssl_vhost.path)\n\n        # four args to HSTS header\n        assert len(hsts_header) == 4\n\n    def test_http_header_hsts_twice(self):\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        # skip the enable mod\n        self.config.parser.modules[\"headers_module\"] = None\n\n        # This will create an ssl vhost for encryption-example.demo\n        self.config.choose_vhost(\"encryption-example.demo\")\n        self.config.enhance(\"encryption-example.demo\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n\n        with pytest.raises(errors.PluginEnhancementAlreadyPresent):\n            self.config.enhance(\"encryption-example.demo\",\n            \"ensure-http-header\", \"Strict-Transport-Security\")\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_http_header_uir(self, mock_exe, _):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"headers_module\"] = None\n\n        mock_exe.return_value = True\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"ensure-http-header\",\n                            \"Upgrade-Insecure-Requests\")\n\n        assert \"headers_module\" in self.config.parser.modules\n\n        # Get the ssl vhost for certbot.demo\n        ssl_vhost = self.config.assoc[\"certbot.demo\"]\n\n        # These are not immediately available in find_dir even with save() and\n        # load(). They must be found in sites-available\n        uir_header = self.config.parser.find_dir(\n            \"Header\", None, ssl_vhost.path)\n\n        # four args to HSTS header\n        assert len(uir_header) == 4\n\n    def test_http_header_uir_twice(self):\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        # skip the enable mod\n        self.config.parser.modules[\"headers_module\"] = None\n\n        # This will create an ssl vhost for encryption-example.demo\n        self.config.choose_vhost(\"encryption-example.demo\")\n        self.config.enhance(\"encryption-example.demo\", \"ensure-http-header\",\n                            \"Upgrade-Insecure-Requests\")\n\n        with pytest.raises(errors.PluginEnhancementAlreadyPresent):\n            self.config.enhance(\"encryption-example.demo\",\n            \"ensure-http-header\", \"Upgrade-Insecure-Requests\")\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_redirect_well_formed_http(self, mock_exe, _):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.parser.update_runtime_variables = mock.Mock()\n        mock_exe.return_value = True\n        self.config.get_version = mock.Mock(return_value=(2, 2))\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"redirect\")\n\n        # These are not immediately available in find_dir even with save() and\n        # load(). They must be found in sites-available\n        rw_engine = self.config.parser.find_dir(\n            \"RewriteEngine\", \"on\", self.vh_truth[3].path)\n        rw_rule = self.config.parser.find_dir(\n            \"RewriteRule\", None, self.vh_truth[3].path)\n\n        assert len(rw_engine) == 1\n        # three args to rw_rule\n        assert len(rw_rule) == 3\n\n        # [:-3] to remove the vhost index number\n        assert rw_engine[0].startswith(self.vh_truth[3].path[:-3]) is True\n        assert rw_rule[0].startswith(self.vh_truth[3].path[:-3]) is True\n\n    def test_rewrite_rule_exists(self):\n        # Skip the enable mod\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 3, 9))\n        self.config.parser.add_dir(\n            self.vh_truth[3].path, \"RewriteRule\", [\"Unknown\"])\n        # pylint: disable=protected-access\n        assert self.config._is_rewrite_exists(self.vh_truth[3]) is True\n\n    def test_rewrite_engine_exists(self):\n        # Skip the enable mod\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 3, 9))\n        self.config.parser.add_dir(\n            self.vh_truth[3].path, \"RewriteEngine\", \"on\")\n        # pylint: disable=protected-access\n        assert self.config._is_rewrite_engine_on(self.vh_truth[3])\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_redirect_with_existing_rewrite(self, mock_exe, _):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.parser.update_runtime_variables = mock.Mock()\n        mock_exe.return_value = True\n        self.config.get_version = mock.Mock(return_value=(2, 2, 0))\n\n        # Create a preexisting rewrite rule\n        self.config.parser.add_dir(\n            self.vh_truth[3].path, \"RewriteRule\", [\"UnknownPattern\",\n                                                   \"UnknownTarget\"])\n        self.config.save()\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"redirect\")\n\n        # These are not immediately available in find_dir even with save() and\n        # load(). They must be found in sites-available\n        rw_engine = self.config.parser.find_dir(\n            \"RewriteEngine\", \"on\", self.vh_truth[3].path)\n        rw_rule = self.config.parser.find_dir(\n            \"RewriteRule\", None, self.vh_truth[3].path)\n\n        assert len(rw_engine) == 1\n        # three args to rw_rule + 1 arg for the pre existing rewrite\n        assert len(rw_rule) == 5\n        # [:-3] to remove the vhost index number\n        assert rw_engine[0].startswith(self.vh_truth[3].path[:-3]) is True\n        assert rw_rule[0].startswith(self.vh_truth[3].path[:-3]) is True\n\n        assert \"rewrite_module\" in self.config.parser.modules\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_redirect_with_old_https_redirection(self, mock_exe, _):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.parser.update_runtime_variables = mock.Mock()\n        mock_exe.return_value = True\n        self.config.get_version = mock.Mock(return_value=(2, 4, 0))\n\n        ssl_vhost = self.config.choose_vhost(\"certbot.demo\")\n\n        # pylint: disable=protected-access\n        http_vhost = self.config._get_http_vhost(ssl_vhost)\n\n        # Create an old (previously supported) https redirectoin rewrite rule\n        self.config.parser.add_dir(\n            http_vhost.path, \"RewriteRule\",\n            [\"^\",\n             \"https://%{SERVER_NAME}%{REQUEST_URI}\",\n             \"[L,QSA,R=permanent]\"])\n\n        self.config.save()\n\n        try:\n            self.config.enhance(\"certbot.demo\", \"redirect\")\n        except errors.PluginEnhancementAlreadyPresent:\n            args_paths = self.config.parser.find_dir(\n                \"RewriteRule\", None, http_vhost.path, False)\n            arg_vals = [self.config.parser.aug.get(x) for x in args_paths]\n            assert arg_vals == constants.REWRITE_HTTPS_ARGS\n\n\n    def test_redirect_with_conflict(self):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        ssl_vh = obj.VirtualHost(\n            \"fp\", \"ap\", {obj.Addr((\"*\", \"443\")),\n                             obj.Addr((\"zombo.com\",))},\n            True, False)\n        # No names ^ this guy should conflict.\n\n        # pylint: disable=protected-access\n        with pytest.raises(errors.PluginError):\n            self.config._enable_redirect(ssl_vh, \"\")\n\n    def test_redirect_two_domains_one_vhost(self):\n        # Skip the enable mod\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 3, 9))\n\n        # Creates ssl vhost for the domain\n        self.config.choose_vhost(\"red.blue.purple.com\")\n\n        self.config.enhance(\"red.blue.purple.com\", \"redirect\")\n        verify_no_redirect = (\"certbot_apache._internal.configurator.\"\n                              \"ApacheConfigurator._verify_no_certbot_redirect\")\n        with mock.patch(verify_no_redirect) as mock_verify:\n            self.config.enhance(\"green.blue.purple.com\", \"redirect\")\n        assert mock_verify.called is False\n\n    def test_redirect_from_previous_run(self):\n        # Skip the enable mod\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 3, 9))\n        self.config.choose_vhost(\"red.blue.purple.com\")\n        self.config.enhance(\"red.blue.purple.com\", \"redirect\")\n        # Clear state about enabling redirect on this run\n        # pylint: disable=protected-access\n        self.config._enhanced_vhosts[\"redirect\"].clear()\n\n        with pytest.raises(errors.PluginEnhancementAlreadyPresent):\n            self.config.enhance(\"green.blue.purple.com\", \"redirect\")\n\n    def test_create_own_redirect(self):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 3, 9))\n        # For full testing... give names...\n        self.vh_truth[1].name = \"default.com\"\n        self.vh_truth[1].aliases = {\"yes.default.com\"}\n\n        # pylint: disable=protected-access\n        self.config._enable_redirect(self.vh_truth[1], \"\")\n        assert len(self.config.vhosts) == 13\n\n    def test_create_own_redirect_for_old_apache_version(self):\n        self.config.parser.modules[\"rewrite_module\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 2))\n        # For full testing... give names...\n        self.vh_truth[1].name = \"default.com\"\n        self.vh_truth[1].aliases = {\"yes.default.com\"}\n\n        # pylint: disable=protected-access\n        self.config._enable_redirect(self.vh_truth[1], \"\")\n        assert len(self.config.vhosts) == 13\n\n    def test_sift_rewrite_rule(self):\n        # pylint: disable=protected-access\n        small_quoted_target = \"RewriteRule ^ \\\"http://\\\"\"\n        assert self.config._sift_rewrite_rule(small_quoted_target) is False\n\n        https_target = \"RewriteRule ^ https://satoshi\"\n        assert self.config._sift_rewrite_rule(https_target) is True\n\n        normal_target = \"RewriteRule ^/(.*) http://www.a.com:1234/$1 [L,R]\"\n        assert self.config._sift_rewrite_rule(normal_target) is False\n\n        not_rewriterule = \"NotRewriteRule ^ ...\"\n        assert self.config._sift_rewrite_rule(not_rewriterule) is False\n\n    def get_key_and_achalls(self):\n        \"\"\"Return testing achallenges.\"\"\"\n        account_key = self.rsa512jwk\n        achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(\n                    token=b\"jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q\"),\n                \"pending\"),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"encryption-example.demo\"),\n            account_key=account_key)\n        achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(\n                    token=b\"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU\"),\n                \"pending\"),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"certbot.demo\"),\n            account_key=account_key)\n        achall3 = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=(b'x' * 16)), \"pending\"),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"example.org\"),\n            account_key=account_key)\n\n        return account_key, (achall1, achall2, achall3)\n\n    def test_enable_site_nondebian(self):\n        inc_path = \"/path/to/wherever\"\n        vhost = self.vh_truth[0]\n        vhost.enabled = False\n        vhost.filep = inc_path\n        assert self.config.parser.find_dir(\"Include\", inc_path) == []\n        assert os.path.dirname(inc_path) not in self.config.parser.existing_paths\n        self.config.enable_site(vhost)\n        assert len(self.config.parser.find_dir(\"Include\", inc_path)) >= 1\n        assert os.path.dirname(inc_path) in self.config.parser.existing_paths\n        assert os.path.basename(inc_path) in self.config.parser.existing_paths[\n                os.path.dirname(inc_path)]\n\n    @mock.patch('certbot_apache._internal.configurator.display_util.notify')\n    def test_deploy_cert_not_parsed_path(self, unused_mock_notify):\n        # Make sure that we add include to root config for vhosts when\n        # handle-sites is false\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n        tmp_path = filesystem.realpath(tempfile.mkdtemp(\"vhostroot\"))\n        filesystem.chmod(tmp_path, 0o755)\n        mock_p = \"certbot_apache._internal.configurator.ApacheConfigurator._get_ssl_vhost_path\"\n        mock_a = \"certbot_apache._internal.parser.ApacheParser.add_include\"\n\n        with mock.patch(mock_p) as mock_path:\n            mock_path.return_value = os.path.join(tmp_path, \"whatever.conf\")\n            with mock.patch(mock_a) as mock_add:\n                self.config.deploy_cert(\n                    \"encryption-example.demo\",\n                    \"example/cert.pem\", \"example/key.pem\",\n                    \"example/cert_chain.pem\")\n                # Test that we actually called add_include\n                assert mock_add.called is True\n        shutil.rmtree(tmp_path)\n\n    def test_deploy_cert_no_mod_ssl(self):\n        # Create\n        self.config.make_vhost_ssl(self.vh_truth[0])\n        self.config.parser.modules[\"socache_shmcb_module\"] = None\n        self.config.prepare_server_https = mock.Mock()\n\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.deploy_cert(\"encryption-example.demo\", \"example/cert.pem\", \"example/key.pem\",\n            \"example/cert_chain.pem\", \"example/fullchain.pem\")\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.parsed_in_original\")\n    def test_choose_vhost_and_servername_addition_parsed(self, mock_parsed):\n        ret_vh = self.vh_truth[8]\n        ret_vh.enabled = True\n        self.config.enable_site(ret_vh)\n        # Make sure that we return early\n        assert mock_parsed.called is False\n\n    def test_enable_mod_unsupported(self):\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.enable_mod(\"whatever\")\n\n    def test_choose_vhosts_wildcard(self):\n        # pylint: disable=protected-access\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            mock_select_vhs.return_value = [self.vh_truth[3]]\n            vhs = self.config._choose_vhosts_wildcard(\"*.certbot.demo\",\n                                                     create_ssl=True)\n            # Check that the dialog was called with one vh: certbot.demo\n            assert mock_select_vhs.call_args[0][0][0] == self.vh_truth[3]\n            assert len(mock_select_vhs.call_args_list) == 1\n\n            # And the actual returned values\n            assert len(vhs) == 1\n            assert vhs[0].name == \"certbot.demo\"\n            assert vhs[0].ssl is True\n\n            assert vhs[0] != self.vh_truth[3]\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.make_vhost_ssl\")\n    def test_choose_vhosts_wildcard_no_ssl(self, mock_makessl):\n        # pylint: disable=protected-access\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            mock_select_vhs.return_value = [self.vh_truth[1]]\n            vhs = self.config._choose_vhosts_wildcard(\"*.certbot.demo\",\n                                                     create_ssl=False)\n            assert mock_makessl.called is False\n            assert vhs[0] == self.vh_truth[1]\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._vhosts_for_wildcard\")\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.make_vhost_ssl\")\n    def test_choose_vhosts_wildcard_already_ssl(self, mock_makessl, mock_vh_for_w):\n        # pylint: disable=protected-access\n        # Already SSL vhost\n        mock_vh_for_w.return_value = [self.vh_truth[7]]\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            mock_select_vhs.return_value = [self.vh_truth[7]]\n            vhs = self.config._choose_vhosts_wildcard(\"whatever\",\n                                                     create_ssl=True)\n            assert mock_select_vhs.call_args[0][0][0] == self.vh_truth[7]\n            assert len(mock_select_vhs.call_args_list) == 1\n            # Ensure that make_vhost_ssl was not called, vhost.ssl == true\n            assert mock_makessl.called is False\n\n            # And the actual returned values\n            assert len(vhs) == 1\n            assert vhs[0].ssl is True\n            assert vhs[0] == self.vh_truth[7]\n\n    @mock.patch('certbot_apache._internal.configurator.display_util.notify')\n    def test_deploy_cert_wildcard(self, unused_mock_notify):\n        # pylint: disable=protected-access\n        mock_choose_vhosts = mock.MagicMock()\n        mock_choose_vhosts.return_value = [self.vh_truth[7]]\n        self.config._choose_vhosts_wildcard = mock_choose_vhosts\n        mock_d = \"certbot_apache._internal.configurator.ApacheConfigurator._deploy_cert\"\n        with mock.patch(mock_d) as mock_dep:\n            self.config.deploy_cert(\"*.wildcard.example.org\", \"/tmp/path\",\n                                    \"/tmp/path\", \"/tmp/path\", \"/tmp/path\")\n            assert mock_dep.called is True\n            assert len(mock_dep.call_args_list) == 1\n            assert self.vh_truth[7] == mock_dep.call_args_list[0][0][0]\n\n    @mock.patch(\"certbot_apache._internal.display_ops.select_vhost_multiple\")\n    def test_deploy_cert_wildcard_no_vhosts(self, mock_dialog):\n        # pylint: disable=protected-access\n        mock_dialog.return_value = []\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\"*.wild.cat\", \"/tmp/path\", \"/tmp/path\",\n                           \"/tmp/path\", \"/tmp/path\")\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._choose_vhosts_wildcard\")\n    def test_enhance_wildcard_after_install(self, mock_choose):\n        # pylint: disable=protected-access\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"headers_module\"] = None\n        self.vh_truth[3].ssl = True\n        self.config._wildcard_vhosts[\"*.certbot.demo\"] = [self.vh_truth[3]]\n        self.config.enhance(\"*.certbot.demo\", \"ensure-http-header\",\n                            \"Upgrade-Insecure-Requests\")\n        assert mock_choose.called is False\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._choose_vhosts_wildcard\")\n    def test_enhance_wildcard_no_install(self, mock_choose):\n        self.vh_truth[3].ssl = True\n        mock_choose.return_value = [self.vh_truth[3]]\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.parser.modules[\"headers_module\"] = None\n        self.config.enhance(\"*.certbot.demo\", \"ensure-http-header\",\n                            \"Upgrade-Insecure-Requests\")\n        assert mock_choose.called is True\n\n    def test_add_vhost_id(self):\n        for vh in [self.vh_truth[0], self.vh_truth[1], self.vh_truth[2]]:\n            vh_id = self.config.add_vhost_id(vh)\n            assert vh == self.config.find_vhost_by_id(vh_id)\n\n    def test_find_vhost_by_id_404(self):\n        with pytest.raises(errors.PluginError):\n            self.config.find_vhost_by_id(\"nonexistent\")\n\n    def test_add_vhost_id_already_exists(self):\n        first_id = self.config.add_vhost_id(self.vh_truth[0])\n        second_id = self.config.add_vhost_id(self.vh_truth[0])\n        assert first_id == second_id\n\n    def test_realpath_replaces_symlink(self):\n        orig_match = self.config.parser.aug.match\n        mock_vhost = copy.deepcopy(self.vh_truth[0])\n        mock_vhost.filep = mock_vhost.filep.replace('sites-enabled', u'sites-available')\n        mock_vhost.path = mock_vhost.path.replace('sites-enabled', 'sites-available')\n        mock_vhost.enabled = False\n        self.config.parser.parse_file(mock_vhost.filep)\n\n        def mock_match(aug_expr):\n            \"\"\"Return a mocked match list of VirtualHosts\"\"\"\n            if \"/mocked/path\" in aug_expr:\n                return [self.vh_truth[1].path, self.vh_truth[0].path, mock_vhost.path]\n            return orig_match(aug_expr)\n\n        self.config.parser.parser_paths = [\"/mocked/path\"]\n        self.config.parser.aug.match = mock_match\n        vhs = self.config.get_virtual_hosts()\n        assert len(vhs) == 2\n        assert vhs[0] == self.vh_truth[1]\n        # mock_vhost should have replaced the vh_truth[0], because its filepath\n        # isn't a symlink\n        assert vhs[1] == mock_vhost\n\n\nclass AugeasVhostsTest(util.ApacheTest):\n    \"\"\"Test vhosts with illegal names dependent on augeas version.\"\"\"\n    # pylint: disable=protected-access\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        td = \"debian_apache_2_4/augeas_vhosts\"\n        cr = \"debian_apache_2_4/augeas_vhosts/apache2\"\n        vr = \"debian_apache_2_4/augeas_vhosts/apache2/sites-available\"\n        super().setUp(test_dir=td,\n                      config_root=cr,\n                      vhost_root=vr)\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir,\n            self.work_dir)\n\n    def test_choosevhost_with_illegal_name(self):\n        self.config.parser.aug = mock.MagicMock()\n        self.config.parser.aug.match.side_effect = RuntimeError\n        path = \"debian_apache_2_4/augeas_vhosts/apache2/sites-available/old-and-default.conf\"\n        chosen_vhost = self.config._create_vhost(path)\n        assert chosen_vhost is None\n\n    def test_choosevhost_works(self):\n        path = \"debian_apache_2_4/augeas_vhosts/apache2/sites-available/old-and-default.conf\"\n        chosen_vhost = self.config._create_vhost(path)\n        assert chosen_vhost is None or chosen_vhost.path == path\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator._create_vhost\")\n    def test_get_vhost_continue(self, mock_vhost):\n        mock_vhost.return_value = None\n        vhs = self.config.get_virtual_hosts()\n        assert [] == vhs\n\n    def test_choose_vhost_with_matching_wildcard(self):\n        names = (\n            \"an.example.net\", \"another.example.net\", \"an.other.example.net\")\n        for name in names:\n            with self.subTest(name=name):\n                assert name not in self.config.choose_vhost(name).aliases\n\n    @mock.patch(\"certbot_apache._internal.obj.VirtualHost.conflicts\")\n    def test_choose_vhost_without_matching_wildcard(self, mock_conflicts):\n        mock_conflicts.return_value = False\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost\"\n        with mock.patch(mock_path, lambda _, vhosts: vhosts[0]):\n            for name in (\"a.example.net\", \"other.example.net\"):\n                assert name in self.config.choose_vhost(name).aliases\n\n    @mock.patch(\"certbot_apache._internal.obj.VirtualHost.conflicts\")\n    def test_choose_vhost_wildcard_not_found(self, mock_conflicts):\n        mock_conflicts.return_value = False\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost\"\n        names = (\n            \"abc.example.net\", \"not.there.tld\", \"aa.wildcard.tld\"\n        )\n        with mock.patch(mock_path) as mock_select:\n            mock_select.return_value = self.config.vhosts[0]\n            for name in names:\n                orig_cc = mock_select.call_count\n                self.config.choose_vhost(name)\n                assert mock_select.call_count - orig_cc == 1\n\n    def test_choose_vhost_wildcard_found(self):\n        mock_path = \"certbot_apache._internal.display_ops.select_vhost\"\n        names = (\n            \"ab.example.net\", \"a.wildcard.tld\", \"yetanother.example.net\"\n        )\n        with mock.patch(mock_path) as mock_select:\n            mock_select.return_value = self.config.vhosts[0]\n            for name in names:\n                self.config.choose_vhost(name)\n                assert mock_select.call_count == 0\n\n    def test_augeas_span_error(self):\n        broken_vhost = self.config.vhosts[0]\n        broken_vhost.path = broken_vhost.path + \"/nonexistent\"\n        with pytest.raises(errors.PluginError):\n            self.config.make_vhost_ssl(broken_vhost)\n\n\nclass MultiVhostsTest(util.ApacheTest):\n    \"\"\"Test configuration with multiple virtualhosts in a single file.\"\"\"\n    # pylint: disable=protected-access\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        td = \"debian_apache_2_4/multi_vhosts\"\n        cr = \"debian_apache_2_4/multi_vhosts/apache2\"\n        vr = \"debian_apache_2_4/multi_vhosts/apache2/sites-available\"\n        super().setUp(test_dir=td, config_root=cr, vhost_root=vr)\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path,\n            self.config_dir, self.work_dir, conf_vhost_path=self.vhost_path)\n        self.vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multi_vhosts\")\n\n    def test_make_vhost_ssl(self):\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[1])\n\n        assert ssl_vhost.filep == \\\n            os.path.join(self.config_path, \"sites-available\",\n                         \"default-le-ssl.conf\")\n\n        assert ssl_vhost.path == \\\n                         \"/files\" + ssl_vhost.filep + \"/IfModule/VirtualHost\"\n        assert len(ssl_vhost.addrs) == 1\n        assert {obj.Addr.fromstring(\"*:443\")} == ssl_vhost.addrs\n        assert ssl_vhost.name == \"banana.vomit.com\"\n        assert ssl_vhost.ssl is True\n        assert ssl_vhost.enabled is False\n\n        mock_path = \"certbot_apache._internal.configurator.ApacheConfigurator._get_new_vh_path\"\n        with mock.patch(mock_path) as mock_getpath:\n            mock_getpath.return_value = None\n            with pytest.raises(errors.PluginError):\n                self.config.make_vhost_ssl(self.vh_truth[1])\n\n    def test_get_new_path(self):\n        with_index_1 = [\"/path[1]/section[1]\"]\n        without_index = [\"/path/section\"]\n        with_index_2 = [\"/path[2]/section[2]\"]\n        assert self.config._get_new_vh_path(without_index, with_index_1) is None\n        assert self.config._get_new_vh_path(without_index, with_index_2) == with_index_2[0]\n\n        both = with_index_1 + with_index_2\n        assert self.config._get_new_vh_path(without_index, both) == with_index_2[0]\n\n    @mock.patch(\"certbot_apache._internal.configurator.display_util.notify\")\n    def test_make_vhost_ssl_with_existing_rewrite_rule(self, mock_notify):\n        self.config.parser.modules[\"rewrite_module\"] = None\n\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[4])\n\n        assert self.config.parser.find_dir(\"RewriteEngine\", \"on\", ssl_vhost.path, False)\n\n        with open(ssl_vhost.filep) as the_file:\n            conf_text = the_file.read()\n        commented_rewrite_rule = (\"# RewriteRule \\\"^/secrets/(.+)\\\" \"\n                                  \"\\\"https://new.example.com/docs/$1\\\" [R,L]\")\n        uncommented_rewrite_rule = (\"RewriteRule \\\"^/docs/(.+)\\\"  \"\n                                    \"\\\"http://new.example.com/docs/$1\\\"  [R,L]\")\n        assert commented_rewrite_rule in conf_text\n        assert uncommented_rewrite_rule in conf_text\n        assert mock_notify.call_count == 1\n        assert \"Some rewrite rules\" in mock_notify.call_args[0][0]\n\n    @mock.patch(\"certbot_apache._internal.configurator.display_util.notify\")\n    def test_make_vhost_ssl_with_existing_rewrite_conds(self, mock_notify):\n        self.config.parser.modules[\"rewrite_module\"] = None\n\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[3])\n\n        with open(ssl_vhost.filep) as the_file:\n            conf_lines = the_file.readlines()\n        conf_line_set = [l.strip() for l in conf_lines]\n        not_commented_cond1 = (\"RewriteCond \"\n                \"%{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f\")\n        not_commented_rewrite_rule = (\"RewriteRule \"\n            \"^(.*)$ b://u%{REQUEST_URI} [P,NE,L]\")\n\n        commented_cond1 = \"# RewriteCond %{HTTPS} !=on\"\n        commented_cond2 = \"# RewriteCond %{HTTPS} !^$\"\n        commented_rewrite_rule = (\"# RewriteRule ^ \"\n                                  \"https://%{SERVER_NAME}%{REQUEST_URI} \"\n                                  \"[L,NE,R=permanent]\")\n\n        assert not_commented_cond1 in conf_line_set\n        assert not_commented_rewrite_rule in conf_line_set\n\n        assert commented_cond1 in conf_line_set\n        assert commented_cond2 in conf_line_set\n        assert commented_rewrite_rule in conf_line_set\n        assert mock_notify.call_count == 1\n        assert \"Some rewrite rules\" in mock_notify.call_args[0][0]\n\n\nclass InstallSslOptionsConfTest(util.ApacheTest):\n    \"\"\"Test that the options-ssl-nginx.conf file is installed and updated properly.\"\"\"\n\n    def setUp(self): # pylint: disable=arguments-differ\n        super().setUp()\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir)\n\n    def _call(self):\n        self.config.install_ssl_options_conf(self.config.mod_ssl_conf,\n                                             self.config.updated_mod_ssl_conf_digest)\n\n    def _current_ssl_options_hash(self):\n        return crypto_util.sha256sum(self.config.pick_apache_config())\n\n    def _assert_current_file(self):\n        assert os.path.isfile(self.config.mod_ssl_conf) is True\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf) == \\\n            self._current_ssl_options_hash()\n\n    def test_no_file(self):\n        # prepare should have placed a file there\n        self._assert_current_file()\n        os.remove(self.config.mod_ssl_conf)\n        assert os.path.isfile(self.config.mod_ssl_conf) is False\n        self._call()\n        self._assert_current_file()\n\n    def test_current_file(self):\n        self._assert_current_file()\n        self._call()\n        self._assert_current_file()\n\n    def test_prev_file_updates_to_current(self):\n        from certbot_apache._internal.constants import ALL_SSL_OPTIONS_HASHES\n        ALL_SSL_OPTIONS_HASHES.insert(0, \"test_hash_does_not_match\")\n        with mock.patch('certbot.crypto_util.sha256sum') as mock_sha256:\n            mock_sha256.return_value = ALL_SSL_OPTIONS_HASHES[0]\n            self._call()\n        self._assert_current_file()\n\n    def test_manually_modified_current_file_does_not_update(self):\n        with open(self.config.mod_ssl_conf, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.called is False\n        assert os.path.isfile(self.config.mod_ssl_conf) is True\n        assert crypto_util.sha256sum(\n            self.config.pick_apache_config()) == \\\n            self._current_ssl_options_hash()\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf) != \\\n            self._current_ssl_options_hash()\n\n    def test_manually_modified_past_file_warns(self):\n        with open(self.config.mod_ssl_conf, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with open(self.config.updated_mod_ssl_conf_digest, \"w\") as f:\n            f.write(\"hashofanoldversion\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.call_args[0][0] == \\\n                \"%s has been manually modified; updated file \" \\\n                \"saved to %s. We recommend updating %s for security purposes.\"\n        assert crypto_util.sha256sum(\n            self.config.pick_apache_config()) == \\\n            self._current_ssl_options_hash()\n        # only print warning once\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.called is False\n\n    @mock.patch('certbot_apache._internal.configurator.logger.warning')\n    @mock.patch('certbot_apache._internal.configurator.ApacheConfigurator.openssl_version')\n    def test_pick_apache_config_versions_and_warnings(self, mock_openssl_version, mock_warning):\n        def has_logged_warning():\n            \"\"\"Returns True if a warning was logged about updating Apache.\"\"\"\n            return any('update apache' in call.args[0] for call in mock_warning.call_args_list)\n\n        # old apache, no openssl\n        self.config.version = (2, 4, 10)\n        mock_openssl_version.return_value = None\n        assert 'old' in self.config.pick_apache_config()\n        assert not has_logged_warning()\n\n        # old apache, old openssl\n        mock_warning.reset_mock()\n        mock_openssl_version.return_value = '1.0.2a'\n        assert 'old' in self.config.pick_apache_config()\n        assert has_logged_warning()\n\n        # old apache, new openssl\n        mock_warning.reset_mock()\n        mock_openssl_version.return_value = '1.0.2m'\n        assert 'old' in self.config.pick_apache_config()\n        assert has_logged_warning()\n\n        # new apache, no openssl\n        mock_warning.reset_mock()\n        self.config.version = (2, 4, 11)\n        mock_openssl_version.return_value = None\n        assert 'old' in self.config.pick_apache_config()\n        assert not has_logged_warning()\n\n        # new apache, old openssl\n        mock_warning.reset_mock()\n        mock_openssl_version.return_value = '1.0.2a'\n        assert 'old' in self.config.pick_apache_config()\n        assert has_logged_warning()\n\n        # new apache, new openssl\n        mock_warning.reset_mock()\n        mock_openssl_version.return_value = '1.0.2m'\n        assert 'current' in self.config.pick_apache_config()\n        assert not has_logged_warning()\n\n    def test_ssl_config_files_hash_in_all_hashes(self):\n        \"\"\"\n        It is really critical that all TLS Apache config files have their SHA256 hash registered in\n        constants.ALL_SSL_OPTIONS_HASHES. Otherwise Certbot will mistakenly assume that the config\n        file has been manually edited by the user, and will refuse to update it.\n        This test ensures that all necessary hashes are present.\n        \"\"\"\n        import importlib.resources\n\n        from certbot_apache._internal.constants import ALL_SSL_OPTIONS_HASHES\n\n        ref = importlib.resources.files(\"certbot_apache\") / \"_internal\" / \"tls_configs\"\n        with importlib.resources.as_file(ref) as tls_configs_dir:\n            all_files = [os.path.join(tls_configs_dir, name) for name in os.listdir(tls_configs_dir)\n                        if name.endswith('options-ssl-apache.conf')]\n            assert len(all_files) >= 1\n            for one_file in all_files:\n                file_hash = crypto_util.sha256sum(one_file)\n                assert file_hash in ALL_SSL_OPTIONS_HASHES, \\\n                    f\"Constants.ALL_SSL_OPTIONS_HASHES must be appended with the sha256 \" \\\n                    f\"hash of {one_file} when it is updated.\"\n\n    def test_openssl_version(self):\n        self.config._openssl_version = None\n        some_string_contents = b\"\"\"\n            SSLOpenSSLConfCmd\n            OpenSSL configuration command\n            SSLv3 not supported by this version of OpenSSL\n            '%s': invalid OpenSSL configuration command\n            OpenSSL 1.0.2g  1 Mar 2016\n            OpenSSL\n            AH02407: \"SSLOpenSSLConfCmd %s %s\" failed for %s\n            AH02556: \"SSLOpenSSLConfCmd %s %s\" applied to %s\n            OpenSSL 1.0.2g  1 Mar 2016\n            \"\"\"\n        # ssl_module as a DSO\n        self.config.parser.modules['ssl_module'] = '/fake/path'\n        with mock.patch(\"certbot_apache._internal.configurator.\"\n            \"ApacheConfigurator._open_module_file\") as mock_omf:\n            mock_omf.return_value = some_string_contents\n            assert self.config.openssl_version() == \"1.0.2g\"\n\n        # ssl_module statically linked\n        self.config._openssl_version = None\n        self.config.parser.modules['ssl_module'] = None\n        self.config.options.bin = '/fake/path/to/httpd'\n        with mock.patch(\"certbot_apache._internal.configurator.\"\n            \"ApacheConfigurator._open_module_file\") as mock_omf:\n            mock_omf.return_value = some_string_contents\n            assert self.config.openssl_version() == \"1.0.2g\"\n\n    def test_openssl_version_warns(self):\n        self.config._openssl_version = '1.0.2a'\n        assert self.config.openssl_version() == '1.0.2a'\n\n        self.config._openssl_version = None\n        with mock.patch(\"certbot_apache._internal.configurator.logger.warning\") as mock_log:\n            assert self.config.openssl_version() is None\n            assert \"Could not find ssl_module\" in mock_log.call_args[0][0]\n\n        # When no ssl_module is present at all\n        self.config._openssl_version = None\n        assert \"ssl_module\" not in self.config.parser.modules\n        with mock.patch(\"certbot_apache._internal.configurator.logger.warning\") as mock_log:\n            assert self.config.openssl_version() is None\n            assert \"Could not find ssl_module\" in mock_log.call_args[0][0]\n\n        # When ssl_module is statically linked but --apache-bin not provided\n        self.config._openssl_version = None\n        self.config.options.bin = None\n        self.config.parser.modules['ssl_module'] = None\n        with mock.patch(\"certbot_apache._internal.configurator.logger.warning\") as mock_log:\n            assert self.config.openssl_version() is None\n            assert \"ssl_module is statically linked but\" in mock_log.call_args[0][0]\n\n        self.config.parser.modules['ssl_module'] = \"/fake/path\"\n        with mock.patch(\"certbot_apache._internal.configurator.logger.warning\") as mock_log:\n            # Check that correct logger.warning was printed\n            assert self.config.openssl_version() is None\n            assert \"Unable to read\" in mock_log.call_args[0][0]\n\n        contents_missing_openssl = b\"these contents won't match the regex\"\n        with mock.patch(\"certbot_apache._internal.configurator.\"\n            \"ApacheConfigurator._open_module_file\") as mock_omf:\n            mock_omf.return_value = contents_missing_openssl\n            with mock.patch(\"certbot_apache._internal.configurator.logger.warning\") as mock_log:\n                # Check that correct logger.warning was printed\n                assert self.config.openssl_version() is None\n                assert \"Could not find OpenSSL\" in mock_log.call_args[0][0]\n\n    def test_open_module_file(self):\n        mock_open = mock.mock_open(read_data=\"testing 12 3\")\n        with mock.patch(\"builtins.open\", mock_open):\n            assert self.config._open_module_file(\"/nonsense/\") == \"testing 12 3\"\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/conftest.py",
    "content": "from unittest import mock\n\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef mock_sleep():\n    with mock.patch(\"time.sleep\") as mocked:\n        yield mocked\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/debian_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.configurator for Debian overrides\"\"\"\nimport shutil\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.tests import util as certbot_util\nfrom certbot_apache._internal import apache_util\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal.tests import util\n\n\nclass MultipleVhostsTestDebian(util.ApacheTest):\n    \"\"\"Multiple vhost tests for Debian family of distros\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n            os_info=\"debian\")\n        self.config = self.mock_deploy_cert(self.config)\n        self.vh_truth = util.get_vh_truth(self.temp_dir,\n                                          \"debian_apache_2_4/multiple_vhosts\")\n\n    def mock_deploy_cert(self, config):\n        \"\"\"A test for a mock deploy cert\"\"\"\n        config.real_deploy_cert = self.config.deploy_cert\n\n        def mocked_deploy_cert(*args, **kwargs):\n            \"\"\"a helper to mock a deployed cert\"\"\"\n            g_mod = \"certbot_apache._internal.configurator.ApacheConfigurator.enable_mod\"\n            d_mod = \"certbot_apache._internal.override_debian.DebianConfigurator.enable_mod\"\n            with mock.patch(g_mod):\n                with mock.patch(d_mod):\n                    config.real_deploy_cert(*args, **kwargs)\n        self.config.deploy_cert = mocked_deploy_cert\n        return self.config\n\n    def test_enable_mod_unsupported_dirs(self):\n        shutil.rmtree(os.path.join(self.config.parser.root, \"mods-enabled\"))\n        with pytest.raises(errors.NotSupportedError):\n            self.config.enable_mod(\"ssl\")\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    @mock.patch(\"certbot_apache._internal.apache_util.subprocess.run\")\n    def test_enable_mod(self, mock_run, mock_exe_exists, mock_run_script):\n        mock_run.return_value.stdout = \"Define: DUMP_RUN_CFG\"\n        mock_run.return_value.stderr = \"\"\n        mock_run.return_value.returncode = 0\n        mock_exe_exists.return_value = True\n\n        self.config.enable_mod(\"ssl\")\n        assert \"ssl_module\" in self.config.parser.modules\n        assert \"mod_ssl.c\" in self.config.parser.modules\n\n        assert mock_run_script.called is True\n\n    def test_deploy_cert_enable_new_vhost(self):\n        # Create\n        ssl_vhost = self.config.make_vhost_ssl(self.vh_truth[0])\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        assert ssl_vhost.enabled is False\n        with certbot_util.patch_display_util():\n            self.config.deploy_cert(\n                \"encryption-example.demo\", \"example/cert.pem\", \"example/key.pem\",\n                \"example/cert_chain.pem\", \"example/fullchain.pem\")\n            assert ssl_vhost.enabled is True\n            # Make sure that we don't error out if symlink already exists\n            ssl_vhost.enabled = False\n            assert ssl_vhost.enabled is False\n            self.config.deploy_cert(\n                \"encryption-example.demo\", \"example/cert.pem\", \"example/key.pem\",\n                \"example/cert_chain.pem\", \"example/fullchain.pem\")\n            assert ssl_vhost.enabled is True\n\n    def test_enable_site_failure(self):\n        self.config.parser.root = \"/tmp/nonexistent\"\n        with mock.patch(\"certbot.compat.os.path.isdir\") as mock_dir:\n            mock_dir.return_value = True\n            with mock.patch(\"certbot.compat.os.path.islink\") as mock_link:\n                mock_link.return_value = False\n                with pytest.raises(errors.NotSupportedError):\n                    self.config.enable_site(obj.VirtualHost(\"asdf\", \"afsaf\", set(), False, False))\n\n    def test_deploy_cert_newssl(self):\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir,\n            self.work_dir, version=(2, 4, 16))\n        self.config = self.mock_deploy_cert(self.config)\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n\n        # Get the default 443 vhost\n        self.config.assoc[\"random.demo\"] = self.vh_truth[1]\n        with certbot_util.patch_display_util():\n            self.config.deploy_cert(\n                \"random.demo\", \"example/cert.pem\", \"example/key.pem\",\n                \"example/cert_chain.pem\", \"example/fullchain.pem\")\n        self.config.save()\n\n        # Verify ssl_module was enabled.\n        assert self.vh_truth[1].enabled is True\n        assert \"ssl_module\" in self.config.parser.modules\n\n        loc_cert = self.config.parser.find_dir(\n            \"sslcertificatefile\", \"example/fullchain.pem\",\n            self.vh_truth[1].path)\n        loc_key = self.config.parser.find_dir(\n            \"sslcertificateKeyfile\", \"example/key.pem\", self.vh_truth[1].path)\n\n        # Verify one directive was found in the correct file\n        assert len(loc_cert) == 1\n        assert apache_util.get_file_path(loc_cert[0]) == \\\n            self.vh_truth[1].filep\n\n        assert len(loc_key) == 1\n        assert apache_util.get_file_path(loc_key[0]) == \\\n            self.vh_truth[1].filep\n\n    def test_deploy_cert_newssl_no_fullchain(self):\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir,\n            self.work_dir, version=(2, 4, 16))\n        self.config = self.mock_deploy_cert(self.config)\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n\n        # Get the default 443 vhost\n        self.config.assoc[\"random.demo\"] = self.vh_truth[1]\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\n                              \"random.demo\", \"example/cert.pem\",\n                              \"example/key.pem\")\n\n    def test_deploy_cert_old_apache_no_chain(self):\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir,\n            self.work_dir, version=(2, 4, 7))\n        self.config = self.mock_deploy_cert(self.config)\n        self.config.parser.modules[\"ssl_module\"] = None\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n\n        # Get the default 443 vhost\n        self.config.assoc[\"random.demo\"] = self.vh_truth[1]\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\n                              \"random.demo\", \"example/cert.pem\",\n                              \"example/key.pem\")\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_ocsp_stapling_enable_mod(self, mock_exe, _):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        self.config.get_version = mock.Mock(return_value=(2, 4, 7))\n        mock_exe.return_value = True\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"staple-ocsp\")\n        assert \"socache_shmcb_module\" in self.config.parser.modules\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_ensure_http_header_enable_mod(self, mock_exe, _):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        self.config.parser.modules[\"mod_ssl.c\"] = None\n        mock_exe.return_value = True\n\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n        assert \"headers_module\" in self.config.parser.modules\n\n    @mock.patch(\"certbot.util.run_script\")\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_redirect_enable_mod(self, mock_exe, _):\n        self.config.parser.update_runtime_variables = mock.Mock()\n        mock_exe.return_value = True\n        self.config.get_version = mock.Mock(return_value=(2, 2))\n        # This will create an ssl vhost for certbot.demo\n        self.config.choose_vhost(\"certbot.demo\")\n        self.config.enhance(\"certbot.demo\", \"redirect\")\n        assert \"rewrite_module\" in self.config.parser.modules\n\n    def test_enable_site_already_enabled(self):\n        assert self.vh_truth[1].enabled is True\n        self.config.enable_site(self.vh_truth[1])\n\n    def test_enable_site_call_parent(self):\n        with mock.patch(\n            \"certbot_apache._internal.configurator.ApacheConfigurator.enable_site\") as e_s:\n            self.config.parser.root = \"/tmp/nonexistent\"\n            vh = self.vh_truth[0]\n            vh.enabled = False\n            self.config.enable_site(vh)\n            assert e_s.called is True\n\n    @mock.patch(\"certbot.util.exe_exists\")\n    def test_enable_mod_no_disable(self, mock_exe_exists):\n        mock_exe_exists.return_value = False\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.enable_mod(\"ssl\")\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/display_ops_test.py",
    "content": "\"\"\"Test certbot_apache._internal.display_ops.\"\"\"\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.display import util as display_util\nfrom certbot.tests import util as certbot_util\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal.display_ops import select_vhost_multiple\nfrom certbot_apache._internal.tests import util\n\n\nclass SelectVhostMultiTest(unittest.TestCase):\n    \"\"\"Tests for certbot_apache._internal.display_ops.select_vhost_multiple.\"\"\"\n\n    def setUp(self):\n        self.base_dir = \"/example_path\"\n        self.vhosts = util.get_vh_truth(\n            self.base_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def test_select_no_input(self):\n        assert len(select_vhost_multiple([])) == 0\n\n    @certbot_util.patch_display_util()\n    def test_select_correct(self, mock_util):\n        mock_util().checklist.return_value = (\n            display_util.OK, [self.vhosts[3].display_repr(),\n                              self.vhosts[2].display_repr()])\n        vhs = select_vhost_multiple([self.vhosts[3],\n                                     self.vhosts[2],\n                                     self.vhosts[1]])\n        assert self.vhosts[2] in vhs\n        assert self.vhosts[3] in vhs\n        assert self.vhosts[1] not in vhs\n\n    @certbot_util.patch_display_util()\n    def test_select_cancel(self, mock_util):\n        mock_util().checklist.return_value = (display_util.CANCEL, \"whatever\")\n        vhs = select_vhost_multiple([self.vhosts[2], self.vhosts[3]])\n        assert vhs == []\n\n\nclass SelectVhostTest(unittest.TestCase):\n    \"\"\"Tests for certbot_apache._internal.display_ops.select_vhost.\"\"\"\n\n    def setUp(self):\n        self.base_dir = \"/example_path\"\n        self.vhosts = util.get_vh_truth(\n            self.base_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    @classmethod\n    def _call(cls, vhosts):\n        from certbot_apache._internal.display_ops import select_vhost\n        return select_vhost(\"example.com\", vhosts)\n\n    @certbot_util.patch_display_util()\n    def test_successful_choice(self, mock_util):\n        mock_util().menu.return_value = (display_util.OK, 3)\n        assert self.vhosts[3] == self._call(self.vhosts)\n\n    @certbot_util.patch_display_util()\n    def test_noninteractive(self, mock_util):\n        mock_util().menu.side_effect = errors.MissingCommandlineFlag(\"no vhost default\")\n        try:\n            self._call(self.vhosts)\n        except errors.MissingCommandlineFlag as e:\n            assert \"vhost ambiguity\" in str(e)\n\n    @certbot_util.patch_display_util()\n    def test_more_info_cancel(self, mock_util):\n        mock_util().menu.side_effect = [\n            (display_util.CANCEL, -1),\n        ]\n\n        assert self._call(self.vhosts) is None\n\n    def test_no_vhosts(self):\n        assert self._call([]) is None\n\n    @mock.patch(\"certbot_apache._internal.display_ops.display_util\")\n    @mock.patch(\"certbot_apache._internal.display_ops.logger\")\n    def test_small_display(self, mock_logger, mock_display_util):\n        mock_display_util.WIDTH = 20\n        mock_display_util.menu.return_value = (display_util.OK, 0)\n        self._call(self.vhosts)\n\n        assert mock_logger.debug.call_count == 1\n\n    @certbot_util.patch_display_util()\n    def test_multiple_names(self, mock_util):\n        mock_util().menu.return_value = (display_util.OK, 5)\n\n        self.vhosts.append(\n            obj.VirtualHost(\n                \"path\", \"aug_path\", {obj.Addr.fromstring(\"*:80\")},\n                False, False,\n                \"wildcard.com\", {\"*.wildcard.com\"}))\n\n        assert self.vhosts[5] == self._call(self.vhosts)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/dualnode_test.py",
    "content": "\"\"\"Tests for DualParserNode implementation\"\"\"\nimport unittest\nfrom unittest import mock\n\nfrom certbot_apache._internal import assertions\nfrom certbot_apache._internal import augeasparser\nfrom certbot_apache._internal import dualparser\nimport pytest\n\n\nclass DualParserNodeTest(unittest.TestCase):  # pylint: disable=too-many-public-methods\n    \"\"\"DualParserNode tests\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        parser_mock = mock.MagicMock()\n        parser_mock.aug.match.return_value = []\n        parser_mock.get_arg.return_value = []\n        self.metadata = {\"augeasparser\": parser_mock, \"augeaspath\": \"/invalid\", \"ac_ast\": None}\n        self.block = dualparser.DualBlockNode(name=\"block\",\n                                              ancestor=None,\n                                              filepath=\"/tmp/something\",\n                                              metadata=self.metadata)\n        self.block_two = dualparser.DualBlockNode(name=\"block\",\n                                                  ancestor=self.block,\n                                                  filepath=\"/tmp/something\",\n                                                  metadata=self.metadata)\n        self.directive = dualparser.DualDirectiveNode(name=\"directive\",\n                                                      ancestor=self.block,\n                                                      filepath=\"/tmp/something\",\n                                                      metadata=self.metadata)\n        self.comment = dualparser.DualCommentNode(comment=\"comment\",\n                                                  ancestor=self.block,\n                                                  filepath=\"/tmp/something\",\n                                                  metadata=self.metadata)\n\n    def test_create_with_precreated(self):\n        cnode = dualparser.DualCommentNode(comment=\"comment\",\n                                           ancestor=self.block,\n                                           filepath=\"/tmp/something\",\n                                           primary=self.comment.secondary,\n                                           secondary=self.comment.primary)\n        dnode = dualparser.DualDirectiveNode(name=\"directive\",\n                                             ancestor=self.block,\n                                             filepath=\"/tmp/something\",\n                                             primary=self.directive.secondary,\n                                             secondary=self.directive.primary)\n        bnode = dualparser.DualBlockNode(name=\"block\",\n                                         ancestor=self.block,\n                                         filepath=\"/tmp/something\",\n                                         primary=self.block.secondary,\n                                         secondary=self.block.primary)\n        # Switched around\n        assert cnode.primary == self.comment.secondary\n        assert cnode.secondary == self.comment.primary\n        assert dnode.primary == self.directive.secondary\n        assert dnode.secondary == self.directive.primary\n        assert bnode.primary == self.block.secondary\n        assert bnode.secondary == self.block.primary\n\n    def test_set_params(self):\n        params = (\"first\", \"second\")\n        self.directive.primary.set_parameters = mock.Mock()\n        self.directive.secondary.set_parameters = mock.Mock()\n        self.directive.set_parameters(params)\n        assert self.directive.primary.set_parameters.called is True\n        assert self.directive.secondary.set_parameters.called is True\n\n    def test_set_parameters(self):\n        pparams = mock.MagicMock()\n        sparams = mock.MagicMock()\n        pparams.parameters = (\"a\", \"b\")\n        sparams.parameters = (\"a\", \"b\")\n        self.directive.primary.set_parameters = pparams\n        self.directive.secondary.set_parameters = sparams\n        self.directive.set_parameters((\"param\", \"seq\"))\n        assert pparams.called is True\n        assert sparams.called is True\n\n    def test_delete_child(self):\n        pdel = mock.MagicMock()\n        sdel = mock.MagicMock()\n        self.block.primary.delete_child = pdel\n        self.block.secondary.delete_child = sdel\n        self.block.delete_child(self.comment)\n        assert pdel.called is True\n        assert sdel.called is True\n\n    def test_unsaved_files(self):\n        puns = mock.MagicMock()\n        suns = mock.MagicMock()\n        puns.return_value = assertions.PASS\n        suns.return_value = assertions.PASS\n        self.block.primary.unsaved_files = puns\n        self.block.secondary.unsaved_files = suns\n        self.block.unsaved_files()\n        assert puns.called is True\n        assert suns.called is True\n\n    def test_getattr_equality(self):\n        self.directive.primary.variableexception = \"value\"\n        self.directive.secondary.variableexception = \"not_value\"\n        with pytest.raises(AssertionError):\n            _ = self.directive.variableexception\n\n        self.directive.primary.variable = \"value\"\n        self.directive.secondary.variable = \"value\"\n        try:\n            self.directive.variable\n        except AssertionError: # pragma: no cover\n            self.fail(\"getattr check raised an AssertionError where it shouldn't have\")\n\n    def test_parsernode_dirty_assert(self):\n        # disable assertion pass\n        self.comment.primary.comment = \"value\"\n        self.comment.secondary.comment = \"value\"\n        self.comment.primary.filepath = \"x\"\n        self.comment.secondary.filepath = \"x\"\n\n        self.comment.primary.dirty = False\n        self.comment.secondary.dirty = True\n        with pytest.raises(AssertionError):\n            assertions.assertEqual(self.comment.primary, self.comment.secondary)\n\n    def test_parsernode_filepath_assert(self):\n        # disable assertion pass\n        self.comment.primary.comment = \"value\"\n        self.comment.secondary.comment = \"value\"\n\n        self.comment.primary.filepath = \"first\"\n        self.comment.secondary.filepath = \"second\"\n        with pytest.raises(AssertionError):\n            assertions.assertEqual(self.comment.primary, self.comment.secondary)\n\n    def test_add_child_block(self):\n        mock_first = mock.MagicMock(return_value=self.block.primary)\n        mock_second = mock.MagicMock(return_value=self.block.secondary)\n        self.block.primary.add_child_block = mock_first\n        self.block.secondary.add_child_block = mock_second\n        self.block.add_child_block(\"Block\")\n        assert mock_first.called is True\n        assert mock_second.called is True\n\n    def test_add_child_directive(self):\n        mock_first = mock.MagicMock(return_value=self.directive.primary)\n        mock_second = mock.MagicMock(return_value=self.directive.secondary)\n        self.block.primary.add_child_directive = mock_first\n        self.block.secondary.add_child_directive = mock_second\n        self.block.add_child_directive(\"Directive\")\n        assert mock_first.called is True\n        assert mock_second.called is True\n\n    def test_add_child_comment(self):\n        mock_first = mock.MagicMock(return_value=self.comment.primary)\n        mock_second = mock.MagicMock(return_value=self.comment.secondary)\n        self.block.primary.add_child_comment = mock_first\n        self.block.secondary.add_child_comment = mock_second\n        self.block.add_child_comment(\"Comment\")\n        assert mock_first.called is True\n        assert mock_second.called is True\n\n    def test_find_comments(self):\n        pri_comments = [augeasparser.AugeasCommentNode(comment=\"some comment\",\n                                                       ancestor=self.block,\n                                                       filepath=\"/path/to/whatever\",\n                                                       metadata=self.metadata)]\n        sec_comments = [augeasparser.AugeasCommentNode(comment=assertions.PASS,\n                                                       ancestor=self.block,\n                                                       filepath=assertions.PASS,\n                                                       metadata=self.metadata)]\n        find_coms_primary = mock.MagicMock(return_value=pri_comments)\n        find_coms_secondary = mock.MagicMock(return_value=sec_comments)\n        self.block.primary.find_comments = find_coms_primary\n        self.block.secondary.find_comments = find_coms_secondary\n\n        dcoms = self.block.find_comments(\"comment\")\n        p_dcoms = [d.primary for d in dcoms]\n        s_dcoms = [d.secondary for d in dcoms]\n        p_coms = self.block.primary.find_comments(\"comment\")\n        s_coms = self.block.secondary.find_comments(\"comment\")\n        # Check that every comment response is represented in the list of\n        # DualParserNode instances.\n        for p in p_dcoms:\n            assert p in p_coms\n        for s in s_dcoms:\n            assert s in s_coms\n\n    def test_find_blocks_first_passing(self):\n        youshallnotpass = [augeasparser.AugeasBlockNode(name=\"notpassing\",\n                                                        ancestor=self.block,\n                                                        filepath=\"/path/to/whatever\",\n                                                        metadata=self.metadata)]\n        youshallpass = [augeasparser.AugeasBlockNode(name=assertions.PASS,\n                                                     ancestor=self.block,\n                                                     filepath=assertions.PASS,\n                                                     metadata=self.metadata)]\n        find_blocks_primary = mock.MagicMock(return_value=youshallpass)\n        find_blocks_secondary = mock.MagicMock(return_value=youshallnotpass)\n        self.block.primary.find_blocks = find_blocks_primary\n        self.block.secondary.find_blocks = find_blocks_secondary\n\n        blocks = self.block.find_blocks(\"something\")\n        for block in blocks:\n            try:\n                assertions.assertEqual(block.primary, block.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassDirective(block.primary) is True\n            assert assertions.isPassDirective(block.secondary) is False\n\n    def test_find_blocks_second_passing(self):\n        youshallnotpass = [augeasparser.AugeasBlockNode(name=\"notpassing\",\n                                                        ancestor=self.block,\n                                                        filepath=\"/path/to/whatever\",\n                                                        metadata=self.metadata)]\n        youshallpass = [augeasparser.AugeasBlockNode(name=assertions.PASS,\n                                                     ancestor=self.block,\n                                                     filepath=assertions.PASS,\n                                                     metadata=self.metadata)]\n        find_blocks_primary = mock.MagicMock(return_value=youshallnotpass)\n        find_blocks_secondary = mock.MagicMock(return_value=youshallpass)\n        self.block.primary.find_blocks = find_blocks_primary\n        self.block.secondary.find_blocks = find_blocks_secondary\n\n        blocks = self.block.find_blocks(\"something\")\n        for block in blocks:\n            try:\n                assertions.assertEqual(block.primary, block.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassDirective(block.primary) is False\n            assert assertions.isPassDirective(block.secondary) is True\n\n    def test_find_dirs_first_passing(self):\n        notpassing = [augeasparser.AugeasDirectiveNode(name=\"notpassing\",\n                                                       ancestor=self.block,\n                                                       filepath=\"/path/to/whatever\",\n                                                       metadata=self.metadata)]\n        passing = [augeasparser.AugeasDirectiveNode(name=assertions.PASS,\n                                                    ancestor=self.block,\n                                                    filepath=assertions.PASS,\n                                                    metadata=self.metadata)]\n        find_dirs_primary = mock.MagicMock(return_value=passing)\n        find_dirs_secondary = mock.MagicMock(return_value=notpassing)\n        self.block.primary.find_directives = find_dirs_primary\n        self.block.secondary.find_directives = find_dirs_secondary\n\n        directives = self.block.find_directives(\"something\")\n        for directive in directives:\n            try:\n                assertions.assertEqual(directive.primary, directive.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassDirective(directive.primary) is True\n            assert assertions.isPassDirective(directive.secondary) is False\n\n    def test_find_dirs_second_passing(self):\n        notpassing = [augeasparser.AugeasDirectiveNode(name=\"notpassing\",\n                                                       ancestor=self.block,\n                                                       filepath=\"/path/to/whatever\",\n                                                       metadata=self.metadata)]\n        passing = [augeasparser.AugeasDirectiveNode(name=assertions.PASS,\n                                                    ancestor=self.block,\n                                                    filepath=assertions.PASS,\n                                                    metadata=self.metadata)]\n        find_dirs_primary = mock.MagicMock(return_value=notpassing)\n        find_dirs_secondary = mock.MagicMock(return_value=passing)\n        self.block.primary.find_directives = find_dirs_primary\n        self.block.secondary.find_directives = find_dirs_secondary\n\n        directives = self.block.find_directives(\"something\")\n        for directive in directives:\n            try:\n                assertions.assertEqual(directive.primary, directive.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassDirective(directive.primary) is False\n            assert assertions.isPassDirective(directive.secondary) is True\n\n    def test_find_coms_first_passing(self):\n        notpassing = [augeasparser.AugeasCommentNode(comment=\"notpassing\",\n                                                     ancestor=self.block,\n                                                     filepath=\"/path/to/whatever\",\n                                                     metadata=self.metadata)]\n        passing = [augeasparser.AugeasCommentNode(comment=assertions.PASS,\n                                                  ancestor=self.block,\n                                                  filepath=assertions.PASS,\n                                                  metadata=self.metadata)]\n        find_coms_primary = mock.MagicMock(return_value=passing)\n        find_coms_secondary = mock.MagicMock(return_value=notpassing)\n        self.block.primary.find_comments = find_coms_primary\n        self.block.secondary.find_comments = find_coms_secondary\n\n        comments = self.block.find_comments(\"something\")\n        for comment in comments:\n            try:\n                assertions.assertEqual(comment.primary, comment.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassComment(comment.primary) is True\n            assert assertions.isPassComment(comment.secondary) is False\n\n    def test_find_coms_second_passing(self):\n        notpassing = [augeasparser.AugeasCommentNode(comment=\"notpassing\",\n                                                     ancestor=self.block,\n                                                     filepath=\"/path/to/whatever\",\n                                                     metadata=self.metadata)]\n        passing = [augeasparser.AugeasCommentNode(comment=assertions.PASS,\n                                                  ancestor=self.block,\n                                                  filepath=assertions.PASS,\n                                                  metadata=self.metadata)]\n        find_coms_primary = mock.MagicMock(return_value=notpassing)\n        find_coms_secondary = mock.MagicMock(return_value=passing)\n        self.block.primary.find_comments = find_coms_primary\n        self.block.secondary.find_comments = find_coms_secondary\n\n        comments = self.block.find_comments(\"something\")\n        for comment in comments:\n            try:\n                assertions.assertEqual(comment.primary, comment.secondary)\n            except AssertionError: # pragma: no cover\n                self.fail(\"Assertion should have passed\")\n            assert assertions.isPassComment(comment.primary) is False\n            assert assertions.isPassComment(comment.secondary) is True\n\n    def test_find_blocks_no_pass_equal(self):\n        notpassing1 = [augeasparser.AugeasBlockNode(name=\"notpassing\",\n                                                    ancestor=self.block,\n                                                    filepath=\"/path/to/whatever\",\n                                                    metadata=self.metadata)]\n        notpassing2 = [augeasparser.AugeasBlockNode(name=\"notpassing\",\n                                                    ancestor=self.block,\n                                                    filepath=\"/path/to/whatever\",\n                                                    metadata=self.metadata)]\n        find_blocks_primary = mock.MagicMock(return_value=notpassing1)\n        find_blocks_secondary = mock.MagicMock(return_value=notpassing2)\n        self.block.primary.find_blocks = find_blocks_primary\n        self.block.secondary.find_blocks = find_blocks_secondary\n\n        blocks = self.block.find_blocks(\"anything\")\n        assert len(blocks) == 1\n        for block in blocks:\n            assert block.primary == block.secondary\n            assert block.primary is not block.secondary\n\n    def test_find_dirs_no_pass_equal(self):\n        notpassing1 = [augeasparser.AugeasDirectiveNode(name=\"notpassing\",\n                                                        ancestor=self.block,\n                                                        filepath=\"/path/to/whatever\",\n                                                        metadata=self.metadata)]\n        notpassing2 = [augeasparser.AugeasDirectiveNode(name=\"notpassing\",\n                                                        ancestor=self.block,\n                                                        filepath=\"/path/to/whatever\",\n                                                        metadata=self.metadata)]\n        find_dirs_primary = mock.MagicMock(return_value=notpassing1)\n        find_dirs_secondary = mock.MagicMock(return_value=notpassing2)\n        self.block.primary.find_directives = find_dirs_primary\n        self.block.secondary.find_directives = find_dirs_secondary\n\n        directives = self.block.find_directives(\"anything\")\n        assert len(directives) == 1\n        for directive in directives:\n            assert directive.primary == directive.secondary\n            assert directive.primary is not directive.secondary\n\n    def test_find_comments_no_pass_equal(self):\n        notpassing1 = [augeasparser.AugeasCommentNode(comment=\"notpassing\",\n                                                      ancestor=self.block,\n                                                      filepath=\"/path/to/whatever\",\n                                                      metadata=self.metadata)]\n        notpassing2 = [augeasparser.AugeasCommentNode(comment=\"notpassing\",\n                                                      ancestor=self.block,\n                                                      filepath=\"/path/to/whatever\",\n                                                      metadata=self.metadata)]\n        find_coms_primary = mock.MagicMock(return_value=notpassing1)\n        find_coms_secondary = mock.MagicMock(return_value=notpassing2)\n        self.block.primary.find_comments = find_coms_primary\n        self.block.secondary.find_comments = find_coms_secondary\n\n        comments = self.block.find_comments(\"anything\")\n        assert len(comments) == 1\n        for comment in comments:\n            assert comment.primary == comment.secondary\n            assert comment.primary is not comment.secondary\n\n    def test_find_blocks_no_pass_notequal(self):\n        notpassing1 = [augeasparser.AugeasBlockNode(name=\"notpassing\",\n                                                    ancestor=self.block,\n                                                    filepath=\"/path/to/whatever\",\n                                                    metadata=self.metadata)]\n        notpassing2 = [augeasparser.AugeasBlockNode(name=\"different\",\n                                                    ancestor=self.block,\n                                                    filepath=\"/path/to/whatever\",\n                                                    metadata=self.metadata)]\n        find_blocks_primary = mock.MagicMock(return_value=notpassing1)\n        find_blocks_secondary = mock.MagicMock(return_value=notpassing2)\n        self.block.primary.find_blocks = find_blocks_primary\n        self.block.secondary.find_blocks = find_blocks_secondary\n\n        with pytest.raises(AssertionError):\n            _ = self.block.find_blocks(\"anything\")\n\n    def test_parsernode_notequal(self):\n        ne_block = augeasparser.AugeasBlockNode(name=\"different\",\n                                                ancestor=self.block,\n                                                filepath=\"/path/to/whatever\",\n                                                metadata=self.metadata)\n        ne_directive = augeasparser.AugeasDirectiveNode(name=\"different\",\n                                                        ancestor=self.block,\n                                                        filepath=\"/path/to/whatever\",\n                                                        metadata=self.metadata)\n        ne_comment = augeasparser.AugeasCommentNode(comment=\"different\",\n                                                    ancestor=self.block,\n                                                    filepath=\"/path/to/whatever\",\n                                                    metadata=self.metadata)\n        assert self.block != ne_block\n        assert self.directive != ne_directive\n        assert self.comment != ne_comment\n\n    def test_parsed_paths(self):\n        mock_p = mock.MagicMock(return_value=['/path/file.conf',\n                                              '/another/path',\n                                              '/path/other.conf'])\n        mock_s = mock.MagicMock(return_value=['/path/*.conf', '/another/path'])\n        self.block.primary.parsed_paths = mock_p\n        self.block.secondary.parsed_paths = mock_s\n        self.block.parsed_paths()\n        assert mock_p.called is True\n        assert mock_s.called is True\n\n    def test_parsed_paths_error(self):\n        mock_p = mock.MagicMock(return_value=['/path/file.conf'])\n        mock_s = mock.MagicMock(return_value=['/path/*.conf', '/another/path'])\n        self.block.primary.parsed_paths = mock_p\n        self.block.secondary.parsed_paths = mock_s\n        with pytest.raises(AssertionError):\n            self.block.parsed_paths()\n\n    def test_find_ancestors(self):\n        primarymock = mock.MagicMock(return_value=[])\n        secondarymock = mock.MagicMock(return_value=[])\n        self.block.primary.find_ancestors = primarymock\n        self.block.secondary.find_ancestors = secondarymock\n        self.block.find_ancestors(\"anything\")\n        assert primarymock.called is True\n        assert secondarymock.called is True\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/entrypoint_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.entrypoint for override class resolution\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import entrypoint\n\n\ndef test_get_configurator():\n    with mock.patch(\"certbot.util.get_os_info\") as mock_info:\n        for distro in entrypoint.OVERRIDE_CLASSES:\n            return_value = (distro, \"whatever\")\n            if distro == 'fedora_old':\n                return_value = ('fedora', '28')\n            elif distro == 'fedora':\n                return_value = ('fedora', '29')\n            mock_info.return_value = return_value\n            assert entrypoint.get_configurator() == \\\n                             entrypoint.OVERRIDE_CLASSES[distro]\n\ndef test_nonexistent_like():\n    with mock.patch(\"certbot.util.get_os_info\") as mock_info:\n        mock_info.return_value = (\"nonexistent\", \"irrelevant\")\n        with mock.patch(\"certbot.util.get_systemd_os_like\") as mock_like:\n            for like in entrypoint.OVERRIDE_CLASSES:\n                mock_like.return_value = [like]\n                assert entrypoint.get_configurator() == \\\n                                 entrypoint.OVERRIDE_CLASSES[like]\n\ndef test_nonexistent_generic():\n    with mock.patch(\"certbot.util.get_os_info\") as mock_info:\n        mock_info.return_value = (\"nonexistent\", \"irrelevant\")\n        with mock.patch(\"certbot.util.get_systemd_os_like\") as mock_like:\n            mock_like.return_value = [\"unknown\"]\n            assert entrypoint.get_configurator() == \\\n                             configurator.ApacheConfigurator\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/fedora_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.configurator for Fedora 29+ overrides\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal import override_fedora\nfrom certbot_apache._internal.tests import util\n\n\ndef get_vh_truth(temp_dir, config_name):\n    \"\"\"Return the ground truth for the specified directory.\"\"\"\n    prefix = os.path.join(\n        temp_dir, config_name, \"httpd/conf.d\")\n\n    aug_pre = \"/files\" + prefix\n    # TODO: eventually, these tests should have a dedicated configuration instead\n    #  of reusing the ones from centos_test\n    vh_truth = [\n        obj.VirtualHost(\n            os.path.join(prefix, \"centos.example.com.conf\"),\n            os.path.join(aug_pre, \"centos.example.com.conf/VirtualHost\"),\n            {obj.Addr.fromstring(\"*:80\")},\n            False, True, \"centos.example.com\"),\n        obj.VirtualHost(\n            os.path.join(prefix, \"ssl.conf\"),\n            os.path.join(aug_pre, \"ssl.conf/VirtualHost\"),\n            {obj.Addr.fromstring(\"_default_:443\")},\n            True, True, None)\n    ]\n    return vh_truth\n\n\nclass FedoraRestartTest(util.ApacheTest):\n    \"\"\"Tests for Fedora specific self-signed certificate override\"\"\"\n\n    # TODO: eventually, these tests should have a dedicated configuration instead\n    #  of reusing the ones from centos_test\n    def setUp(self):  # pylint: disable=arguments-differ\n        test_dir = \"centos7_apache/apache\"\n        config_root = \"centos7_apache/apache/httpd\"\n        vhost_root = \"centos7_apache/apache/httpd/conf.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n            os_info=\"fedora\")\n        self.vh_truth = get_vh_truth(\n            self.temp_dir, \"centos7_apache/apache\")\n\n    def _run_fedora_test(self):\n        assert isinstance(self.config, override_fedora.FedoraConfigurator)\n        self.config.config_test()\n\n    def test_fedora_restart_error(self):\n        c_test = \"certbot_apache._internal.configurator.ApacheConfigurator.config_test\"\n        with mock.patch(c_test) as mock_test:\n            # First call raises error, second doesn't\n            mock_test.side_effect = [errors.MisconfigurationError, '']\n            with mock.patch(\"certbot.util.run_script\") as mock_run:\n                mock_run.side_effect = errors.SubprocessError\n                with pytest.raises(errors.MisconfigurationError):\n                    self._run_fedora_test()\n\n    def test_fedora_restart(self):\n        c_test = \"certbot_apache._internal.configurator.ApacheConfigurator.config_test\"\n        with mock.patch(c_test) as mock_test:\n            with mock.patch(\"certbot.util.run_script\") as mock_run:\n                # First call raises error, second doesn't\n                mock_test.side_effect = [errors.MisconfigurationError, '']\n                self._run_fedora_test()\n                assert mock_test.call_count == 2\n                assert mock_run.call_args[0][0] == \\\n                                ['systemctl', 'restart', 'httpd']\n\n\nclass MultipleVhostsTestFedora(util.ApacheTest):\n    \"\"\"Multiple vhost tests for CentOS / RHEL family of distros\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        test_dir = \"centos7_apache/apache\"\n        config_root = \"centos7_apache/apache/httpd\"\n        vhost_root = \"centos7_apache/apache/httpd/conf.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n            os_info=\"fedora\")\n        self.vh_truth = get_vh_truth(\n            self.temp_dir, \"centos7_apache/apache\")\n\n    def test_get_parser(self):\n        assert isinstance(self.config.parser, override_fedora.FedoraParser)\n\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_opportunistic_httpd_runtime_parsing(self, mock_get):\n        define_val = (\n            'Define: TEST1\\n'\n            'Define: TEST2\\n'\n            'Define: DUMP_RUN_CFG\\n'\n        )\n        mod_val = (\n            'Loaded Modules:\\n'\n            ' mock_module (static)\\n'\n            ' another_module (static)\\n'\n        )\n        def mock_get_cfg(command):\n            \"\"\"Mock httpd process stdout\"\"\"\n            if command == ['httpd', '-t', '-D', 'DUMP_RUN_CFG']:\n                return define_val\n            elif command == ['httpd', '-t', '-D', 'DUMP_MODULES']:\n                return mod_val\n            return \"\"\n        mock_get.side_effect = mock_get_cfg\n        self.config.parser.modules = {}\n        self.config.parser.variables = {}\n\n        with mock.patch(\"certbot.util.get_os_info\") as mock_osi:\n            # Make sure we have the have the CentOS httpd constants\n            mock_osi.return_value = (\"fedora\", \"29\")\n            self.config.parser.update_runtime_variables()\n\n        assert mock_get.call_count == 3\n        assert len(self.config.parser.modules) == 4\n        assert len(self.config.parser.variables) == 2\n        assert \"TEST2\" in self.config.parser.variables\n        assert \"mod_another.c\" in self.config.parser.modules\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_get_version(self, mock_run_script):\n        mock_run_script.return_value = ('', None)\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n        assert mock_run_script.call_args[0][0][0] == 'httpd'\n\n    def test_get_virtual_hosts(self):\n        \"\"\"Make sure all vhosts are being properly found.\"\"\"\n        vhs = self.config.get_virtual_hosts()\n        assert len(vhs) == 2\n        found = 0\n\n        for vhost in vhs:\n            for centos_truth in self.vh_truth:\n                if vhost == centos_truth:\n                    found += 1\n                    break\n            else:\n                raise Exception(\"Missed: %s\" % vhost)  # pragma: no cover\n        assert found == 2\n\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_get_sysconfig_vars(self, mock_cfg):\n        \"\"\"Make sure we read the sysconfig OPTIONS variable correctly\"\"\"\n        # Return nothing for the process calls\n        mock_cfg.return_value = \"\"\n        self.config.parser.sysconfig_filep = filesystem.realpath(\n            os.path.join(self.config.parser.root, \"../sysconfig/httpd\"))\n        self.config.parser.variables = {}\n\n        with mock.patch(\"certbot.util.get_os_info\") as mock_osi:\n            # Make sure we have the have the CentOS httpd constants\n            mock_osi.return_value = (\"fedora\", \"29\")\n            self.config.parser.update_runtime_variables()\n\n        assert \"mock_define\" in self.config.parser.variables\n        assert \"mock_define_too\" in self.config.parser.variables\n        assert \"mock_value\" in self.config.parser.variables\n        assert \"TRUE\" == self.config.parser.variables[\"mock_value\"]\n        assert \"MOCK_NOSEP\" in self.config.parser.variables\n        assert \"NOSEP_VAL\" == self.config.parser.variables[\"NOSEP_TWO\"]\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_alt_restart_works(self, mock_run_script):\n        mock_run_script.side_effect = [None, errors.SubprocessError, None]\n        self.config.restart()\n        assert mock_run_script.call_count == 3\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_alt_restart_errors(self, mock_run_script):\n        mock_run_script.side_effect = [None,\n                                       errors.SubprocessError,\n                                       errors.SubprocessError]\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.restart()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/gentoo_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.configurator for Gentoo overrides\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot_apache._internal import obj\nfrom certbot_apache._internal import override_gentoo\nfrom certbot_apache._internal.tests import util\n\n\ndef get_vh_truth(temp_dir, config_name):\n    \"\"\"Return the ground truth for the specified directory.\"\"\"\n    prefix = os.path.join(\n        temp_dir, config_name, \"apache2/vhosts.d\")\n\n    aug_pre = \"/files\" + prefix\n    vh_truth = [\n        obj.VirtualHost(\n            os.path.join(prefix, \"gentoo.example.com.conf\"),\n            os.path.join(aug_pre, \"gentoo.example.com.conf/VirtualHost\"),\n            {obj.Addr.fromstring(\"*:80\")},\n            False, True, \"gentoo.example.com\"),\n        obj.VirtualHost(\n            os.path.join(prefix, \"00_default_vhost.conf\"),\n            os.path.join(aug_pre, \"00_default_vhost.conf/IfDefine/VirtualHost\"),\n            {obj.Addr.fromstring(\"*:80\")},\n            False, True, \"localhost\"),\n        obj.VirtualHost(\n            os.path.join(prefix, \"00_default_ssl_vhost.conf\"),\n            os.path.join(aug_pre,\n                         \"00_default_ssl_vhost.conf\" +\n                         \"/IfDefine/IfDefine/IfModule/VirtualHost\"),\n            {obj.Addr.fromstring(\"_default_:443\")},\n            True, True, \"localhost\")\n    ]\n    return vh_truth\n\nclass MultipleVhostsTestGentoo(util.ApacheTest):\n    \"\"\"Multiple vhost tests for non-debian distro\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        test_dir = \"gentoo_apache/apache\"\n        config_root = \"gentoo_apache/apache/apache2\"\n        vhost_root = \"gentoo_apache/apache/apache2/vhosts.d\"\n        super().setUp(test_dir=test_dir,\n                      config_root=config_root,\n                      vhost_root=vhost_root)\n\n        # pylint: disable=line-too-long\n        with mock.patch(\"certbot_apache._internal.override_gentoo.GentooParser.update_runtime_variables\"):\n            self.config = util.get_apache_configurator(\n                self.config_path, self.vhost_path, self.config_dir, self.work_dir,\n                os_info=\"gentoo\")\n        self.vh_truth = get_vh_truth(\n            self.temp_dir, \"gentoo_apache/apache\")\n\n    def test_get_parser(self):\n        assert isinstance(self.config.parser, override_gentoo.GentooParser)\n\n    def test_get_virtual_hosts(self):\n        \"\"\"Make sure all vhosts are being properly found.\"\"\"\n        vhs = self.config.get_virtual_hosts()\n        assert len(vhs) == 3\n        found = 0\n\n        for vhost in vhs:\n            for gentoo_truth in self.vh_truth:\n                if vhost == gentoo_truth:\n                    found += 1\n                    break\n            else:\n                raise Exception(\"Missed: %s\" % vhost)  # pragma: no cover\n        assert found == 3\n\n    def test_get_sysconfig_vars(self):\n        \"\"\"Make sure we read the Gentoo APACHE2_OPTS variable correctly\"\"\"\n        defines = ['DEFAULT_VHOST', 'INFO',\n                   'SSL', 'SSL_DEFAULT_VHOST', 'LANGUAGE']\n        self.config.parser.apacheconfig_filep = filesystem.realpath(\n            os.path.join(self.config.parser.root, \"../conf.d/apache2\"))\n        self.config.parser.variables = {}\n        with mock.patch(\"certbot_apache._internal.override_gentoo.GentooParser.update_modules\"):\n            self.config.parser.update_runtime_variables()\n        for define in defines:\n            assert define in self.config.parser.variables\n\n    @mock.patch(\"certbot_apache._internal.apache_util.parse_from_subprocess\")\n    def test_no_binary_configdump(self, mock_subprocess):\n        \"\"\"Make sure we don't call binary dumps other than modules from Apache\n        as this is not supported in Gentoo currently\"\"\"\n\n        with mock.patch(\"certbot_apache._internal.override_gentoo.GentooParser.update_modules\"):\n            self.config.parser.update_runtime_variables()\n            self.config.parser.reset_modules()\n        assert mock_subprocess.called is False\n\n        self.config.parser.update_runtime_variables()\n        self.config.parser.reset_modules()\n        assert mock_subprocess.called is True\n\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_opportunistic_httpd_runtime_parsing(self, mock_get):\n        mod_val = (\n            'Loaded Modules:\\n'\n            ' mock_module (static)\\n'\n            ' another_module (static)\\n'\n        )\n        def mock_get_cfg(command):\n            \"\"\"Mock httpd process stdout\"\"\"\n            if command == ['apache2ctl', 'modules']:\n                return mod_val\n            return None  # pragma: no cover\n        mock_get.side_effect = mock_get_cfg\n        self.config.parser.modules = {}\n\n        with mock.patch(\"certbot.util.get_os_info\") as mock_osi:\n            # Make sure we have the have the Gentoo httpd constants\n            mock_osi.return_value = (\"gentoo\", \"123\")\n            self.config.parser.update_runtime_variables()\n\n        assert mock_get.call_count == 1\n        assert len(self.config.parser.modules) == 4\n        assert \"mod_another.c\" in self.config.parser.modules\n\n    @mock.patch(\"certbot_apache._internal.configurator.util.run_script\")\n    def test_alt_restart_works(self, mock_run_script):\n        mock_run_script.side_effect = [None, errors.SubprocessError, None]\n        self.config.restart()\n        assert mock_run_script.call_count == 3\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/http_01_test.py",
    "content": "\"\"\"Test for certbot_apache._internal.http_01.\"\"\"\nimport errno\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot.compat import filesystem\nfrom certbot.compat import os\nfrom certbot.tests import acme_util\nfrom certbot_apache._internal.parser import get_aug_path\nfrom certbot_apache._internal.tests import util\n\nNUM_ACHALLS = 3\n\n\nclass ApacheHttp01Test(util.ApacheTest):\n    \"\"\"Test for certbot_apache._internal.http_01.ApacheHttp01.\"\"\"\n\n    def setUp(self, *args, **kwargs):  # pylint: disable=arguments-differ\n        super().setUp(*args, **kwargs)\n\n        self.account_key = self.rsa512jwk\n        self.achalls: list[achallenges.KeyAuthorizationAnnotatedChallenge] = []\n        vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n        # Takes the vhosts for encryption-example.demo, certbot.demo\n        # and vhost.in.rootconf\n        self.vhosts = [vh_truth[0], vh_truth[3], vh_truth[10]]\n\n        for i in range(NUM_ACHALLS):\n            self.achalls.append(\n                achallenges.KeyAuthorizationAnnotatedChallenge(\n                    challb=acme_util.chall_to_challb(\n                        challenges.HTTP01(token=((chr(ord('a') + i).encode() * 16))),\n                        \"pending\"),\n                    identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=self.vhosts[i].name),\n                    account_key=self.account_key))\n\n        modules = [\"ssl\", \"rewrite\", \"authz_core\", \"authz_host\"]\n        for mod in modules:\n            self.config.parser.modules[\"mod_{0}.c\".format(mod)] = None\n            self.config.parser.modules[mod + \"_module\"] = None\n\n        from certbot_apache._internal.http_01 import ApacheHttp01\n        self.http = ApacheHttp01(self.config)\n\n    def test_empty_perform(self):\n        assert len(self.http.perform()) == 0\n\n    def test_ip_address_perform(self):\n        self.http.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_IP, value=\"127.0.0.1\"),\n                account_key=self.account_key)]\n        with pytest.raises(errors.ConfigurationError):\n            self.http.perform()\n\n    @mock.patch(\"certbot_apache._internal.configurator.ApacheConfigurator.enable_mod\")\n    def test_enable_modules_apache_2_4(self, mock_enmod):\n        del self.config.parser.modules[\"authz_core_module\"]\n        del self.config.parser.modules[\"mod_authz_host.c\"]\n\n        enmod_calls = self.common_enable_modules_test(mock_enmod)\n        assert enmod_calls[0][0][0] == \"authz_core\"\n\n    def common_enable_modules_test(self, mock_enmod):\n        \"\"\"Tests enabling mod_rewrite and other modules.\"\"\"\n        del self.config.parser.modules[\"rewrite_module\"]\n        del self.config.parser.modules[\"mod_rewrite.c\"]\n\n        self.http.prepare_http01_modules()\n\n        assert mock_enmod.called is True\n        calls = mock_enmod.call_args_list\n        other_calls = []\n        for call in calls:\n            if call[0][0] != \"rewrite\":\n                other_calls.append(call)\n\n        # If these lists are equal, we never enabled mod_rewrite\n        assert calls != other_calls\n        return other_calls\n\n    def test_same_vhost(self):\n        vhost = next(v for v in self.config.vhosts if v.name == \"certbot.demo\")\n        achalls = [\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=vhost.name),\n                account_key=self.account_key),\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'b' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=next(iter(vhost.aliases))),\n                account_key=self.account_key)\n        ]\n        self.common_perform_test(achalls, [vhost])\n\n    def test_anonymous_vhost(self):\n        vhosts = [v for v in self.config.vhosts if not v.ssl]\n        achalls = [\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"something.nonexistent\"),\n                account_key=self.account_key)]\n        self.common_perform_test(achalls, vhosts)\n\n    def test_configure_multiple_vhosts(self):\n        vhosts = [v for v in self.config.vhosts if \"duplicate.example.com\" in v.get_names()]\n        assert len(vhosts) == 2\n        achalls = [\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"duplicate.example.com\"),\n                account_key=self.account_key)]\n        self.common_perform_test(achalls, vhosts)\n\n    def test_configure_name_and_blank(self):\n        domain = \"certbot.demo\"\n        vhosts = [v for v in self.config.vhosts if v.name == domain or v.name is None]\n        achalls = [\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=domain),\n                account_key=self.account_key),\n        ]\n        self.common_perform_test(achalls, vhosts)\n\n    def test_no_vhost(self):\n        for achall in self.achalls:\n            self.http.add_chall(achall)\n        self.config.config.http01_port = 12345\n        with pytest.raises(errors.PluginError):\n            self.http.perform()\n\n    def test_perform_1_achall_apache_2_4(self):\n        self.combinations_perform_test(num_achalls=1, minor_version=4)\n\n    def test_perform_2_achall_apache_2_4(self):\n        self.combinations_perform_test(num_achalls=2, minor_version=4)\n\n    def test_perform_3_achall_apache_2_4(self):\n        self.combinations_perform_test(num_achalls=3, minor_version=4)\n\n    def test_activate_disabled_vhost(self):\n        vhosts = [v for v in self.config.vhosts if v.name == \"certbot.demo\"]\n        achalls = [\n            achallenges.KeyAuthorizationAnnotatedChallenge(\n                challb=acme_util.chall_to_challb(\n                    challenges.HTTP01(token=((b'a' * 16))),\n                    \"pending\"),\n                identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"certbot.demo\"),\n                account_key=self.account_key)]\n        vhosts[0].enabled = False\n        self.common_perform_test(achalls, vhosts)\n        matches = self.config.parser.find_dir(\n            \"Include\", vhosts[0].filep,\n            get_aug_path(self.config.parser.loc[\"default\"]))\n        assert len(matches) == 1\n\n    def combinations_perform_test(self, num_achalls, minor_version):\n        \"\"\"Test perform with the given achall count and Apache version.\"\"\"\n        achalls = self.achalls[:num_achalls]\n        vhosts = self.vhosts[:num_achalls]\n        self.config.version = (2, minor_version)\n        self.common_perform_test(achalls, vhosts)\n\n    def common_perform_test(self, achalls, vhosts):\n        \"\"\"Tests perform with the given achalls.\"\"\"\n        challenge_dir = self.http.challenge_dir\n        assert os.path.exists(challenge_dir) is False\n        for achall in achalls:\n            self.http.add_chall(achall)\n\n        expected_response = [\n            achall.response(self.account_key) for achall in achalls]\n        assert self.http.perform() == expected_response\n\n        assert os.path.isdir(self.http.challenge_dir) is True\n        assert filesystem.has_min_permissions(self.http.challenge_dir, 0o755) is True\n        self._test_challenge_conf()\n\n        for achall in achalls:\n            self._test_challenge_file(achall)\n\n        for vhost in vhosts:\n            matches = self.config.parser.find_dir(\"Include\",\n                                                self.http.challenge_conf_pre,\n                                                vhost.path)\n            assert len(matches) == 1\n            matches = self.config.parser.find_dir(\"Include\",\n                                                self.http.challenge_conf_post,\n                                                vhost.path)\n            assert len(matches) == 1\n\n        assert os.path.exists(challenge_dir) is True\n\n    @mock.patch(\"certbot_apache._internal.http_01.filesystem.makedirs\")\n    def test_failed_makedirs(self, mock_makedirs):\n        mock_makedirs.side_effect = OSError(errno.EACCES, \"msg\")\n        self.http.add_chall(self.achalls[0])\n        with pytest.raises(errors.PluginError):\n            self.http.perform()\n\n    def _test_challenge_conf(self):\n        with open(self.http.challenge_conf_pre) as f:\n            pre_conf_contents = f.read()\n\n        with open(self.http.challenge_conf_post) as f:\n            post_conf_contents = f.read()\n\n        assert \"RewriteEngine on\" in post_conf_contents\n        assert \"RewriteRule\" in pre_conf_contents\n\n        assert self.http.challenge_dir in post_conf_contents\n        assert \"Require all granted\" in post_conf_contents\n\n    def _test_challenge_file(self, achall):\n        name = os.path.join(self.http.challenge_dir, achall.chall.encode(\"token\"))\n        validation = achall.validation(self.account_key)\n\n        assert filesystem.has_min_permissions(name, 0o644) is True\n        with open(name, 'rb') as f:\n            assert f.read() == validation.encode()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/obj_test.py",
    "content": "\"\"\"Tests for certbot_apache._internal.obj.\"\"\"\nimport sys\nimport unittest\n\nimport pytest\n\n\nclass VirtualHostTest(unittest.TestCase):\n    \"\"\"Test the VirtualHost class.\"\"\"\n\n    def setUp(self):\n        from certbot_apache._internal.obj import Addr\n        from certbot_apache._internal.obj import VirtualHost\n\n        self.addr1 = Addr.fromstring(\"127.0.0.1\")\n        self.addr2 = Addr.fromstring(\"127.0.0.1:443\")\n        self.addr_default = Addr.fromstring(\"_default_:443\")\n\n        self.vhost1 = VirtualHost(\n            \"filep\", \"vh_path\", {self.addr1}, False, False, \"localhost\")\n\n        self.vhost1b = VirtualHost(\n            \"filep\", \"vh_path\", {self.addr1}, False, False, \"localhost\")\n\n        self.vhost2 = VirtualHost(\n            \"fp\", \"vhp\", {self.addr2}, False, False, \"localhost\")\n\n    def test_repr(self):\n        assert repr(self.addr2) == \\\n            \"certbot_apache._internal.obj.Addr(('127.0.0.1', '443'))\"\n\n    def test_eq(self):\n        assert self.vhost1b == self.vhost1\n        assert self.vhost1 != self.vhost2\n        assert str(self.vhost1b) == str(self.vhost1)\n        assert self.vhost1b != 1234\n\n    def test_ne(self):\n        assert self.vhost1 != self.vhost2\n        assert self.vhost1 == self.vhost1b\n\n    def test_conflicts(self):\n        from certbot_apache._internal.obj import Addr\n        from certbot_apache._internal.obj import VirtualHost\n\n        complex_vh = VirtualHost(\n            \"fp\", \"vhp\",\n            {Addr.fromstring(\"*:443\"), Addr.fromstring(\"1.2.3.4:443\")},\n            False, False)\n        assert complex_vh.conflicts([self.addr1]) is True\n        assert complex_vh.conflicts([self.addr2]) is True\n        assert complex_vh.conflicts([self.addr_default]) is False\n\n        assert self.vhost1.conflicts([self.addr2]) is True\n        assert self.vhost1.conflicts([self.addr_default]) is False\n\n        assert self.vhost2.conflicts([self.addr1, self.addr_default]) is False\n\n    def test_same_server(self):\n        from certbot_apache._internal.obj import VirtualHost\n        no_name1 = VirtualHost(\n            \"fp\", \"vhp\", {self.addr1}, False, False, None)\n        no_name2 = VirtualHost(\n            \"fp\", \"vhp\", {self.addr2}, False, False, None)\n        no_name3 = VirtualHost(\n            \"fp\", \"vhp\", {self.addr_default},\n            False, False, None)\n        no_name4 = VirtualHost(\n            \"fp\", \"vhp\", {self.addr2, self.addr_default},\n            False, False, None)\n\n        assert self.vhost1.same_server(self.vhost2) is True\n        assert no_name1.same_server(no_name2) is True\n\n        assert self.vhost1.same_server(no_name1) is False\n        assert no_name1.same_server(no_name3) is False\n        assert no_name1.same_server(no_name4) is False\n\n\nclass AddrTest(unittest.TestCase):\n    \"\"\"Test obj.Addr.\"\"\"\n    def setUp(self):\n        from certbot_apache._internal.obj import Addr\n        self.addr = Addr.fromstring(\"*:443\")\n\n        self.addr1 = Addr.fromstring(\"127.0.0.1\")\n        self.addr2 = Addr.fromstring(\"127.0.0.1:*\")\n\n        self.addr_defined = Addr.fromstring(\"127.0.0.1:443\")\n        self.addr_default = Addr.fromstring(\"_default_:443\")\n\n    def test_wildcard(self):\n        assert self.addr.is_wildcard() is False\n        assert self.addr1.is_wildcard() is True\n        assert self.addr2.is_wildcard() is True\n\n    def test_get_sni_addr(self):\n        from certbot_apache._internal.obj import Addr\n        assert self.addr.get_sni_addr(\"443\") == Addr.fromstring(\"*:443\")\n        assert self.addr.get_sni_addr(\"225\") == Addr.fromstring(\"*:225\")\n        assert self.addr1.get_sni_addr(\"443\") == Addr.fromstring(\"127.0.0.1\")\n\n    def test_conflicts(self):\n        # Note: Defined IP is more important than defined port in match\n        assert self.addr.conflicts(self.addr1) is True\n        assert self.addr.conflicts(self.addr2) is True\n        assert self.addr.conflicts(self.addr_defined) is True\n        assert self.addr.conflicts(self.addr_default) is False\n\n        assert self.addr1.conflicts(self.addr) is False\n        assert self.addr1.conflicts(self.addr_defined) is True\n        assert self.addr1.conflicts(self.addr_default) is False\n\n        assert self.addr_defined.conflicts(self.addr1) is False\n        assert self.addr_defined.conflicts(self.addr2) is False\n        assert self.addr_defined.conflicts(self.addr) is False\n        assert self.addr_defined.conflicts(self.addr_default) is False\n\n        assert self.addr_default.conflicts(self.addr) is True\n        assert self.addr_default.conflicts(self.addr1) is True\n        assert self.addr_default.conflicts(self.addr_defined) is True\n\n        # Self test\n        assert self.addr.conflicts(self.addr) is True\n        assert self.addr1.conflicts(self.addr1) is True\n        # This is a tricky one...\n        assert self.addr1.conflicts(self.addr2) is True\n\n    def test_equal(self):\n        assert self.addr1 == self.addr2\n        assert self.addr != self.addr1\n        assert self.addr != 123\n\n    def test_not_equal(self):\n        assert self.addr1 == self.addr2\n        assert self.addr != self.addr1\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/parser_test.py",
    "content": "\"\"\"Tests for certbot_apache._internal.parser.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_apache._internal.tests import util\n\n\nclass BasicParserTest(util.ParserTest):\n    \"\"\"Apache Parser Test.\"\"\"\n\n    def test_bad_parse(self):\n        self.parser.parse_file(os.path.join(self.parser.root,\n                                            \"conf-available\", \"bad_conf_file.conf\"))\n        with pytest.raises(errors.PluginError):\n            self.parser.check_parsing_errors(\"httpd.aug\")\n\n    def test_bad_save(self):\n        mock_save = mock.Mock()\n        mock_save.side_effect = IOError\n        self.parser.aug.save = mock_save\n        with pytest.raises(errors.PluginError):\n            self.parser.unsaved_files()\n\n    @mock.patch(\"certbot_apache._internal.parser.logger\")\n    def test_bad_save_errors(self, mock_logger):\n        nx_path = \"/non/existent/path.conf\"\n        self.parser.aug.set(\"/augeas/load/Httpd/incl[last()]\", nx_path)\n        self.parser.add_dir(f\"/files{nx_path}\", \"AddDirective\", \"test\")\n\n        with pytest.raises(IOError):\n            self.parser.save({})\n        mock_logger.error.assert_called_with(\n            'Unable to save files: %s.%s', '/non/existent/path.conf', mock.ANY)\n        mock_logger.debug.assert_called_with(\n            \"Error %s saving %s: %s\", \"mk_augtemp\",\n            \"/non/existent/path.conf\", \"No such file or directory\")\n\n    def test_aug_version(self):\n        mock_match = mock.Mock(return_value=[\"something\"])\n        self.parser.aug.match = mock_match\n        # pylint: disable=protected-access\n        assert self.parser.check_aug_version() == \\\n                         [\"something\"]\n        self.parser.aug.match.side_effect = RuntimeError\n        assert self.parser.check_aug_version() is False\n\n    def test_find_config_root_no_root(self):\n        # pylint: disable=protected-access\n        os.remove(self.parser.loc[\"root\"])\n        with pytest.raises(errors.NoInstallationError):\n            self.parser._find_config_root()\n\n    def test_parse_file(self):\n        \"\"\"Test parse_file.\n\n        certbot.conf is chosen as the test file as it will not be\n        included during the normal course of execution.\n\n        \"\"\"\n        file_path = os.path.join(\n            self.config_path, \"not-parsed-by-default\", \"certbot.conf\")\n\n        self.parser.parse_file(file_path)  # pylint: disable=protected-access\n\n        # search for the httpd incl\n        matches = self.parser.aug.match(\n            \"/augeas/load/Httpd/incl [. ='%s']\" % file_path)\n\n        assert matches\n\n    def test_find_dir(self):\n        test = self.parser.find_dir(\"Listen\", \"80\")\n        # This will only look in enabled hosts\n        test2 = self.parser.find_dir(\"documentroot\")\n\n        assert len(test) == 1\n        assert len(test2) == 8\n\n    def test_add_dir(self):\n        aug_default = \"/files\" + self.parser.loc[\"default\"]\n        self.parser.add_dir(aug_default, \"AddDirective\", \"test\")\n\n        assert self.parser.find_dir(\"AddDirective\", \"test\", aug_default)\n\n        self.parser.add_dir(aug_default, \"AddList\", [\"1\", \"2\", \"3\", \"4\"])\n        matches = self.parser.find_dir(\"AddList\", None, aug_default)\n        for i, match in enumerate(matches):\n            assert self.parser.aug.get(match) == str(i + 1)\n\n    def test_add_dir_beginning(self):\n        aug_default = \"/files\" + self.parser.loc[\"default\"]\n        self.parser.add_dir_beginning(aug_default,\n                                      \"AddDirectiveBeginning\",\n                                      \"testBegin\")\n\n        assert self.parser.find_dir(\"AddDirectiveBeginning\", \"testBegin\", aug_default)\n\n        assert self.parser.aug.get(aug_default+\"/directive[1]\") == \"AddDirectiveBeginning\"\n        self.parser.add_dir_beginning(aug_default, \"AddList\", [\"1\", \"2\", \"3\", \"4\"])\n        matches = self.parser.find_dir(\"AddList\", None, aug_default)\n        for i, match in enumerate(matches):\n            assert self.parser.aug.get(match) == str(i + 1)\n\n        for name in (\"empty.conf\", \"no-directives.conf\"):\n            conf = \"/files\" + os.path.join(self.parser.root, \"sites-available\", name)\n            self.parser.add_dir_beginning(conf, \"AddDirectiveBeginning\", \"testBegin\")\n            assert len(self.parser.find_dir(\"AddDirectiveBeginning\", \"testBegin\", conf)) > \\\n                0\n\n    def test_empty_arg(self):\n        assert self.parser.get_arg(\"/files/whatever/nonexistent\") is None\n\n    def test_add_dir_to_ifmodssl(self):\n        \"\"\"test add_dir_to_ifmodssl.\n\n        Path must be valid before attempting to add to augeas\n\n        \"\"\"\n        from certbot_apache._internal.parser import get_aug_path\n\n        # This makes sure that find_dir will work\n        self.parser.modules[\"mod_ssl.c\"] = \"/fake/path\"\n\n        self.parser.add_dir_to_ifmodssl(\n            get_aug_path(self.parser.loc[\"default\"]),\n            \"FakeDirective\", [\"123\"])\n\n        matches = self.parser.find_dir(\"FakeDirective\", \"123\")\n\n        assert len(matches) == 1\n        assert \"IfModule\" in matches[0]\n\n    def test_add_dir_to_ifmodssl_multiple(self):\n        from certbot_apache._internal.parser import get_aug_path\n\n        # This makes sure that find_dir will work\n        self.parser.modules[\"mod_ssl.c\"] = \"/fake/path\"\n\n        self.parser.add_dir_to_ifmodssl(\n            get_aug_path(self.parser.loc[\"default\"]),\n            \"FakeDirective\", [\"123\", \"456\", \"789\"])\n\n        matches = self.parser.find_dir(\"FakeDirective\")\n\n        assert len(matches) == 3\n        assert \"IfModule\" in matches[0]\n\n    def test_get_aug_path(self):\n        from certbot_apache._internal.parser import get_aug_path\n        assert \"/files/etc/apache\" == get_aug_path(\"/etc/apache\")\n\n    def test_set_locations(self):\n        with mock.patch(\"certbot_apache._internal.parser.os.path\") as mock_path:\n\n            mock_path.isfile.side_effect = [False, False]\n\n            # pylint: disable=protected-access\n            results = self.parser._set_locations()\n\n            assert results[\"default\"] == results[\"listen\"]\n            assert results[\"default\"] == results[\"name\"]\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.find_dir\")\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.get_arg\")\n    def test_parse_modules_bad_syntax(self, mock_arg, mock_find):\n        mock_find.return_value = [\"1\", \"2\", \"3\", \"4\", \"5\", \"6\", \"7\", \"8\"]\n        mock_arg.return_value = None\n        with mock.patch(\"certbot_apache._internal.parser.logger\") as mock_logger:\n            self.parser.parse_modules()\n            # Make sure that we got None return value and logged the file\n            assert mock_logger.debug.called is True\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.find_dir\")\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_update_runtime_variables(self, mock_cfg, _):\n        define_val = (\n            'ServerRoot: \"/etc/apache2\"\\n'\n            'Main DocumentRoot: \"/var/www\"\\n'\n            'Main ErrorLog: \"/var/log/apache2/error.log\"\\n'\n            'Mutex ssl-stapling: using_defaults\\n'\n            'Mutex ssl-cache: using_defaults\\n'\n            'Mutex default: dir=\"/var/lock/apache2\" mechanism=fcntl\\n'\n            'Mutex watchdog-callback: using_defaults\\n'\n            'PidFile: \"/var/run/apache2/apache2.pid\"\\n'\n            'Define: TEST\\n'\n            'Define: DUMP_RUN_CFG\\n'\n            'Define: U_MICH\\n'\n            'Define: TLS=443\\n'\n            'Define: WITH_ASSIGNMENT=URL=http://example.com\\n'\n            'Define: EMPTY=\\n'\n            'Define: example_path=Documents/path\\n'\n            'User: name=\"www-data\" id=33 not_used\\n'\n            'Group: name=\"www-data\" id=33 not_used\\n'\n        )\n        inc_val = (\n            'Included configuration files:\\n'\n            '  (*) /etc/apache2/apache2.conf\\n'\n            '    (146) /etc/apache2/mods-enabled/access_compat.load\\n'\n            '    (146) /etc/apache2/mods-enabled/alias.load\\n'\n            '    (146) /etc/apache2/mods-enabled/auth_basic.load\\n'\n            '    (146) /etc/apache2/mods-enabled/authn_core.load\\n'\n            '    (146) /etc/apache2/mods-enabled/authn_file.load\\n'\n            '    (146) /etc/apache2/mods-enabled/authz_core.load\\n'\n            '    (146) /etc/apache2/mods-enabled/authz_host.load\\n'\n            '    (146) /etc/apache2/mods-enabled/authz_user.load\\n'\n            '    (146) /etc/apache2/mods-enabled/autoindex.load\\n'\n            '    (146) /etc/apache2/mods-enabled/deflate.load\\n'\n            '    (146) /etc/apache2/mods-enabled/dir.load\\n'\n            '    (146) /etc/apache2/mods-enabled/env.load\\n'\n            '    (146) /etc/apache2/mods-enabled/filter.load\\n'\n            '    (146) /etc/apache2/mods-enabled/mime.load\\n'\n            '    (146) /etc/apache2/mods-enabled/mpm_event.load\\n'\n            '    (146) /etc/apache2/mods-enabled/negotiation.load\\n'\n            '    (146) /etc/apache2/mods-enabled/reqtimeout.load\\n'\n            '    (146) /etc/apache2/mods-enabled/setenvif.load\\n'\n            '    (146) /etc/apache2/mods-enabled/socache_shmcb.load\\n'\n            '    (146) /etc/apache2/mods-enabled/ssl.load\\n'\n            '    (146) /etc/apache2/mods-enabled/status.load\\n'\n            '    (147) /etc/apache2/mods-enabled/alias.conf\\n'\n            '    (147) /etc/apache2/mods-enabled/autoindex.conf\\n'\n            '    (147) /etc/apache2/mods-enabled/deflate.conf\\n'\n        )\n        mod_val = (\n            'Loaded Modules:\\n'\n            ' core_module (static)\\n'\n            ' so_module (static)\\n'\n            ' watchdog_module (static)\\n'\n            ' http_module (static)\\n'\n            ' log_config_module (static)\\n'\n            ' logio_module (static)\\n'\n            ' version_module (static)\\n'\n            ' unixd_module (static)\\n'\n            ' access_compat_module (shared)\\n'\n            ' alias_module (shared)\\n'\n            ' auth_basic_module (shared)\\n'\n            ' authn_core_module (shared)\\n'\n            ' authn_file_module (shared)\\n'\n            ' authz_core_module (shared)\\n'\n            ' authz_host_module (shared)\\n'\n            ' authz_user_module (shared)\\n'\n            ' autoindex_module (shared)\\n'\n            ' deflate_module (shared)\\n'\n            ' dir_module (shared)\\n'\n            ' env_module (shared)\\n'\n            ' filter_module (shared)\\n'\n            ' mime_module (shared)\\n'\n            ' mpm_event_module (shared)\\n'\n            ' negotiation_module (shared)\\n'\n            ' reqtimeout_module (shared)\\n'\n            ' setenvif_module (shared)\\n'\n            ' socache_shmcb_module (shared)\\n'\n            ' ssl_module (shared)\\n'\n            ' status_module (shared)\\n'\n        )\n\n        def mock_get_vars(cmd):\n            \"\"\"Mock command output\"\"\"\n            if cmd[-1] == \"DUMP_RUN_CFG\":\n                return define_val\n            elif cmd[-1] == \"DUMP_INCLUDES\":\n                return inc_val\n            elif cmd[-1] == \"DUMP_MODULES\":\n                return mod_val\n            return None  # pragma: no cover\n\n        mock_cfg.side_effect = mock_get_vars\n\n        expected_vars = {\"TEST\": \"\", \"U_MICH\": \"\", \"TLS\": \"443\",\n                         \"example_path\": \"Documents/path\",\n                         \"WITH_ASSIGNMENT\": \"URL=http://example.com\",\n                         \"EMPTY\": \"\",\n                         }\n\n        self.parser.modules = {}\n        with mock.patch(\n            \"certbot_apache._internal.parser.ApacheParser.parse_file\") as mock_parse:\n            self.parser.update_runtime_variables()\n            assert self.parser.variables == expected_vars\n            assert len(self.parser.modules) == 58\n            # None of the includes in inc_val should be in parsed paths.\n            # Make sure we tried to include them all.\n            assert mock_parse.call_count == 25\n\n    @mock.patch(\"certbot_apache._internal.parser.ApacheParser.find_dir\")\n    @mock.patch(\"certbot_apache._internal.apache_util._get_runtime_cfg\")\n    def test_update_runtime_variables_alt_values(self, mock_cfg, _):\n        inc_val = (\n            'Included configuration files:\\n'\n            '  (*) {0}\\n'\n            '    (146) /etc/apache2/mods-enabled/access_compat.load\\n'\n            '    (146) {1}/mods-enabled/alias.load\\n'\n        ).format(self.parser.loc[\"root\"],\n                 os.path.dirname(self.parser.loc[\"root\"]))\n\n        mock_cfg.return_value = inc_val\n        self.parser.modules = {}\n\n        with mock.patch(\n            \"certbot_apache._internal.parser.ApacheParser.parse_file\") as mock_parse:\n            self.parser.update_runtime_variables()\n            # No matching modules should have been found\n            assert len(self.parser.modules) == 0\n            # Only one of the three includes do not exist in already parsed\n            # path derived from root configuration Include statements\n            assert mock_parse.call_count == 1\n\n    @mock.patch(\"certbot_apache._internal.apache_util.subprocess.run\")\n    def test_update_runtime_vars_bad_ctl(self, mock_run):\n        mock_run.side_effect = OSError\n        with pytest.raises(errors.MisconfigurationError):\n            self.parser.update_runtime_variables()\n\n    @mock.patch(\"certbot_apache._internal.apache_util.subprocess.run\")\n    def test_update_runtime_vars_bad_exit(self, mock_run):\n        mock_proc = mock_run.return_value\n        mock_proc.stdout = \"\"\n        mock_proc.stderr = \"\"\n        mock_proc.returncode = -1\n        with pytest.raises(errors.MisconfigurationError):\n            self.parser.update_runtime_variables()\n\n    def test_add_comment(self):\n        from certbot_apache._internal.parser import get_aug_path\n        self.parser.add_comment(get_aug_path(self.parser.loc[\"name\"]), \"123456\")\n        comm = self.parser.find_comments(\"123456\")\n        assert len(comm) == 1\n        assert self.parser.loc[\"name\"] in comm[0]\n\n\nclass ParserInitTest(util.ApacheTest):\n\n    @mock.patch(\"certbot_apache._internal.parser.init_augeas\")\n    def test_prepare_no_augeas(self, mock_init_augeas):\n        from certbot_apache._internal.parser import ApacheParser\n        mock_init_augeas.side_effect = errors.NoInstallationError\n        self.config.config_test = mock.Mock()\n        with pytest.raises(errors.NoInstallationError):\n            ApacheParser(os.path.relpath(self.config_path), self.config,\n            \"/dummy/vhostpath\", version=(2, 4, 22))\n\n    def test_init_old_aug(self):\n        from certbot_apache._internal.parser import ApacheParser\n        with mock.patch(\"certbot_apache._internal.parser.ApacheParser.check_aug_version\") as mock_c:\n            mock_c.return_value = False\n            with pytest.raises(errors.NotSupportedError):\n                ApacheParser(os.path.relpath(self.config_path), self.config,\n                \"/dummy/vhostpath\", version=(2, 4, 22))\n\n    def test_root_normalized(self):\n        from certbot_apache._internal.parser import ApacheParser\n\n        with mock.patch(\"certbot_apache._internal.parser.ApacheParser.\"\n                        \"update_runtime_variables\"):\n            path = os.path.join(\n                self.temp_dir,\n                \"debian_apache_2_4/////multiple_vhosts/../multiple_vhosts/apache2\")\n\n            parser = ApacheParser(path, self.config, \"/dummy/vhostpath\")\n\n        assert parser.root == self.config_path\n\n    def test_root_absolute(self):\n        from certbot_apache._internal.parser import ApacheParser\n        with mock.patch(\"certbot_apache._internal.parser.ApacheParser.\"\n                        \"update_runtime_variables\"):\n            parser = ApacheParser(\n                os.path.relpath(self.config_path), self.config, \"/dummy/vhostpath\")\n\n        assert parser.root == self.config_path\n\n    def test_root_no_trailing_slash(self):\n        from certbot_apache._internal.parser import ApacheParser\n        with mock.patch(\"certbot_apache._internal.parser.ApacheParser.\"\n                        \"update_runtime_variables\"):\n            parser = ApacheParser(\n                self.config_path + os.path.sep, self.config, \"/dummy/vhostpath\")\n        assert parser.root == self.config_path\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/parsernode_configurator_test.py",
    "content": "\"\"\"Tests for ApacheConfigurator for AugeasParserNode classes\"\"\"\nimport importlib\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot_apache._internal.tests import util\n\n\nif importlib.util.find_spec('apacheconfig'):\n    HAS_APACHECONFIG = True\nelse:  # pragma: no cover\n    HAS_APACHECONFIG = False\n\n\n@unittest.skipIf(not HAS_APACHECONFIG, reason='Tests require apacheconfig dependency')\nclass ConfiguratorParserNodeTest(util.ApacheTest):  # pylint: disable=too-many-public-methods\n    \"\"\"Test AugeasParserNode using available test configurations\"\"\"\n\n    def setUp(self):  # pylint: disable=arguments-differ\n        super().setUp()\n\n        self.config = util.get_apache_configurator(\n            self.config_path, self.vhost_path, self.config_dir,\n            self.work_dir, use_parsernode=True)\n        self.vh_truth = util.get_vh_truth(\n            self.temp_dir, \"debian_apache_2_4/multiple_vhosts\")\n\n    def test_parsernode_get_vhosts(self):\n        self.config.USE_PARSERNODE = True\n        vhosts = self.config.get_virtual_hosts()\n        # Legacy get_virtual_hosts() do not set the node\n        assert vhosts[0].node is not None\n\n    def test_parsernode_get_vhosts_mismatch(self):\n        vhosts = self.config.get_virtual_hosts_v2()\n        # One of the returned VirtualHost objects differs\n        vhosts[0].name = \"IdidntExpectThat\"\n        self.config.get_virtual_hosts_v2 = mock.MagicMock(return_value=vhosts)\n        with pytest.raises(AssertionError):\n            _ = self.config.get_virtual_hosts()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/parsernode_test.py",
    "content": "\"\"\" Tests for ParserNode interface \"\"\"\n\nimport sys\n\nimport pytest\n\nfrom certbot_apache._internal import interfaces\nfrom certbot_apache._internal import parsernode_util as util\n\n\nclass DummyParserNode(interfaces.ParserNode):\n    \"\"\" A dummy class implementing ParserNode interface \"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"\n        Initializes the ParserNode instance.\n        \"\"\"\n        ancestor, dirty, filepath, metadata = util.parsernode_kwargs(kwargs)\n        self.ancestor = ancestor\n        self.dirty = dirty\n        self.filepath = filepath\n        self.metadata = metadata\n        super().__init__(**kwargs)\n\n    def save(self, msg):  # pragma: no cover\n        \"\"\"Save\"\"\"\n        pass\n\n    def find_ancestors(self, name):  # pragma: no cover\n        \"\"\" Find ancestors \"\"\"\n        return []\n\n\nclass DummyCommentNode(DummyParserNode):\n    \"\"\" A dummy class implementing CommentNode interface \"\"\"\n\n    def __init__(self, **kwargs):\n        \"\"\"\n        Initializes the CommentNode instance and sets its instance variables.\n        \"\"\"\n        comment, kwargs = util.commentnode_kwargs(kwargs)\n        self.comment = comment\n        super().__init__(**kwargs)\n\n\nclass DummyDirectiveNode(DummyParserNode):\n    \"\"\" A dummy class implementing DirectiveNode interface \"\"\"\n\n    # pylint: disable=too-many-arguments\n    def __init__(self, **kwargs):\n        \"\"\"\n        Initializes the DirectiveNode instance and sets its instance variables.\n        \"\"\"\n        name, parameters, enabled, kwargs = util.directivenode_kwargs(kwargs)\n        self.name = name\n        self.parameters = parameters\n        self.enabled = enabled\n\n        super().__init__(**kwargs)\n\n    def set_parameters(self, parameters):  # pragma: no cover\n        \"\"\"Set parameters\"\"\"\n        pass\n\n\nclass DummyBlockNode(DummyDirectiveNode):\n    \"\"\" A dummy class implementing BlockNode interface \"\"\"\n\n    def add_child_block(self, name, parameters=None, position=None):  # pragma: no cover\n        \"\"\"Add child block\"\"\"\n        pass\n\n    def add_child_directive(self, name, parameters=None, position=None):  # pragma: no cover\n        \"\"\"Add child directive\"\"\"\n        pass\n\n    def add_child_comment(self, comment=\"\", position=None):  # pragma: no cover\n        \"\"\"Add child comment\"\"\"\n        pass\n\n    def find_blocks(self, name, exclude=True):  # pragma: no cover\n        \"\"\"Find blocks\"\"\"\n        pass\n\n    def find_directives(self, name, exclude=True):  # pragma: no cover\n        \"\"\"Find directives\"\"\"\n        pass\n\n    def find_comments(self, comment, exact=False):  # pragma: no cover\n        \"\"\"Find comments\"\"\"\n        pass\n\n    def delete_child(self, child):  # pragma: no cover\n        \"\"\"Delete child\"\"\"\n        pass\n\n    def unsaved_files(self):  # pragma: no cover\n        \"\"\"Unsaved files\"\"\"\n        pass\n\n\ninterfaces.CommentNode.register(DummyCommentNode)\ninterfaces.DirectiveNode.register(DummyDirectiveNode)\ninterfaces.BlockNode.register(DummyBlockNode)\n\ndef test_dummy():\n    \"\"\"Dummy placeholder test case for ParserNode interfaces\"\"\"\n    dummyblock = DummyBlockNode(\n        name=\"None\",\n        parameters=(),\n        ancestor=None,\n        dirty=False,\n        filepath=\"/some/random/path\"\n    )\n    dummydirective = DummyDirectiveNode( # noqa: F841\n        name=\"Name\",\n        ancestor=None,\n        filepath=\"/another/path\"\n    )\n    dummycomment = DummyCommentNode( # noqa: F841\n        comment=\"Comment\",\n        ancestor=dummyblock,\n        filepath=\"/some/file\"\n    )\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/parsernode_util_test.py",
    "content": "\"\"\" Tests for ParserNode utils \"\"\"\nimport sys\n\nimport pytest\n\nfrom certbot_apache._internal import parsernode_util as util\n\n\ndef _setup_parsernode():\n    \"\"\" Sets up kwargs dict for ParserNode \"\"\"\n    return {\n        \"ancestor\": None,\n        \"dirty\": False,\n        \"filepath\": \"/tmp\",\n    }\n\ndef _setup_commentnode():\n    \"\"\" Sets up kwargs dict for CommentNode \"\"\"\n\n    pn = _setup_parsernode()\n    pn[\"comment\"] = \"x\"\n    return pn\n\ndef _setup_directivenode():\n    \"\"\" Sets up kwargs dict for DirectiveNode \"\"\"\n\n    pn = _setup_parsernode()\n    pn[\"name\"] = \"Name\"\n    pn[\"parameters\"] = (\"first\",)\n    pn[\"enabled\"] = True\n    return pn\n\ndef test_unknown_parameter():\n    params = _setup_parsernode()\n    params[\"unknown\"] = \"unknown\"\n    with pytest.raises(TypeError):\n        util.parsernode_kwargs(params)\n\n    params = _setup_commentnode()\n    params[\"unknown\"] = \"unknown\"\n    with pytest.raises(TypeError):\n        util.commentnode_kwargs(params)\n\n    params = _setup_directivenode()\n    params[\"unknown\"] = \"unknown\"\n    with pytest.raises(TypeError):\n        util.directivenode_kwargs(params)\n\ndef test_parsernode():\n    params = _setup_parsernode()\n    ctrl = _setup_parsernode()\n\n    ancestor, dirty, filepath, metadata = util.parsernode_kwargs(params)\n    assert ancestor == ctrl[\"ancestor\"]\n    assert dirty == ctrl[\"dirty\"]\n    assert filepath == ctrl[\"filepath\"]\n    assert metadata == {}\n\ndef test_parsernode_from_metadata():\n    params = _setup_parsernode()\n    params.pop(\"filepath\")\n    md = {\"some\": \"value\"}\n    params[\"metadata\"] = md\n\n    # Just testing that error from missing required parameters is not raised\n    _, _, _, metadata = util.parsernode_kwargs(params)\n    assert metadata == md\n\ndef test_commentnode():\n    params = _setup_commentnode()\n    ctrl = _setup_commentnode()\n\n    comment, _ = util.commentnode_kwargs(params)\n    assert comment == ctrl[\"comment\"]\n\ndef test_commentnode_from_metadata():\n    params = _setup_commentnode()\n    params.pop(\"comment\")\n    params[\"metadata\"] = {}\n\n    # Just testing that error from missing required parameters is not raised\n    util.commentnode_kwargs(params)\n\ndef test_directivenode():\n    params = _setup_directivenode()\n    ctrl = _setup_directivenode()\n\n    name, parameters, enabled, _ = util.directivenode_kwargs(params)\n    assert name == ctrl[\"name\"]\n    assert parameters == ctrl[\"parameters\"]\n    assert enabled == ctrl[\"enabled\"]\n\ndef test_directivenode_from_metadata():\n    params = _setup_directivenode()\n    params.pop(\"filepath\")\n    params.pop(\"name\")\n    params[\"metadata\"] = {\"irrelevant\": \"value\"}\n\n    # Just testing that error from missing required parameters is not raised\n    util.directivenode_kwargs(params)\n\ndef test_missing_required():\n    c_params = _setup_commentnode()\n    c_params.pop(\"comment\")\n    with pytest.raises(TypeError):\n        util.commentnode_kwargs(c_params)\n\n    d_params = _setup_directivenode()\n    d_params.pop(\"ancestor\")\n    with pytest.raises(TypeError):\n        util.directivenode_kwargs(d_params)\n\n    p_params = _setup_parsernode()\n    p_params.pop(\"filepath\")\n    with pytest.raises(TypeError):\n        util.parsernode_kwargs(p_params)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf/httpd.conf",
    "content": "#\n# This is the main Apache HTTP server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See <URL:http://httpd.apache.org/docs/2.4/> for detailed information.\n# In particular, see \n# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html>\n# for a discussion of each configuration directive.\n#\n# Do NOT simply read the instructions in here without understanding\n# what they do.  They're here only as hints or reminders.  If you are unsure\n# consult the online docs. You have been warned.  \n#\n# Configuration and logfile names: If the filenames you specify for many\n# of the server's control files begin with \"/\" (or \"drive:/\" for Win32), the\n# server will use that explicit path.  If the filenames do *not* begin\n# with \"/\", the value of ServerRoot is prepended -- so 'log/access_log'\n# with ServerRoot set to '/www' will be interpreted by the\n# server as '/www/log/access_log', where as '/log/access_log' will be\n# interpreted as '/log/access_log'.\n\n#\n# ServerRoot: The top of the directory tree under which the server's\n# configuration, error, and log files are kept.\n#\n# Do not add a slash at the end of the directory path.  If you point\n# ServerRoot at a non-local disk, be sure to specify a local disk on the\n# Mutex directive, if file-based mutexes are used.  If you wish to share the\n# same ServerRoot for multiple httpd daemons, you will need to change at\n# least PidFile.\n#\nServerRoot \"/etc/httpd\"\n\n#\n# Listen: Allows you to bind Apache to specific IP addresses and/or\n# ports, instead of the default. See also the <VirtualHost>\n# directive.\n#\n# Change this to Listen on specific IP addresses as shown below to \n# prevent Apache from glomming onto all bound IP addresses.\n#\n#Listen 12.34.56.78:80\nListen 80\n\n#\n# Dynamic Shared Object (DSO) Support\n#\n# To be able to use the functionality of a module which was built as a DSO you\n# have to place corresponding `LoadModule' lines at this location so the\n# directives contained in it are actually available _before_ they are used.\n# Statically compiled modules (those listed by `httpd -l') do not need\n# to be loaded here.\n#\n# Example:\n# LoadModule foo_module modules/mod_foo.so\n#\nInclude conf.modules.d/*.conf\n\n#\n# If you wish httpd to run as a different user or group, you must run\n# httpd as root initially and it will switch.  \n#\n# User/Group: The name (or #number) of the user/group to run httpd as.\n# It is usually good practice to create a dedicated user and group for\n# running httpd, as with most system services.\n#\nUser apache\nGroup apache\n\n# 'Main' server configuration\n#\n# The directives in this section set up the values used by the 'main'\n# server, which responds to any requests that aren't handled by a\n# <VirtualHost> definition.  These values also provide defaults for\n# any <VirtualHost> containers you may define later in the file.\n#\n# All of these directives may appear inside <VirtualHost> containers,\n# in which case these default settings will be overridden for the\n# virtual host being defined.\n#\n\n#\n# ServerAdmin: Your address, where problems with the server should be\n# e-mailed.  This address appears on some server-generated pages, such\n# as error documents.  e.g. admin@your-domain.com\n#\nServerAdmin root@localhost\n\n#\n# ServerName gives the name and port that the server uses to identify itself.\n# This can often be determined automatically, but we recommend you specify\n# it explicitly to prevent problems during startup.\n#\n# If your host doesn't have a registered DNS name, enter its IP address here.\n#\n#ServerName www.example.com:80\n\n#\n# Deny access to the entirety of your server's filesystem. You must\n# explicitly permit access to web content directories in other \n# <Directory> blocks below.\n#\n<Directory />\n    AllowOverride none\n    Require all denied\n</Directory>\n\n#\n# Note that from this point forward you must specifically allow\n# particular features to be enabled - so if something's not working as\n# you might expect, make sure that you have specifically enabled it\n# below.\n#\n\n#\n# DocumentRoot: The directory out of which you will serve your\n# documents. By default, all requests are taken from this directory, but\n# symbolic links and aliases may be used to point to other locations.\n#\nDocumentRoot \"/var/www/html\"\n\n#\n# Relax access to content within /var/www.\n#\n<Directory \"/var/www\">\n    AllowOverride None\n    # Allow open access:\n    Require all granted\n</Directory>\n\n# Further relax access to the default document root:\n<Directory \"/var/www/html\">\n    #\n    # Possible values for the Options directive are \"None\", \"All\",\n    # or any combination of:\n    #   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews\n    #\n    # Note that \"MultiViews\" must be named *explicitly* --- \"Options All\"\n    # doesn't give it to you.\n    #\n    # The Options directive is both complicated and important.  Please see\n    # http://httpd.apache.org/docs/2.4/mod/core.html#options\n    # for more information.\n    #\n    Options Indexes FollowSymLinks\n\n    #\n    # AllowOverride controls what directives may be placed in .htaccess files.\n    # It can be \"All\", \"None\", or any combination of the keywords:\n    #   Options FileInfo AuthConfig Limit\n    #\n    AllowOverride None\n\n    #\n    # Controls who can get stuff from this server.\n    #\n    Require all granted\n</Directory>\n\n#\n# DirectoryIndex: sets the file that Apache will serve if a directory\n# is requested.\n#\n<IfModule dir_module>\n    DirectoryIndex index.html\n</IfModule>\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being \n# viewed by Web clients. \n#\n<Files \".ht*\">\n    Require all denied\n</Files>\n\n#\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog \"logs/error_log\"\n\n#\n# LogLevel: Control the number of messages logged to the error_log.\n# Possible values include: debug, info, notice, warn, error, crit,\n# alert, emerg.\n#\nLogLevel warn\n\n<IfModule log_config_module>\n    #\n    # The following directives define some format nicknames for use with\n    # a CustomLog directive (see below).\n    #\n    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\n    LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b\" common\n\n    <IfModule logio_module>\n      # You need to enable mod_logio.c to use %I and %O\n      LogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O\" combinedio\n    </IfModule>\n\n    #\n    # The location and format of the access logfile (Common Logfile Format).\n    # If you do not define any access logfiles within a <VirtualHost>\n    # container, they will be logged here.  Contrariwise, if you *do*\n    # define per-<VirtualHost> access logfiles, transactions will be\n    # logged therein and *not* in this file.\n    #\n    #CustomLog \"logs/access_log\" common\n\n    #\n    # If you prefer a logfile with access, agent, and referer information\n    # (Combined Logfile Format) you can use the following directive.\n    #\n    CustomLog \"logs/access_log\" combined\n</IfModule>\n\n<IfModule alias_module>\n    #\n    # Redirect: Allows you to tell clients about documents that used to \n    # exist in your server's namespace, but do not anymore. The client \n    # will make a new request for the document at its new location.\n    # Example:\n    # Redirect permanent /foo http://www.example.com/bar\n\n    #\n    # Alias: Maps web paths into filesystem paths and is used to\n    # access content that does not live under the DocumentRoot.\n    # Example:\n    # Alias /webpath /full/filesystem/path\n    #\n    # If you include a trailing / on /webpath then the server will\n    # require it to be present in the URL.  You will also likely\n    # need to provide a <Directory> section to allow access to\n    # the filesystem path.\n\n    #\n    # ScriptAlias: This controls which directories contain server scripts. \n    # ScriptAliases are essentially the same as Aliases, except that\n    # documents in the target directory are treated as applications and\n    # run by the server when requested rather than as documents sent to the\n    # client.  The same rules about trailing \"/\" apply to ScriptAlias\n    # directives as to Alias.\n    #\n    ScriptAlias /cgi-bin/ \"/var/www/cgi-bin/\"\n\n</IfModule>\n\n#\n# \"/var/www/cgi-bin\" should be changed to whatever your ScriptAliased\n# CGI directory exists, if you have that configured.\n#\n<Directory \"/var/www/cgi-bin\">\n    AllowOverride None\n    Options None\n    Require all granted\n</Directory>\n\n<IfModule mime_module>\n    #\n    # TypesConfig points to the file containing the list of mappings from\n    # filename extension to MIME-type.\n    #\n    TypesConfig /etc/mime.types\n\n    #\n    # AddType allows you to add to or override the MIME configuration\n    # file specified in TypesConfig for specific file types.\n    #\n    #AddType application/x-gzip .tgz\n    #\n    # AddEncoding allows you to have certain browsers uncompress\n    # information on the fly. Note: Not all browsers support this.\n    #\n    #AddEncoding x-compress .Z\n    #AddEncoding x-gzip .gz .tgz\n    #\n    # If the AddEncoding directives above are commented-out, then you\n    # probably should define those extensions to indicate media types:\n    #\n    AddType application/x-compress .Z\n    AddType application/x-gzip .gz .tgz\n\n    #\n    # AddHandler allows you to map certain file extensions to \"handlers\":\n    # actions unrelated to filetype. These can be either built into the server\n    # or added with the Action directive (see below)\n    #\n    # To use CGI scripts outside of ScriptAliased directories:\n    # (You will also need to add \"ExecCGI\" to the \"Options\" directive.)\n    #\n    #AddHandler cgi-script .cgi\n\n    # For type maps (negotiated resources):\n    #AddHandler type-map var\n\n    #\n    # Filters allow you to process content before it is sent to the client.\n    #\n    # To parse .shtml files for server-side includes (SSI):\n    # (You will also need to add \"Includes\" to the \"Options\" directive.)\n    #\n    AddType text/html .shtml\n    AddOutputFilter INCLUDES .shtml\n</IfModule>\n\n#\n# Specify a default charset for all content served; this enables\n# interpretation of all content as UTF-8 by default.  To use the \n# default browser choice (ISO-8859-1), or to allow the META tags\n# in HTML content to override this choice, comment out this\n# directive:\n#\nAddDefaultCharset UTF-8\n\n<IfModule mime_magic_module>\n    #\n    # The mod_mime_magic module allows the server to use various hints from the\n    # contents of the file itself to determine its type.  The MIMEMagicFile\n    # directive tells the module where the hint definitions are located.\n    #\n    MIMEMagicFile conf/magic\n</IfModule>\n\n#\n# Customizable error responses come in three flavors:\n# 1) plain text 2) local redirects 3) external redirects\n#\n# Some examples:\n#ErrorDocument 500 \"The server made a boo boo.\"\n#ErrorDocument 404 /missing.html\n#ErrorDocument 404 \"/cgi-bin/missing_handler.pl\"\n#ErrorDocument 402 http://www.example.com/subscription_info.html\n#\n\n#\n# EnableMMAP and EnableSendfile: On systems that support it, \n# memory-mapping or the sendfile syscall may be used to deliver\n# files.  This usually improves server performance, but must\n# be turned off when serving from networked-mounted \n# filesystems or if support for these functions is otherwise\n# broken on your system.\n# Defaults if commented: EnableMMAP On, EnableSendfile Off\n#\n#EnableMMAP off\nEnableSendfile on\n\n# Supplemental configuration\n#\n# Load config files in the \"/etc/httpd/conf.d\" directory, if any.\nIncludeOptional conf.d/*.conf\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf/magic",
    "content": "# Magic data for mod_mime_magic Apache module (originally for file(1) command)\n# The module is described in /manual/mod/mod_mime_magic.html\n#\n# The format is 4-5 columns:\n#    Column #1: byte number to begin checking from, \">\" indicates continuation\n#    Column #2: type of data to match\n#    Column #3: contents of data to match\n#    Column #4: MIME type of result\n#    Column #5: MIME encoding of result (optional)\n\n#------------------------------------------------------------------------------\n# Localstuff:  file(1) magic for locally observed files\n# Add any locally observed files here.\n\n#------------------------------------------------------------------------------\n# end local stuff\n#------------------------------------------------------------------------------\n\n#------------------------------------------------------------------------------\n# Java\n\n0\tshort\t\t0xcafe\n>2\tshort\t\t0xbabe\t\tapplication/java\n\n#------------------------------------------------------------------------------\n# audio:  file(1) magic for sound formats\n#\n# from Jan Nicolai Langfeldt <janl@ifi.uio.no>,\n#\n\n# Sun/NeXT audio data\n0\tstring\t\t.snd\n>12\tbelong\t\t1\t\taudio/basic\n>12\tbelong\t\t2\t\taudio/basic\n>12\tbelong\t\t3\t\taudio/basic\n>12\tbelong\t\t4\t\taudio/basic\n>12\tbelong\t\t5\t\taudio/basic\n>12\tbelong\t\t6\t\taudio/basic\n>12\tbelong\t\t7\t\taudio/basic\n\n>12\tbelong\t\t23\t\taudio/x-adpcm\n\n# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format\n# that uses little-endian encoding and has a different magic number\n# (0x0064732E in little-endian encoding).\n0\tlelong\t\t0x0064732E\t\n>12\tlelong\t\t1\t\taudio/x-dec-basic\n>12\tlelong\t\t2\t\taudio/x-dec-basic\n>12\tlelong\t\t3\t\taudio/x-dec-basic\n>12\tlelong\t\t4\t\taudio/x-dec-basic\n>12\tlelong\t\t5\t\taudio/x-dec-basic\n>12\tlelong\t\t6\t\taudio/x-dec-basic\n>12\tlelong\t\t7\t\taudio/x-dec-basic\n#                                       compressed (G.721 ADPCM)\n>12\tlelong\t\t23\t\taudio/x-dec-adpcm\n\n# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are \"FORM\"\n#\t\t\t\t\tAIFF audio data\n8\tstring\t\tAIFF\t\taudio/x-aiff\t\n#\t\t\t\t\tAIFF-C audio data\n8\tstring\t\tAIFC\t\taudio/x-aiff\t\n#\t\t\t\t\tIFF/8SVX audio data\n8\tstring\t\t8SVX\t\taudio/x-aiff\t\n\n# Creative Labs AUDIO stuff\n#\t\t\t\t\tStandard MIDI data\n0\tstring\tMThd\t\t\taudio/unknown\t\n#>9 \tbyte\t>0\t\t\t(format %d)\n#>11\tbyte\t>1\t\t\tusing %d channels\n#\t\t\t\t\tCreative Music (CMF) data\n0\tstring\tCTMF\t\t\taudio/unknown\t\n#\t\t\t\t\tSoundBlaster instrument data\n0\tstring\tSBI\t\t\taudio/unknown\t\n#\t\t\t\t\tCreative Labs voice data\n0\tstring\tCreative\\ Voice\\ File\taudio/unknown\t\n## is this next line right?  it came this way...\n#>19\tbyte\t0x1A\n#>23\tbyte\t>0\t\t\t- version %d\n#>22\tbyte\t>0\t\t\t\\b.%d\n\n# [GRR 950115:  is this also Creative Labs?  Guessing that first line\n#  should be string instead of unknown-endian long...]\n#0\tlong\t\t0x4e54524b\tMultiTrack sound data\n#0\tstring\t\tNTRK\t\tMultiTrack sound data\n#>4\tlong\t\tx\t\t- version %ld\n\n# Microsoft WAVE format (*.wav)\n# [GRR 950115:  probably all of the shorts and longs should be leshort/lelong]\n#\t\t\t\t\tMicrosoft RIFF\n0\tstring\t\tRIFF\t\taudio/unknown\n#\t\t\t\t\t- WAVE format\n>8\tstring\t\tWAVE\t\taudio/x-wav\n# MPEG audio.\n0   beshort&0xfff0  0xfff0  audio/mpeg\n# C64 SID Music files, from Linus Walleij <triad@df.lth.se>\n0   string      PSID        audio/prs.sid\n\n#------------------------------------------------------------------------------\n# c-lang:  file(1) magic for C programs or various scripts\n#\n\n# XPM icons (Greg Roelofs, newt@uchicago.edu)\n# ideally should go into \"images\", but entries below would tag XPM as C source\n0\tstring\t\t/*\\ XPM\t\timage/x-xbm\t7bit\n\n# this first will upset you if you're a PL/1 shop... (are there any left?)\n# in which case rm it; ascmagic will catch real C programs\n#\t\t\t\t\tC or REXX program text\n0\tstring\t\t/*\t\ttext/plain\n#\t\t\t\t\tC++ program text\n0\tstring\t\t//\t\ttext/plain\n\n#------------------------------------------------------------------------------\n# compress:  file(1) magic for pure-compression formats (no archives)\n#\n# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc.\n#\n# Formats for various forms of compressed data\n# Formats for \"compress\" proper have been moved into \"compress.c\",\n# because it tries to uncompress it to figure out what's inside.\n\n# standard unix compress\n0\tstring\t\t\\037\\235\tapplication/octet-stream\tx-compress\n\n# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver)\n0       string          \\037\\213        application/octet-stream\tx-gzip\n\n# According to gzip.h, this is the correct byte order for packed data.\n0\tstring\t\t\\037\\036\tapplication/octet-stream\n#\n# This magic number is byte-order-independent.\n#\n0\tshort\t\t017437\t\tapplication/octet-stream\n\n# XXX - why *two* entries for \"compacted data\", one of which is\n# byte-order independent, and one of which is byte-order dependent?\n#\n# compacted data\n0\tshort\t\t0x1fff\t\tapplication/octet-stream\n0\tstring\t\t\\377\\037\tapplication/octet-stream\n# huf output\n0\tshort\t\t0145405\t\tapplication/octet-stream\n\n# Squeeze and Crunch...\n# These numbers were gleaned from the Unix versions of the programs to\n# handle these formats.  Note that I can only uncrunch, not crunch, and\n# I didn't have a crunched file handy, so the crunch number is untested.\n#\t\t\t\tKeith Waclena <keith@cerberus.uchicago.edu>\n#0\tleshort\t\t0x76FF\t\tsqueezed data (CP/M, DOS)\n#0\tleshort\t\t0x76FE\t\tcrunched data (CP/M, DOS)\n\n# Freeze\n#0\tstring\t\t\\037\\237\tFrozen file 2.1\n#0\tstring\t\t\\037\\236\tFrozen file 1.0 (or gzip 0.5)\n\n# lzh?\n#0\tstring\t\t\\037\\240\tLZH compressed data\n\n#------------------------------------------------------------------------------\n# frame:  file(1) magic for FrameMaker files\n#\n# This stuff came on a FrameMaker demo tape, most of which is\n# copyright, but this file is \"published\" as witness the following:\n#\n0\tstring\t\t\\<MakerFile\tapplication/x-frame\n0\tstring\t\t\\<MIFFile\tapplication/x-frame\n0\tstring\t\t\\<MakerDictionary\tapplication/x-frame\n0\tstring\t\t\\<MakerScreenFon\tapplication/x-frame\n0\tstring\t\t\\<MML\t\tapplication/x-frame\n0\tstring\t\t\\<Book\t\tapplication/x-frame\n0\tstring\t\t\\<Maker\t\tapplication/x-frame\n\n#------------------------------------------------------------------------------\n# html:  file(1) magic for HTML (HyperText Markup Language) docs\n#\n# from Daniel Quinlan <quinlan@yggdrasil.com>\n# and Anna Shergold <anna@inext.co.uk>\n#\n0   string      \\<!DOCTYPE\\ HTML    text/html\n0   string      \\<!doctype\\ html    text/html\n0   string      \\<HEAD      text/html\n0   string      \\<head      text/html\n0   string      \\<TITLE     text/html\n0   string      \\<title     text/html\n0   string      \\<html      text/html\n0   string      \\<HTML      text/html\n0   string      \\<!--       text/html\n0   string      \\<h1        text/html\n0   string      \\<H1        text/html\n\n# XML eXtensible Markup Language, from Linus Walleij <triad@df.lth.se>\n0   string      \\<?xml      text/xml\n\n#------------------------------------------------------------------------------\n# images:  file(1) magic for image formats (see also \"c-lang\" for XPM bitmaps)\n#\n# originally from jef@helios.ee.lbl.gov (Jef Poskanzer),\n# additions by janl@ifi.uio.no as well as others. Jan also suggested\n# merging several one- and two-line files into here.\n#\n# XXX - byte order for GIF and TIFF fields?\n# [GRR:  TIFF allows both byte orders; GIF is probably little-endian]\n#\n\n# [GRR:  what the hell is this doing in here?]\n#0\tstring\t\txbtoa\t\tbtoa'd file\n\n# PBMPLUS\n#\t\t\t\t\tPBM file\n0\tstring\t\tP1\t\timage/x-portable-bitmap\t7bit\n#\t\t\t\t\tPGM file\n0\tstring\t\tP2\t\timage/x-portable-greymap\t7bit\n#\t\t\t\t\tPPM file\n0\tstring\t\tP3\t\timage/x-portable-pixmap\t7bit\n#\t\t\t\t\tPBM \"rawbits\" file\n0\tstring\t\tP4\t\timage/x-portable-bitmap\n#\t\t\t\t\tPGM \"rawbits\" file\n0\tstring\t\tP5\t\timage/x-portable-greymap\n#\t\t\t\t\tPPM \"rawbits\" file\n0\tstring\t\tP6\t\timage/x-portable-pixmap\n\n# NIFF (Navy Interchange File Format, a modification of TIFF)\n# [GRR:  this *must* go before TIFF]\n0\tstring\t\tIIN1\t\timage/x-niff\n\n# TIFF and friends\n#\t\t\t\t\tTIFF file, big-endian\n0\tstring\t\tMM\t\timage/tiff\n#\t\t\t\t\tTIFF file, little-endian\n0\tstring\t\tII\t\timage/tiff\n\n# possible GIF replacements; none yet released!\n# (Greg Roelofs, newt@uchicago.edu)\n#\n# GRR 950115:  this was mine (\"Zip GIF\"):\n#\t\t\t\t\tZIF image (GIF+deflate alpha)\n0\tstring\t\tGIF94z\t\timage/unknown\n#\n# GRR 950115:  this is Jeremy Wohl's Free Graphics Format (better):\n#\t\t\t\t\tFGF image (GIF+deflate beta)\n0\tstring\t\tFGF95a\t\timage/unknown\n#\n# GRR 950115:  this is Thomas Boutell's Portable Bitmap Format proposal\n# (best; not yet implemented):\n#\t\t\t\t\tPBF image (deflate compression)\n0\tstring\t\tPBF\t\timage/unknown\n\n# GIF\n0\tstring\t\tGIF\t\timage/gif\n\n# JPEG images\n0\tbeshort\t\t0xffd8\t\timage/jpeg\n\n# PC bitmaps (OS/2, Windoze BMP files)  (Greg Roelofs, newt@uchicago.edu)\n0\tstring\t\tBM\t\timage/bmp\n#>14\tbyte\t\t12\t\t(OS/2 1.x format)\n#>14\tbyte\t\t64\t\t(OS/2 2.x format)\n#>14\tbyte\t\t40\t\t(Windows 3.x format)\n#0\tstring\t\tIC\t\ticon\n#0\tstring\t\tPI\t\tpointer\n#0\tstring\t\tCI\t\tcolor icon\n#0\tstring\t\tCP\t\tcolor pointer\n#0\tstring\t\tBA\t\tbitmap array\n\n0\tstring\t\t\\x89PNG\t\timage/png\n0\tstring\t\tFWS\t\tapplication/x-shockwave-flash\n0\tstring\t\tCWS\t\tapplication/x-shockwave-flash\n\n#------------------------------------------------------------------------------\n# lisp:  file(1) magic for lisp programs\n#\n# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com)\n0\tstring\t;;\t\t\ttext/plain\t8bit\n# Emacs 18 - this is always correct, but not very magical.\n0\tstring\t\\012(\t\t\tapplication/x-elc\n# Emacs 19\n0\tstring\t;ELC\\023\\000\\000\\000\tapplication/x-elc\n\n#------------------------------------------------------------------------------\n# mail.news:  file(1) magic for mail and news\n#\n# There are tests to ascmagic.c to cope with mail and news.\n0\tstring\t\tRelay-Version: \tmessage/rfc822\t7bit\n0\tstring\t\t#!\\ rnews\tmessage/rfc822\t7bit\n0\tstring\t\tN#!\\ rnews\tmessage/rfc822\t7bit\n0\tstring\t\tForward\\ to \tmessage/rfc822\t7bit\n0\tstring\t\tPipe\\ to \tmessage/rfc822\t7bit\n0\tstring\t\tReturn-Path:\tmessage/rfc822\t7bit\n0\tstring\t\tPath:\t\tmessage/news\t8bit\n0\tstring\t\tXref:\t\tmessage/news\t8bit\n0\tstring\t\tFrom:\t\tmessage/rfc822\t7bit\n0\tstring\t\tArticle \tmessage/news\t8bit\n#------------------------------------------------------------------------------\n# msword: file(1) magic for MS Word files\n#\n# Contributor claims:\n# Reversed-engineered MS Word magic numbers\n#\n\n0\tstring\t\t\\376\\067\\0\\043\t\t\tapplication/msword\n0\tstring\t\t\\333\\245-\\0\\0\\0\t\t\tapplication/msword\n\n# disable this one because it applies also to other\n# Office/OLE documents for which msword is not correct. See PR#2608.\n#0\tstring\t\t\\320\\317\\021\\340\\241\\261\tapplication/msword\n\n\n\n#------------------------------------------------------------------------------\n# printer:  file(1) magic for printer-formatted files\n#\n\n# PostScript\n0\tstring\t\t%!\t\tapplication/postscript\n0\tstring\t\t\\004%!\t\tapplication/postscript\n\n# Acrobat\n# (due to clamen@cs.cmu.edu)\n0\tstring\t\t%PDF-\t\tapplication/pdf\n\n#------------------------------------------------------------------------------\n# sc:  file(1) magic for \"sc\" spreadsheet\n#\n38\tstring\t\tSpreadsheet\tapplication/x-sc\n\n#------------------------------------------------------------------------------\n# tex:  file(1) magic for TeX files\n#\n# XXX - needs byte-endian stuff (big-endian and little-endian DVI?)\n#\n# From <conklin@talisman.kaleida.com>\n\n# Although we may know the offset of certain text fields in TeX DVI\n# and font files, we can't use them reliably because they are not\n# zero terminated. [but we do anyway, christos]\n0\tstring\t\t\\367\\002\tapplication/x-dvi\n#0\tstring\t\t\\367\\203\tTeX generic font data\n#0\tstring\t\t\\367\\131\tTeX packed font data\n#0\tstring\t\t\\367\\312\tTeX virtual font data\n#0\tstring\t\tThis\\ is\\ TeX,\tTeX transcript text\t\n#0\tstring\t\tThis\\ is\\ METAFONT,\tMETAFONT transcript text\n\n# There is no way to detect TeX Font Metric (*.tfm) files without\n# breaking them apart and reading the data.  The following patterns\n# match most *.tfm files generated by METAFONT or afm2tfm.\n#2\tstring\t\t\\000\\021\tTeX font metric data\n#2\tstring\t\t\\000\\022\tTeX font metric data\n#>34\tstring\t\t>\\0\t\t(%s)\n\n# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com)\n#0\tstring\t\t\\\\input\\ texinfo\tTexinfo source text\n#0\tstring\t\tThis\\ is\\ Info\\ file\tGNU Info text\n\n# correct TeX magic for Linux (and maybe more)\n# from Peter Tobias (tobias@server.et-inf.fho-emden.de)\n#\n0\tleshort\t\t0x02f7\t\tapplication/x-dvi\n\n# RTF - Rich Text Format\n0\tstring\t\t{\\\\rtf\t\tapplication/rtf\n\n#------------------------------------------------------------------------------\n# animation:  file(1) magic for animation/movie formats\n#\n# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8)\n#\t\t\t\t\t\tMPEG file\n0\tstring\t\t\\000\\000\\001\\263\tvideo/mpeg\n#\n# The contributor claims:\n#   I couldn't find a real magic number for these, however, this\n#   -appears- to work.  Note that it might catch other files, too,\n#   so BE CAREFUL!\n#\n# Note that title and author appear in the two 20-byte chunks\n# at decimal offsets 2 and 22, respectively, but they are XOR'ed with\n# 255 (hex FF)! DL format SUCKS BIG ROCKS.\n#\n#\t\t\t\t\t\tDL file version 1 , medium format (160x100, 4 images/screen)\n0\tbyte\t\t1\t\t\tvideo/unknown\n0\tbyte\t\t2\t\t\tvideo/unknown\n# Quicktime video, from Linus Walleij <triad@df.lth.se>\n# from Apple quicktime file format documentation.\n4   string      moov        video/quicktime\n4   string      mdat        video/quicktime\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/README",
    "content": "\nThis directory holds configuration files for the Apache HTTP Server;\nany files in this directory which have the \".conf\" extension will be\nprocessed as httpd configuration files.  The directory is used in\naddition to the directory /etc/httpd/conf.modules.d/, which contains\nconfiguration files necessary to load modules.\n\nFiles are processed in alphabetical order.\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/autoindex.conf",
    "content": "#\n# Directives controlling the display of server-generated directory listings.\n#\n# Required modules: mod_authz_core, mod_authz_host,\n#                   mod_autoindex, mod_alias\n#\n# To see the listing of a directory, the Options directive for the\n# directory must include \"Indexes\", and the directory must not contain\n# a file matching those listed in the DirectoryIndex directive.\n#\n\n#\n# IndexOptions: Controls the appearance of server-generated directory\n# listings.\n#\nIndexOptions FancyIndexing HTMLTable VersionSort\n\n# We include the /icons/ alias for FancyIndexed directory listings.  If\n# you do not use FancyIndexing, you may comment this out.\n#\nAlias /icons/ \"/usr/share/httpd/icons/\"\n\n<Directory \"/usr/share/httpd/icons\">\n    Options Indexes MultiViews FollowSymlinks\n    AllowOverride None\n    Require all granted\n</Directory>\n\n#\n# AddIcon* directives tell the server which icon to show for different\n# files or filename extensions.  These are only displayed for\n# FancyIndexed directories.\n#\nAddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip\n\nAddIconByType (TXT,/icons/text.gif) text/*\nAddIconByType (IMG,/icons/image2.gif) image/*\nAddIconByType (SND,/icons/sound2.gif) audio/*\nAddIconByType (VID,/icons/movie.gif) video/*\n\nAddIcon /icons/binary.gif .bin .exe\nAddIcon /icons/binhex.gif .hqx\nAddIcon /icons/tar.gif .tar\nAddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv\nAddIcon /icons/compressed.gif .Z .z .tgz .gz .zip\nAddIcon /icons/a.gif .ps .ai .eps\nAddIcon /icons/layout.gif .html .shtml .htm .pdf\nAddIcon /icons/text.gif .txt\nAddIcon /icons/c.gif .c\nAddIcon /icons/p.gif .pl .py\nAddIcon /icons/f.gif .for\nAddIcon /icons/dvi.gif .dvi\nAddIcon /icons/uuencoded.gif .uu\nAddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl\nAddIcon /icons/tex.gif .tex\nAddIcon /icons/bomb.gif /core\nAddIcon /icons/bomb.gif */core.*\n\nAddIcon /icons/back.gif ..\nAddIcon /icons/hand.right.gif README\nAddIcon /icons/folder.gif ^^DIRECTORY^^\nAddIcon /icons/blank.gif ^^BLANKICON^^\n\n#\n# DefaultIcon is which icon to show for files which do not have an icon\n# explicitly set.\n#\nDefaultIcon /icons/unknown.gif\n\n#\n# AddDescription allows you to place a short description after a file in\n# server-generated indexes.  These are only displayed for FancyIndexed\n# directories.\n# Format: AddDescription \"description\" filename\n#\n#AddDescription \"GZIP compressed document\" .gz\n#AddDescription \"tar archive\" .tar\n#AddDescription \"GZIP compressed tar archive\" .tgz\n\n#\n# ReadmeName is the name of the README file the server will look for by\n# default, and append to directory listings.\n#\n# HeaderName is the name of a file which should be prepended to\n# directory indexes. \nReadmeName README.html\nHeaderName HEADER.html\n\n#\n# IndexIgnore is a set of filenames which directory indexing should ignore\n# and not include in the listing.  Shell-style wildcarding is permitted.\n#\nIndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/centos.example.com.conf",
    "content": "<VirtualHost *:80>\n\tServerName centos.example.com\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/ssl.conf",
    "content": "#\n# When we also provide SSL we have to listen to the \n# the HTTPS port in addition.\n#\nListen 443 https\n\n##\n##  SSL Global Context\n##\n##  All SSL configuration in this context applies both to\n##  the main server and all SSL-enabled virtual hosts.\n##\n\n#   Pass Phrase Dialog:\n#   Configure the pass phrase gathering process.\n#   The filtering dialog program (`builtin' is an internal\n#   terminal dialog) has to provide the pass phrase on stdout.\nSSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog\n\n#   Inter-Process Session Cache:\n#   Configure the SSL Session Cache: First the mechanism \n#   to use and second the expiring timeout (in seconds).\nSSLSessionCache         shmcb:/run/httpd/sslcache(512000)\nSSLSessionCacheTimeout  300\n\n#   Pseudo Random Number Generator (PRNG):\n#   Configure one or more sources to seed the PRNG of the \n#   SSL library. The seed data should be of good random quality.\n#   WARNING! On some platforms /dev/random blocks if not enough entropy\n#   is available. This means you then cannot use the /dev/random device\n#   because it would lead to very long connection times (as long as\n#   it requires to make more entropy available). But usually those\n#   platforms additionally provide a /dev/urandom device which doesn't\n#   block. So, if available, use this one instead. Read the mod_ssl User\n#   Manual for more details.\nSSLRandomSeed startup file:/dev/urandom  256\nSSLRandomSeed connect builtin\n#SSLRandomSeed startup file:/dev/random  512\n#SSLRandomSeed connect file:/dev/random  512\n#SSLRandomSeed connect file:/dev/urandom 512\n\n#\n# Use \"SSLCryptoDevice\" to enable any supported hardware\n# accelerators. Use \"openssl engine -v\" to list supported\n# engine names.  NOTE: If you enable an accelerator and the\n# server does not start, consult the error logs and ensure\n# your accelerator is functioning properly. \n#\nSSLCryptoDevice builtin\n#SSLCryptoDevice ubsec\n\n##\n## SSL Virtual Host Context\n##\n\n<VirtualHost _default_:443>\n\n# General setup for the virtual host, inherited from global configuration\n#DocumentRoot \"/var/www/html\"\n#ServerName www.example.com:443\n\n# Use separate log files for the SSL virtual host; note that LogLevel\n# is not inherited from httpd.conf.\nErrorLog logs/ssl_error_log\nTransferLog logs/ssl_access_log\nLogLevel warn\n\n#   SSL Engine Switch:\n#   Enable/Disable SSL for this virtual host.\nSSLEngine on\n\n#   SSL Protocol support:\n# List the enable protocol levels with which clients will be able to\n# connect.  Disable SSLv2 access by default:\nSSLProtocol all -SSLv2\n\n#   SSL Cipher Suite:\n#   List the ciphers that the client is permitted to negotiate.\n#   See the mod_ssl documentation for a complete list.\nSSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5:!SEED:!IDEA\n\n#   Speed-optimized SSL Cipher configuration:\n#   If speed is your main concern (on busy HTTPS servers e.g.),\n#   you might want to force clients to specific, performance\n#   optimized ciphers. In this case, prepend those ciphers\n#   to the SSLCipherSuite list, and enable SSLHonorCipherOrder.\n#   Caveat: by giving precedence to RC4-SHA and AES128-SHA\n#   (as in the example below), most connections will no longer\n#   have perfect forward secrecy - if the server's key is\n#   compromised, captures of past or future traffic must be\n#   considered compromised, too.\n#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5\n#SSLHonorCipherOrder on \n\n#   Server Certificate:\n# Point SSLCertificateFile at a PEM encoded certificate.  If\n# the certificate is encrypted, then you will be prompted for a\n# pass phrase.  Note that a kill -HUP will prompt again.  A new\n# certificate can be generated using the genkey(1) command.\n\n#   Server Private Key:\n#   If the key is not combined with the certificate, use this\n#   directive to point at the key file.  Keep in mind that if\n#   you've both a RSA and a DSA private key you can configure\n#   both in parallel (to also allow the use of DSA ciphers, etc.)\n\n#   Server Certificate Chain:\n#   Point SSLCertificateChainFile at a file containing the\n#   concatenation of PEM encoded CA certificates which form the\n#   certificate chain for the server certificate. Alternatively\n#   the referenced file can be the same as SSLCertificateFile\n#   when the CA certificates are directly appended to the server\n#   certificate for convenience.\n#SSLCertificateChainFile /etc/pki/tls/certs/server-chain.crt\n\n#   Certificate Authority (CA):\n#   Set the CA certificate verification path where to find CA\n#   certificates for client authentication or alternatively one\n#   huge file containing all of them (file must be PEM encoded)\n#SSLCACertificateFile /etc/pki/tls/certs/ca-bundle.crt\n\n#   Client Authentication (Type):\n#   Client certificate verification type and depth.  Types are\n#   none, optional, require and optional_no_ca.  Depth is a\n#   number which specifies how deeply to verify the certificate\n#   issuer chain before deciding the certificate is not valid.\n#SSLVerifyClient require\n#SSLVerifyDepth  10\n\n#   Access Control:\n#   With SSLRequire you can do per-directory access control based\n#   on arbitrary complex boolean expressions containing server\n#   variable checks and other lookup directives.  The syntax is a\n#   mixture between C and Perl.  See the mod_ssl documentation\n#   for more details.\n#<Location />\n#SSLRequire (    %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \\\n#            and %{SSL_CLIENT_S_DN_O} eq \"Snake Oil, Ltd.\" \\\n#            and %{SSL_CLIENT_S_DN_OU} in {\"Staff\", \"CA\", \"Dev\"} \\\n#            and %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \\\n#            and %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20       ) \\\n#           or %{REMOTE_ADDR} =~ m/^192\\.76\\.162\\.[0-9]+$/\n#</Location>\n\n#   SSL Engine Options:\n#   Set various options for the SSL engine.\n#   o FakeBasicAuth:\n#     Translate the client X.509 into a Basic Authorisation.  This means that\n#     the standard Auth/DBMAuth methods can be used for access control.  The\n#     user name is the `one line' version of the client's X.509 certificate.\n#     Note that no password is obtained from the user. Every entry in the user\n#     file needs this password: `xxj31ZMTZzkVA'.\n#   o ExportCertData:\n#     This exports two additional environment variables: SSL_CLIENT_CERT and\n#     SSL_SERVER_CERT. These contain the PEM-encoded certificates of the\n#     server (always existing) and the client (only existing when client\n#     authentication is used). This can be used to import the certificates\n#     into CGI scripts.\n#   o StdEnvVars:\n#     This exports the standard SSL/TLS related `SSL_*' environment variables.\n#     Per default this exportation is switched off for performance reasons,\n#     because the extraction step is an expensive operation and is usually\n#     useless for serving static content. So one usually enables the\n#     exportation for CGI and SSI requests only.\n#   o StrictRequire:\n#     This denies access when \"SSLRequireSSL\" or \"SSLRequire\" applied even\n#     under a \"Satisfy any\" situation, i.e. when it applies access is denied\n#     and no other module can change it.\n#   o OptRenegotiate:\n#     This enables optimized SSL connection renegotiation handling when SSL\n#     directives are used in per-directory context. \n#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire\n<Files ~ \"\\.(cgi|shtml|phtml|php3?)$\">\n    SSLOptions +StdEnvVars\n</Files>\n<Directory \"/var/www/cgi-bin\">\n    SSLOptions +StdEnvVars\n</Directory>\n\n#   SSL Protocol Adjustments:\n#   The safe and default but still SSL/TLS standard compliant shutdown\n#   approach is that mod_ssl sends the close notify alert but doesn't wait for\n#   the close notify alert from client. When you need a different shutdown\n#   approach you can use one of the following variables:\n#   o ssl-unclean-shutdown:\n#     This forces an unclean shutdown when the connection is closed, i.e. no\n#     SSL close notify alert is send or allowed to received.  This violates\n#     the SSL/TLS standard but is needed for some brain-dead browsers. Use\n#     this when you receive I/O errors because of the standard approach where\n#     mod_ssl sends the close notify alert.\n#   o ssl-accurate-shutdown:\n#     This forces an accurate shutdown when the connection is closed, i.e. a\n#     SSL close notify alert is send and mod_ssl waits for the close notify\n#     alert of the client. This is 100% SSL/TLS standard compliant, but in\n#     practice often causes hanging connections with brain-dead browsers. Use\n#     this only for browsers where you know that their SSL implementation\n#     works correctly. \n#   Notice: Most problems of broken clients are also related to the HTTP\n#   keep-alive facility, so you usually additionally want to disable\n#   keep-alive for those clients, too. Use variable \"nokeepalive\" for this.\n#   Similarly, one has to force some clients to use HTTP/1.0 to workaround\n#   their broken HTTP/1.1 implementation. Use variables \"downgrade-1.0\" and\n#   \"force-response-1.0\" for this.\nBrowserMatch \"MSIE [2-5]\" nokeepalive ssl-unclean-shutdown downgrade-1.0 force-response-1.0\n\n#   Per-Server Logging:\n#   The home of a custom SSL log file. Use this when you want a\n#   compact non-error SSL logfile on a virtual host basis.\nCustomLog logs/ssl_request_log \"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \\\"%r\\\" %b\"\n</VirtualHost>                                  \n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/userdir.conf",
    "content": "#\n# UserDir: The name of the directory that is appended onto a user's home\n# directory if a ~user request is received.\n#\n# The path to the end user account 'public_html' directory must be\n# accessible to the webserver userid.  This usually means that ~userid\n# must have permissions of 711, ~userid/public_html must have permissions\n# of 755, and documents contained therein must be world-readable.\n# Otherwise, the client will only receive a \"403 Forbidden\" message.\n#\n<IfModule mod_userdir.c>\n    #\n    # UserDir is disabled by default since it can confirm the presence\n    # of a username on the system (depending on home directory\n    # permissions).\n    #\n    UserDir disabled\n\n    #\n    # To enable requests to /~user/ to serve the user's public_html\n    # directory, remove the \"UserDir disabled\" line above, and uncomment\n    # the following line instead:\n    # \n    #UserDir public_html\n</IfModule>\n\n#\n# Control access to UserDir directories.  The following is an example\n# for a site where these directories are restricted to read-only.\n#\n<Directory \"/home/*/public_html\">\n    AllowOverride FileInfo AuthConfig Limit Indexes\n    Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec\n    Require method GET POST OPTIONS\n</Directory>\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.d/welcome.conf",
    "content": "# \n# This configuration file enables the default \"Welcome\" page if there\n# is no default index page present for the root URL.  To disable the\n# Welcome page, comment out all the lines below. \n#\n# NOTE: if this file is removed, it will be restored on upgrades.\n#\n<LocationMatch \"^/+$\">\n    Options -Indexes\n    ErrorDocument 403 /.noindex.html\n</LocationMatch>\n\n<Directory /usr/share/httpd/noindex>\n    AllowOverride None\n    Require all granted\n</Directory>\n\nAlias /.noindex.html /usr/share/httpd/noindex/index.html\nAlias /noindex/css/bootstrap.min.css /usr/share/httpd/noindex/css/bootstrap.min.css\nAlias /noindex/css/open-sans.css /usr/share/httpd/noindex/css/open-sans.css\nAlias /images/apache_pb.gif /usr/share/httpd/noindex/images/apache_pb.gif\nAlias /images/poweredby.png /usr/share/httpd/noindex/images/poweredby.png\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-base.conf",
    "content": "#\n# This file loads most of the modules included with the Apache HTTP\n# Server itself.\n#\n\nLoadModule access_compat_module modules/mod_access_compat.so\nLoadModule actions_module modules/mod_actions.so\nLoadModule alias_module modules/mod_alias.so\nLoadModule allowmethods_module modules/mod_allowmethods.so\nLoadModule auth_basic_module modules/mod_auth_basic.so\nLoadModule auth_digest_module modules/mod_auth_digest.so\nLoadModule authn_anon_module modules/mod_authn_anon.so\nLoadModule authn_core_module modules/mod_authn_core.so\nLoadModule authn_dbd_module modules/mod_authn_dbd.so\nLoadModule authn_dbm_module modules/mod_authn_dbm.so\nLoadModule authn_file_module modules/mod_authn_file.so\nLoadModule authn_socache_module modules/mod_authn_socache.so\nLoadModule authz_core_module modules/mod_authz_core.so\nLoadModule authz_dbd_module modules/mod_authz_dbd.so\nLoadModule authz_dbm_module modules/mod_authz_dbm.so\nLoadModule authz_groupfile_module modules/mod_authz_groupfile.so\nLoadModule authz_host_module modules/mod_authz_host.so\nLoadModule authz_owner_module modules/mod_authz_owner.so\nLoadModule authz_user_module modules/mod_authz_user.so\nLoadModule autoindex_module modules/mod_autoindex.so\nLoadModule cache_module modules/mod_cache.so\nLoadModule cache_disk_module modules/mod_cache_disk.so\nLoadModule data_module modules/mod_data.so\nLoadModule dbd_module modules/mod_dbd.so\nLoadModule deflate_module modules/mod_deflate.so\nLoadModule dir_module modules/mod_dir.so\nLoadModule dumpio_module modules/mod_dumpio.so\nLoadModule echo_module modules/mod_echo.so\nLoadModule env_module modules/mod_env.so\nLoadModule expires_module modules/mod_expires.so\nLoadModule ext_filter_module modules/mod_ext_filter.so\nLoadModule filter_module modules/mod_filter.so\nLoadModule headers_module modules/mod_headers.so\nLoadModule include_module modules/mod_include.so\nLoadModule info_module modules/mod_info.so\nLoadModule log_config_module modules/mod_log_config.so\nLoadModule logio_module modules/mod_logio.so\nLoadModule mime_magic_module modules/mod_mime_magic.so\nLoadModule mime_module modules/mod_mime.so\nLoadModule negotiation_module modules/mod_negotiation.so\nLoadModule remoteip_module modules/mod_remoteip.so\nLoadModule reqtimeout_module modules/mod_reqtimeout.so\nLoadModule rewrite_module modules/mod_rewrite.so\nLoadModule setenvif_module modules/mod_setenvif.so\nLoadModule slotmem_plain_module modules/mod_slotmem_plain.so\nLoadModule slotmem_shm_module modules/mod_slotmem_shm.so\nLoadModule socache_dbm_module modules/mod_socache_dbm.so\nLoadModule socache_memcache_module modules/mod_socache_memcache.so\nLoadModule socache_shmcb_module modules/mod_socache_shmcb.so\nLoadModule status_module modules/mod_status.so\nLoadModule substitute_module modules/mod_substitute.so\nLoadModule suexec_module modules/mod_suexec.so\nLoadModule unique_id_module modules/mod_unique_id.so\nLoadModule unixd_module modules/mod_unixd.so\nLoadModule userdir_module modules/mod_userdir.so\nLoadModule version_module modules/mod_version.so\nLoadModule vhost_alias_module modules/mod_vhost_alias.so\n\n#LoadModule buffer_module modules/mod_buffer.so\n#LoadModule watchdog_module modules/mod_watchdog.so\n#LoadModule heartbeat_module modules/mod_heartbeat.so\n#LoadModule heartmonitor_module modules/mod_heartmonitor.so\n#LoadModule usertrack_module modules/mod_usertrack.so\n#LoadModule dialup_module modules/mod_dialup.so\n#LoadModule charset_lite_module modules/mod_charset_lite.so\n#LoadModule log_debug_module modules/mod_log_debug.so\n#LoadModule ratelimit_module modules/mod_ratelimit.so\n#LoadModule reflector_module modules/mod_reflector.so\n#LoadModule request_module modules/mod_request.so\n#LoadModule sed_module modules/mod_sed.so\n#LoadModule speling_module modules/mod_speling.so\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-dav.conf",
    "content": "LoadModule dav_module modules/mod_dav.so\nLoadModule dav_fs_module modules/mod_dav_fs.so\nLoadModule dav_lock_module modules/mod_dav_lock.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-lua.conf",
    "content": "LoadModule lua_module modules/mod_lua.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-mpm.conf",
    "content": "# Select the MPM module which should be used by uncommenting exactly\n# one of the following LoadModule lines:\n\n# prefork MPM: Implements a non-threaded, pre-forking web server\n# See: http://httpd.apache.org/docs/2.4/mod/prefork.html\nLoadModule mpm_prefork_module modules/mod_mpm_prefork.so\n\n# worker MPM: Multi-Processing Module implementing a hybrid\n# multi-threaded multi-process web server\n# See: http://httpd.apache.org/docs/2.4/mod/worker.html\n#\n#LoadModule mpm_worker_module modules/mod_mpm_worker.so\n\n# event MPM: A variant of the worker MPM with the goal of consuming\n# threads only for connections with active processing\n# See: http://httpd.apache.org/docs/2.4/mod/event.html\n#\n#LoadModule mpm_event_module modules/mod_mpm_event.so\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-proxy.conf",
    "content": "# This file configures all the proxy modules:\nLoadModule proxy_module modules/mod_proxy.so\nLoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so\nLoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so\nLoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so\nLoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so\nLoadModule proxy_ajp_module modules/mod_proxy_ajp.so\nLoadModule proxy_balancer_module modules/mod_proxy_balancer.so\nLoadModule proxy_connect_module modules/mod_proxy_connect.so\nLoadModule proxy_express_module modules/mod_proxy_express.so\nLoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so\nLoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so\nLoadModule proxy_ftp_module modules/mod_proxy_ftp.so\nLoadModule proxy_http_module modules/mod_proxy_http.so\nLoadModule proxy_scgi_module modules/mod_proxy_scgi.so\nLoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-ssl.conf",
    "content": "LoadModule ssl_module modules/mod_ssl.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/00-systemd.conf",
    "content": "# This file configures systemd module:\nLoadModule systemd_module modules/mod_systemd.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/httpd/conf.modules.d/01-cgi.conf",
    "content": "# This configuration file loads a CGI module appropriate to the MPM\n# which has been configured in 00-mpm.conf.  mod_cgid should be used\n# with a threaded MPM; mod_cgi with the prefork MPM.\n\n<IfModule mpm_worker_module>\n   LoadModule cgid_module modules/mod_cgid.so\n</IfModule>\n<IfModule mpm_event_module>\n   LoadModule cgid_module modules/mod_cgid.so\n</IfModule>\n<IfModule mpm_prefork_module>\n   LoadModule cgi_module modules/mod_cgi.so\n</IfModule>\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/sites",
    "content": "conf.d/centos.example.com.conf, centos.example.com\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/centos7_apache/apache/sysconfig/httpd",
    "content": "#\n# This file can be used to set additional environment variables for\n# the httpd process, or pass additional options to the httpd\n# executable.\n#\n# Note: With previous versions of httpd, the MPM could be changed by\n# editing an \"HTTPD\" variable here.  With the current version, that\n# variable is now ignored.  The MPM is a loadable module, and the\n# choice of MPM can be changed by editing the configuration file\n# /etc/httpd/conf.modules.d/00-mpm.conf.\n# \n\n#\n# To pass additional options (for instance, -D definitions) to the\n# httpd binary at startup, set OPTIONS here.\n#\nOPTIONS=\"-D mock_define -D mock_define_too -D mock_value=TRUE -DMOCK_NOSEP -DNOSEP_TWO=NOSEP_VAL\"\n\n#\n# This setting ensures the httpd process is started in the \"C\" locale\n# by default.  (Some modules will not behave correctly if\n# case-sensitive string comparisons are performed in a different\n# locale.)\n#\nLANG=C\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/complex_parsing/apache2.conf",
    "content": "# Global configuration\n\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\nErrorLog ${APACHE_LOG_DIR}/error.log\n\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /var/www/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/\n\n# Include the virtual host configurations:\nIncludeOptional sites-enabled/*.conf\n\nDefine COMPLEX\n\nDefine tls_port 1234\nDefine tls_port_str \"1234\"\n\nDefine fnmatch_filename test_fnmatch.conf\n\n\nInclude test_variables.conf\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/complex_parsing/conf-enabled/dummy.conf",
    "content": "# 3 - one arg directives\n# 2 - two arg directives\n# 1 - three arg directives\nTestArgsDirective one_arg\nTestArgsDirective one_arg two_arg\nTestArgsDirective one_arg\nTestArgsDirective one_arg two_arg\nTestArgsDirective one_arg two_arg three_arg\nTestArgsDirective one_arg\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/complex_parsing/test_fnmatch.conf",
    "content": "FNMATCH_DIRECTIVE Success\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/complex_parsing/test_variables.conf",
    "content": "TestVariablePort ${tls_port}\nTestVariablePortStr \"${tls_port_str}\"\n\nLoadModule status_module modules/mod_status.so\n\n# Basic IfDefine\n<IfDefine COMPLEX>\n    VAR_DIRECTIVE success\n    LoadModule ssl_module modules/mod_ssl.so\n</IfDefine>\n\n<IfDefine !COMPLEX>\n    INVALID_VAR_DIRECTIVE failure\n</IfDefine>\n\n<IfDefine NOT_COMPLEX>\n    INVALID_VAR_DIRECTIVE failure\n</IfDefine>\n\n<IfDefine !NOT_COMPLEX>\n    VAR_DIRECTIVE failure\n</IfDefine>\n\n\n# Basic IfModule\n<IfModule ssl_module>\n    MOD_DIRECTIVE Success\n</IfModule>\n\n<IfModule !ssl_module>\n    INVALID_MOD_DIRECTIVE failure\n</IfModule>\n\n<IfModule fake_module>\n    INVALID_MOD_DIRECTIVE failure\n</IfModule>\n\n<IfModule !fake_module>\n    MOD_DIRECTIVE Success\n</IfModule>\n\n# Nested Tests\n<IfModule status_module>\n    <IfDefine COMPLEX>\n        NESTED_DIRECTIVE success\n\n        <IfModule mod_ssl.c>\n            NESTED_DIRECTIVE success\n        </IfModule>\n\n        <IfModule !mod_ssl.c>\n            INVALID_NESTED_DIRECTIVE failure\n        </IfModule>\n    </IfDefine>\n\n    <IfDefine !COMPLEX>\n        INVALID_NESTED_DIRECTIVE failure\n\n        <IfModule ssl_module>\n            INVALID_NESTED_DIRECTIVE failure\n        </IfModule>\n    </IfDefine>\n\n    NESTED_DIRECTIVE success\n\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/apache2.conf",
    "content": "# This is the main Apache server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See http://httpd.apache.org/docs/2.4/ for detailed information about\n# the directives and /usr/share/doc/apache2/README.Debian about Debian specific\n# hints.\n#\n#\n# Summary of how the Apache 2 configuration works in Debian:\n# The Apache 2 web server configuration in Debian is quite different to\n# upstream's suggested way to configure the web server. This is because Debian's\n# default Apache2 installation attempts to make adding and removing modules,\n# virtual hosts, and extra configuration directives as flexible as possible, in\n# order to make automating the changes and administering the server as easy as\n# possible.\n\n# It is split into several files forming the configuration hierarchy outlined\n# below, all located in the /etc/apache2/ directory:\n#\n#\t/etc/apache2/\n#\t|-- apache2.conf\n#\t|\t`--  ports.conf\n#\t|-- mods-enabled\n#\t|\t|-- *.load\n#\t|\t`-- *.conf\n#\t|-- conf-enabled\n#\t|\t`-- *.conf\n# \t`-- sites-enabled\n#\t \t`-- *.conf\n#\n#\n# * apache2.conf is the main configuration file (this file). It puts the pieces\n#   together by including all remaining configuration files when starting up the\n#   web server.\n#\n# * ports.conf is always included from the main configuration file. It is\n#   supposed to determine listening ports for incoming connections which can be\n#   customized anytime.\n#\n# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/\n#   directories contain particular configuration snippets which manage modules,\n#   global configuration fragments, or virtual host configurations,\n#   respectively.\n#\n#   They are activated by symlinking available configuration files from their\n#   respective *-available/ counterparts. These should be managed by using our\n#   helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See\n#   their respective man pages for detailed information.\n#\n# * The binary is called apache2. Due to the use of environment variables, in\n#   the default configuration, apache2 needs to be started/stopped with\n#   /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not\n#   work with the default configuration.\n\n\n# Global configuration\n\n#\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n#\nMutex file:${APACHE_LOCK_DIR} default\n\n#\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n# This needs to be set in /etc/apache2/envvars\n#\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n#\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\n#\nMaxKeepAliveRequests 100\n\n#\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\n#\nKeepAliveTimeout 5\n\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\n#\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\n#\nHostnameLookups Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n#\n# LogLevel: Control the severity of messages logged to the error_log.\n# Available values: trace8, ..., trace1, debug, info, notice, warn,\n# error, crit, alert, emerg.\n# It is also possible to configure the log level for particular modules, e.g.\n# \"LogLevel info ssl:warn\"\n#\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n# Include list of ports to listen on\nInclude ports.conf\n\n\n# Sets the default security model of the Apache2 HTTPD server. It does\n# not allow access to the root filesystem outside of /usr/share and /var/www.\n# The former is used by web applications packaged in Debian,\n# the latter may be used for local directories served by the web server. If\n# your system is serving content from a sub-directory in /srv you must allow\n# access here, or in any related virtual host.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /usr/share>\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n<Directory /var/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\n#\nAccessFileName .htaccess\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n#\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n# The following directives define some format nicknames for use with\n# a CustomLog directive.\n#\n# These deviate from the Common Log Format definitions in that they use %O\n# (the actual bytes sent including headers) instead of %b (the size of the\n# requested file), because the latter makes it impossible to detect partial\n# requests.\n#\n# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.\n# Use mod_remoteip instead.\n#\nLogFormat \"%v:%p %h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" vhost_combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O\" common\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-agent}i\" agent\n\n# Include of directories ignores editors' and dpkg's backup files,\n# see README.Debian for details.\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/*.conf\n\n# Include the virtual host configurations:\nIncludeOptional sites-enabled/*.conf\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/bad_conf_file.conf",
    "content": "<VirtualHost 1.1.1.1>\n\nServerName invalid.net\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/other-vhosts-access-log.conf",
    "content": "# Define an access log for VirtualHosts that don't define their own logfile\nCustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/security.conf",
    "content": "# Changing the following options will not really affect the security of the\n# server, but might make attacks slightly more difficult in some cases.\n\n#\n# ServerTokens\n# This directive configures what you return as the Server HTTP response\n# Header. The default is 'Full' which sends information about the OS-Type\n# and compiled in modules.\n# Set to one of:  Full | OS | Minimal | Minor | Major | Prod\n# where Full conveys the most information, and Prod the least.\n#ServerTokens Minimal\nServerTokens OS\n#ServerTokens Full\n\n#\n# Optionally add a line containing the server version and virtual host\n# name to server-generated pages (internal error documents, FTP directory\n# listings, mod_status and mod_info output etc., but not CGI generated\n# documents or custom error documents).\n# Set to \"EMail\" to also include a mailto: link to the ServerAdmin.\n# Set to one of:  On | Off | EMail\n#ServerSignature Off\nServerSignature On\n\n#\n# Allow TRACE method\n#\n# Set to \"extended\" to also reflect the request body (only for testing and\n# diagnostic purposes).\n#\n# Set to one of:  On | Off | extended\nTraceEnable Off\n#TraceEnable On\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/conf-available/serve-cgi-bin.conf",
    "content": "<IfModule mod_alias.c>\n\t<IfModule mod_cgi.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfModule mod_cgid.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfDefine ENABLE_USR_LIB_CGI_BIN>\n\t\tScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n\t\t<Directory \"/usr/lib/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n\t\t\tRequire all granted\n\t\t</Directory>\n\t</IfDefine>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/envvars",
    "content": "# envvars - default environment variables for apache2ctl\n\n# this won't be correct after changing uid\nunset HOME\n\n# for supporting multiple apache2 instances\nif [ \"${APACHE_CONFDIR##/etc/apache2-}\" != \"${APACHE_CONFDIR}\" ] ; then\n\tSUFFIX=\"-${APACHE_CONFDIR##/etc/apache2-}\"\nelse\n\tSUFFIX=\nfi\n\n# Since there is no sane way to get the parsed apache2 config in scripts, some\n# settings are defined via environment variables and then used in apache2ctl,\n# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.\nexport APACHE_RUN_USER=www-data\nexport APACHE_RUN_GROUP=www-data\n# temporary state file location. This might be changed to /run in Wheezy+1\nexport APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid\nexport APACHE_RUN_DIR=/var/run/apache2$SUFFIX\nexport APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX\n# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.\nexport APACHE_LOG_DIR=/var/log/apache2$SUFFIX\n\n## The locale used by some modules like mod_dav\nexport LANG=C\n\nexport LANG\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/authz_svn.load",
    "content": "# Depends: dav_svn\n<IfModule !mod_dav_svn.c>\n    Include mods-enabled/dav_svn.load\n</IfModule>\nLoadModule authz_svn_module /usr/lib/apache2/modules/mod_authz_svn.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav.load",
    "content": "<IfModule !mod_dav.c>\n\tLoadModule dav_module /usr/lib/apache2/modules/mod_dav.so\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.conf",
    "content": "# dav_svn.conf - Example Subversion/Apache configuration\n#\n# For details and further options see the Apache user manual and\n# the Subversion book.\n#\n# NOTE: for a setup with multiple vhosts, you will want to do this\n# configuration in /etc/apache2/sites-available/*, not here.\n\n# <Location URL> ... </Location>\n# URL controls how the repository appears to the outside world.\n# In this example clients access the repository as http://hostname/svn/\n# Note, a literal /svn should NOT exist in your document root.\n#<Location /svn>\n\n  # Uncomment this to enable the repository\n  #DAV svn\n\n  # Set this to the path to your repository\n  #SVNPath /var/lib/svn\n  # Alternatively, use SVNParentPath if you have multiple repositories under\n  # under a single directory (/var/lib/svn/repo1, /var/lib/svn/repo2, ...).\n  # You need either SVNPath and SVNParentPath, but not both.\n  #SVNParentPath /var/lib/svn\n\n  # Access control is done at 3 levels: (1) Apache authentication, via\n  # any of several methods.  A \"Basic Auth\" section is commented out\n  # below.  (2) Apache <Limit> and <LimitExcept>, also commented out\n  # below.  (3) mod_authz_svn is a svn-specific authorization module\n  # which offers fine-grained read/write access control for paths\n  # within a repository.  (The first two layers are coarse-grained; you\n  # can only enable/disable access to an entire repository.)  Note that\n  # mod_authz_svn is noticeably slower than the other two layers, so if\n  # you don't need the fine-grained control, don't configure it.\n\n  # Basic Authentication is repository-wide.  It is not secure unless\n  # you are using https.  See the 'htpasswd' command to create and\n  # manage the password file - and the documentation for the\n  # 'auth_basic' and 'authn_file' modules, which you will need for this\n  # (enable them with 'a2enmod').\n  #AuthType Basic\n  #AuthName \"Subversion Repository\"\n  #AuthUserFile /etc/apache2/dav_svn.passwd\n\n  # To enable authorization via mod_authz_svn (enable that module separately):\n  #<IfModule mod_authz_svn.c>\n  #AuthzSVNAccessFile /etc/apache2/dav_svn.authz\n  #</IfModule>\n\n  # The following three lines allow anonymous read, but make\n  # committers authenticate themselves.  It requires the 'authz_user'\n  # module (enable it with 'a2enmod').\n  #<LimitExcept GET PROPFIND OPTIONS REPORT>\n    #Require valid-user\n  #</LimitExcept> \n\n#</Location>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/dav_svn.load",
    "content": "# Depends: dav\n<IfModule !mod_dav_svn.c>\n    <IfModule !mod_dav.c>\n        Include mods-enabled/dav.load\n    </IfModule>\n    LoadModule dav_svn_module /usr/lib/apache2/modules/mod_dav_svn.so\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/rewrite.load",
    "content": "LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.conf",
    "content": "<IfModule mod_ssl.c>\n\n\t# Pseudo Random Number Generator (PRNG):\n\t# Configure one or more sources to seed the PRNG of the SSL library.\n\t# The seed data should be of good random quality.\n\t# WARNING! On some platforms /dev/random blocks if not enough entropy\n\t# is available. This means you then cannot use the /dev/random device\n\t# because it would lead to very long connection times (as long as\n\t# it requires to make more entropy available). But usually those\n\t# platforms additionally provide a /dev/urandom device which doesn't\n\t# block. So, if available, use this one instead. Read the mod_ssl User\n\t# Manual for more details.\n\t#\n\tSSLRandomSeed startup builtin\n\tSSLRandomSeed startup file:/dev/urandom 512\n\tSSLRandomSeed connect builtin\n\tSSLRandomSeed connect file:/dev/urandom 512\n\n\t##\n\t##  SSL Global Context\n\t##\n\t##  All SSL configuration in this context applies both to\n\t##  the main server and all SSL-enabled virtual hosts.\n\t##\n\n\t#\n\t#   Some MIME-types for downloading Certificates and CRLs\n\t#\n\tAddType application/x-x509-ca-cert .crt\n\tAddType application/x-pkcs7-crl\t.crl\n\n\t#   Pass Phrase Dialog:\n\t#   Configure the pass phrase gathering process.\n\t#   The filtering dialog program (`builtin' is an internal\n\t#   terminal dialog) has to provide the pass phrase on stdout.\n\tSSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase\n\n\t#   Inter-Process Session Cache:\n\t#   Configure the SSL Session Cache: First the mechanism \n\t#   to use and second the expiring timeout (in seconds).\n\t#   (The mechanism dbm has known memory leaks and should not be used).\n\t#SSLSessionCache\t\t dbm:${APACHE_RUN_DIR}/ssl_scache\n\tSSLSessionCache\t\tshmcb:${APACHE_RUN_DIR}/ssl_scache(512000)\n\tSSLSessionCacheTimeout  300\n\n\t#   Semaphore:\n\t#   Configure the path to the mutual exclusion semaphore the\n\t#   SSL engine uses internally for inter-process synchronization. \n\t#   (Disabled by default, the global Mutex directive consolidates by default\n\t#   this)\n\t#Mutex file:${APACHE_LOCK_DIR}/ssl_mutex ssl-cache\n\n\n\t#   SSL Cipher Suite:\n\t#   List the ciphers that the client is permitted to negotiate. See the\n\t#   ciphers(1) man page from the openssl package for list of all available\n\t#   options.\n\t#   Enable only secure ciphers:\n\tSSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5\n\n\t#   Speed-optimized SSL Cipher configuration:\n\t#   If speed is your main concern (on busy HTTPS servers e.g.),\n\t#   you might want to force clients to specific, performance\n\t#   optimized ciphers. In this case, prepend those ciphers\n\t#   to the SSLCipherSuite list, and enable SSLHonorCipherOrder.\n\t#   Caveat: by giving precedence to RC4-SHA and AES128-SHA\n\t#   (as in the example below), most connections will no longer\n\t#   have perfect forward secrecy - if the server's key is\n\t#   compromised, captures of past or future traffic must be\n\t#   considered compromised, too.\n\t#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5\n\t#SSLHonorCipherOrder on\n\n\t#   The protocols to enable.\n\t#   Available values: all, SSLv3, TLSv1, TLSv1.1, TLSv1.2\n\t#   SSL v2  is no longer supported\n\tSSLProtocol all\n\n\t#   Allow insecure renegotiation with clients which do not yet support the\n\t#   secure renegotiation protocol. Default: Off\n\t#SSLInsecureRenegotiation on\n\n\t#   Whether to forbid non-SNI clients to access name based virtual hosts.\n\t#   Default: Off\n\t#SSLStrictSNIVHostCheck On\n\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/mods-available/ssl.load",
    "content": "# Depends: setenvif mime socache_shmcb\nLoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/ports.conf",
    "content": "# If you just change the port or add more ports here, you will likely also\n# have to change the VirtualHost statement in\n# /etc/apache2/sites-enabled/000-default.conf\n\nListen 80\n\n<IfModule ssl_module>\n\tListen 443\n</IfModule>\n\n<IfModule mod_gnutls.c>\n\tListen 443\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/another_wildcard.conf",
    "content": "<VirtualHost *:80>\n\tServerName wildcard.tld\n\tServerAlias ?.wildcard.tld\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/old-and-default.conf",
    "content": "<VirtualHost *:80 [::]:80>\n\n\tServerName ip-172-30-0-17\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/apache2/sites-available/wildcard.conf",
    "content": "<VirtualHost *:80>\n\tServerName example.net\n\tServerAlias ??.example.net *.other.example.net *another.example.net\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/augeas_vhosts/sites",
    "content": "sites-available/certbot.conf, certbot.demo\nsites-available/encryption-example.conf, encryption-example.demo\nsites-available/ocsp-ssl.conf, ocspvhost.com\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/apache2.conf",
    "content": "# This is the main Apache server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See http://httpd.apache.org/docs/2.4/ for detailed information about\n# the directives and /usr/share/doc/apache2/README.Debian about Debian specific\n# hints.\n#\n#\n# Summary of how the Apache 2 configuration works in Debian:\n# The Apache 2 web server configuration in Debian is quite different to\n# upstream's suggested way to configure the web server. This is because Debian's\n# default Apache2 installation attempts to make adding and removing modules,\n# virtual hosts, and extra configuration directives as flexible as possible, in\n# order to make automating the changes and administering the server as easy as\n# possible.\n\n# It is split into several files forming the configuration hierarchy outlined\n# below, all located in the /etc/apache2/ directory:\n#\n#\t/etc/apache2/\n#\t|-- apache2.conf\n#\t|\t`--  ports.conf\n#\t|-- mods-enabled\n#\t|\t|-- *.load\n#\t|\t`-- *.conf\n#\t|-- conf-enabled\n#\t|\t`-- *.conf\n# \t`-- sites-enabled\n#\t \t`-- *.conf\n#\n#\n# * apache2.conf is the main configuration file (this file). It puts the pieces\n#   together by including all remaining configuration files when starting up the\n#   web server.\n#\n# * ports.conf is always included from the main configuration file. It is\n#   supposed to determine listening ports for incoming connections which can be\n#   customized anytime.\n#\n# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/\n#   directories contain particular configuration snippets which manage modules,\n#   global configuration fragments, or virtual host configurations,\n#   respectively.\n#\n#   They are activated by symlinking available configuration files from their\n#   respective *-available/ counterparts. These should be managed by using our\n#   helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See\n#   their respective man pages for detailed information.\n#\n# * The binary is called apache2. Due to the use of environment variables, in\n#   the default configuration, apache2 needs to be started/stopped with\n#   /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not\n#   work with the default configuration.\n\n\n# Global configuration\n\n#\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n#\nMutex file:${APACHE_LOCK_DIR} default\n\n#\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n# This needs to be set in /etc/apache2/envvars\n#\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n#\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\n#\nMaxKeepAliveRequests 100\n\n#\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\n#\nKeepAliveTimeout 5\n\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\n#\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\n#\nHostnameLookups Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n#\n# LogLevel: Control the severity of messages logged to the error_log.\n# Available values: trace8, ..., trace1, debug, info, notice, warn,\n# error, crit, alert, emerg.\n# It is also possible to configure the log level for particular modules, e.g.\n# \"LogLevel info ssl:warn\"\n#\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n# Include list of ports to listen on\nInclude ports.conf\n\n\n# Sets the default security model of the Apache2 HTTPD server. It does\n# not allow access to the root filesystem outside of /usr/share and /var/www.\n# The former is used by web applications packaged in Debian,\n# the latter may be used for local directories served by the web server. If\n# your system is serving content from a sub-directory in /srv you must allow\n# access here, or in any related virtual host.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /usr/share>\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n<Directory /var/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\n#\nAccessFileName .htaccess\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n#\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n\n#\n# The following directives define some format nicknames for use with\n# a CustomLog directive.\n#\n# These deviate from the Common Log Format definitions in that they use %O\n# (the actual bytes sent including headers) instead of %b (the size of the\n# requested file), because the latter makes it impossible to detect partial\n# requests.\n#\n# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.\n# Use mod_remoteip instead.\n#\nLogFormat \"%v:%p %h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" vhost_combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O\" common\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-agent}i\" agent\n\n# Include of directories ignores editors' and dpkg's backup files,\n# see README.Debian for details.\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/*.conf\n\n# Include the virtual host configurations:\nIncludeOptional sites-enabled/*.conf\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/other-vhosts-access-log.conf",
    "content": "# Define an access log for VirtualHosts that don't define their own logfile\nCustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/security.conf",
    "content": "# ServerTokens\n# This directive configures what you return as the Server HTTP response\n# Header. The default is 'Full' which sends information about the OS-Type\n# and compiled in modules.\n# Set to one of:  Full | OS | Minimal | Minor | Major | Prod\n# where Full conveys the most information, and Prod the least.\n#ServerTokens Minimal\nServerTokens OS\n#ServerTokens Full\n\n#\n# Optionally add a line containing the server version and virtual host\n# name to server-generated pages (internal error documents, FTP directory\n# listings, mod_status and mod_info output etc., but not CGI generated\n# documents or custom error documents).\n# Set to \"EMail\" to also include a mailto: link to the ServerAdmin.\n# Set to one of:  On | Off | EMail\n#ServerSignature Off\nServerSignature On\n\n#\n# Allow TRACE method\n#\n# Set to \"extended\" to also reflect the request body (only for testing and\n# diagnostic purposes).\n#\n# Set to one of:  On | Off | extended\nTraceEnable Off\n#TraceEnable On\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/conf-available/serve-cgi-bin.conf",
    "content": "<IfModule mod_alias.c>\n\t<IfModule mod_cgi.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfModule mod_cgid.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfDefine ENABLE_USR_LIB_CGI_BIN>\n\t\tScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n\t\t<Directory \"/usr/lib/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n\t\t\tRequire all granted\n\t\t</Directory>\n\t</IfDefine>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/envvars",
    "content": "# envvars - default environment variables for apache2ctl\n\n# this won't be correct after changing uid\nunset HOME\n\n# for supporting multiple apache2 instances\nif [ \"${APACHE_CONFDIR##/etc/apache2-}\" != \"${APACHE_CONFDIR}\" ] ; then\n\tSUFFIX=\"-${APACHE_CONFDIR##/etc/apache2-}\"\nelse\n\tSUFFIX=\nfi\n\n# Since there is no sane way to get the parsed apache2 config in scripts, some\n# settings are defined via environment variables and then used in apache2ctl,\n# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.\nexport APACHE_RUN_USER=www-data\nexport APACHE_RUN_GROUP=www-data\n# temporary state file location. This might be changed to /run in Wheezy+1\nexport APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid\nexport APACHE_RUN_DIR=/var/run/apache2$SUFFIX\nexport APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX\n# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.\nexport APACHE_LOG_DIR=/var/log/apache2$SUFFIX\n\n## The locale used by some modules like mod_dav\nexport LANG=C\n\nexport LANG\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.conf",
    "content": "<IfModule mod_ssl.c>\n\n\t# Pseudo Random Number Generator (PRNG):\n\t# Configure one or more sources to seed the PRNG of the SSL library.\n\t# The seed data should be of good random quality.\n\t# WARNING! On some platforms /dev/random blocks if not enough entropy\n\t# is available. This means you then cannot use the /dev/random device\n\t# because it would lead to very long connection times (as long as\n\t# it requires to make more entropy available). But usually those\n\t# platforms additionally provide a /dev/urandom device which doesn't\n\t# block. So, if available, use this one instead. Read the mod_ssl User\n\t# Manual for more details.\n\t#\n\tSSLRandomSeed startup builtin\n\tSSLRandomSeed startup file:/dev/urandom 512\n\tSSLRandomSeed connect builtin\n\tSSLRandomSeed connect file:/dev/urandom 512\n\n\t##\n\t##  SSL Global Context\n\t##\n\t##  All SSL configuration in this context applies both to\n\t##  the main server and all SSL-enabled virtual hosts.\n\t##\n\n\t#\n\t#   Some MIME-types for downloading Certificates and CRLs\n\t#\n\tAddType application/x-x509-ca-cert .crt\n\tAddType application/x-pkcs7-crl\t.crl\n\n\t#   Pass Phrase Dialog:\n\t#   Configure the pass phrase gathering process.\n\t#   The filtering dialog program (`builtin' is an internal\n\t#   terminal dialog) has to provide the pass phrase on stdout.\n\tSSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase\n\n\t#   Inter-Process Session Cache:\n\t#   Configure the SSL Session Cache: First the mechanism \n\t#   to use and second the expiring timeout (in seconds).\n\t#   (The mechanism dbm has known memory leaks and should not be used).\n\t#SSLSessionCache\t\t dbm:${APACHE_RUN_DIR}/ssl_scache\n\tSSLSessionCache\t\tshmcb:${APACHE_RUN_DIR}/ssl_scache(512000)\n\tSSLSessionCacheTimeout  300\n\n\t#   Semaphore:\n\t#   Configure the path to the mutual exclusion semaphore the\n\t#   SSL engine uses internally for inter-process synchronization. \n\t#   (Disabled by default, the global Mutex directive consolidates by default\n\t#   this)\n\t#Mutex file:${APACHE_LOCK_DIR}/ssl_mutex ssl-cache\n\n\n\t#   SSL Cipher Suite:\n\t#   List the ciphers that the client is permitted to negotiate. See the\n\t#   ciphers(1) man page from the openssl package for list of all available\n\t#   options.\n\t#   Enable only secure ciphers:\n\tSSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5\n\n\t#   Speed-optimized SSL Cipher configuration:\n\t#   If speed is your main concern (on busy HTTPS servers e.g.),\n\t#   you might want to force clients to specific, performance\n\t#   optimized ciphers. In this case, prepend those ciphers\n\t#   to the SSLCipherSuite list, and enable SSLHonorCipherOrder.\n\t#   Caveat: by giving precedence to RC4-SHA and AES128-SHA\n\t#   (as in the example below), most connections will no longer\n\t#   have perfect forward secrecy - if the server's key is\n\t#   compromised, captures of past or future traffic must be\n\t#   considered compromised, too.\n\t#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5\n\t#SSLHonorCipherOrder on\n\n\t#   The protocols to enable.\n\t#   Available values: all, SSLv3, TLSv1, TLSv1.1, TLSv1.2\n\t#   SSL v2  is no longer supported\n\tSSLProtocol all\n\n\t#   Allow insecure renegotiation with clients which do not yet support the\n\t#   secure renegotiation protocol. Default: Off\n\t#SSLInsecureRenegotiation on\n\n\t#   Whether to forbid non-SNI clients to access name based virtual hosts.\n\t#   Default: Off\n\t#SSLStrictSNIVHostCheck On\n\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/mods-available/ssl.load",
    "content": "# Depends: setenvif mime socache_shmcb\nLoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/ports.conf",
    "content": "# If you just change the port or add more ports here, you will likely also\n# have to change the VirtualHost statement in\n# /etc/apache2/sites-enabled/000-default.conf\n\nListen 80\n\nNameVirtualHost *:80\n\n<IfModule ssl_module>\n\tListen 443\n</IfModule>\n\n<IfModule mod_gnutls.c>\n\tListen 443\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n<IfModule mod_ssl.c>\nNameVirtualHost *:443\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/000-default.conf",
    "content": "<VirtualHost *:80>\n        # How well does Certbot work without a ServerName/Alias?\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/apache2/sites-available/default-ssl.conf",
    "content": "<IfModule mod_ssl.c>\n\t<VirtualHost _default_:443>\n\t\tServerAdmin webmaster@localhost\n\n\t\tDocumentRoot /var/www/html\n\n\t\tErrorLog ${APACHE_LOG_DIR}/error.log\n\t\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t\t#   SSL Engine Switch:\n\t\t#   Enable/Disable SSL for this virtual host.\n\t\tSSLEngine on\n\n\t\t#   A self-signed (snakeoil) certificate can be created by installing\n\t\t#   the ssl-cert package. See\n\t\t#   /usr/share/doc/apache2/README.Debian.gz for more info.\n\t\t#   If both key and certificate are stored in the same file, only the\n\t\t#   SSLCertificateFile directive is needed.\n\t\tSSLCertificateFile\t/etc/apache2/certs/certbot-cert_5.pem\n\t\tSSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem\n\n\t\t<FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</FilesMatch>\n\t\t<Directory /usr/lib/cgi-bin>\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</Directory>\n\n\t\tBrowserMatch \"MSIE [2-6]\" \\\n\t\t\t\tnokeepalive ssl-unclean-shutdown \\\n\t\t\t\tdowngrade-1.0 force-response-1.0\n\t\t# MSIE 7 and newer should be able to use keepalive\n\t\tBrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n\n\t</VirtualHost>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/default_vhost/sites",
    "content": "sites-available/000-default.conf, default.com \n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/apache2.conf",
    "content": "# This is the main Apache server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See http://httpd.apache.org/docs/2.4/ for detailed information about\n# the directives and /usr/share/doc/apache2/README.Debian about Debian specific\n# hints.\n#\n#\n# Summary of how the Apache 2 configuration works in Debian:\n# The Apache 2 web server configuration in Debian is quite different to\n# upstream's suggested way to configure the web server. This is because Debian's\n# default Apache2 installation attempts to make adding and removing modules,\n# virtual hosts, and extra configuration directives as flexible as possible, in\n# order to make automating the changes and administering the server as easy as\n# possible.\n\n# It is split into several files forming the configuration hierarchy outlined\n# below, all located in the /etc/apache2/ directory:\n#\n#\t/etc/apache2/\n#\t|-- apache2.conf\n#\t|\t`--  ports.conf\n#\t|-- mods-enabled\n#\t|\t|-- *.load\n#\t|\t`-- *.conf\n#\t|-- conf-enabled\n#\t|\t`-- *.conf\n# \t`-- sites-enabled\n#\t \t`-- *.conf\n#\n#\n# * apache2.conf is the main configuration file (this file). It puts the pieces\n#   together by including all remaining configuration files when starting up the\n#   web server.\n#\n# * ports.conf is always included from the main configuration file. It is\n#   supposed to determine listening ports for incoming connections which can be\n#   customized anytime.\n#\n# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/\n#   directories contain particular configuration snippets which manage modules,\n#   global configuration fragments, or virtual host configurations,\n#   respectively.\n#\n#   They are activated by symlinking available configuration files from their\n#   respective *-available/ counterparts. These should be managed by using our\n#   helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See\n#   their respective man pages for detailed information.\n#\n# * The binary is called apache2. Due to the use of environment variables, in\n#   the default configuration, apache2 needs to be started/stopped with\n#   /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not\n#   work with the default configuration.\n\n\n# Global configuration\n\n#\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n#\nMutex file:${APACHE_LOCK_DIR} default\n\n#\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n# This needs to be set in /etc/apache2/envvars\n#\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n#\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\n#\nMaxKeepAliveRequests 100\n\n#\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\n#\nKeepAliveTimeout 5\n\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\n#\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\n#\nHostnameLookups Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n#\n# LogLevel: Control the severity of messages logged to the error_log.\n# Available values: trace8, ..., trace1, debug, info, notice, warn,\n# error, crit, alert, emerg.\n# It is also possible to configure the log level for particular modules, e.g.\n# \"LogLevel info ssl:warn\"\n#\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n# Include list of ports to listen on\nInclude ports.conf\n\n\n# Sets the default security model of the Apache2 HTTPD server. It does\n# not allow access to the root filesystem outside of /usr/share and /var/www.\n# The former is used by web applications packaged in Debian,\n# the latter may be used for local directories served by the web server. If\n# your system is serving content from a sub-directory in /srv you must allow\n# access here, or in any related virtual host.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /usr/share>\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n<Directory /var/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\n#\nAccessFileName .htaccess\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n#\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n# The following directives define some format nicknames for use with\n# a CustomLog directive.\n#\n# These deviate from the Common Log Format definitions in that they use %O\n# (the actual bytes sent including headers) instead of %b (the size of the\n# requested file), because the latter makes it impossible to detect partial\n# requests.\n#\n# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.\n# Use mod_remoteip instead.\n#\nLogFormat \"%v:%p %h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" vhost_combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O\" common\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-agent}i\" agent\n\n# Include of directories ignores editors' and dpkg's backup files,\n# see README.Debian for details.\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/*.conf\n\n# Include the virtual host configurations:\nIncludeOptional sites-enabled/*.conf\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/envvars",
    "content": "# envvars - default environment variables for apache2ctl\n\n# this won't be correct after changing uid\nunset HOME\n\n# for supporting multiple apache2 instances\nif [ \"${APACHE_CONFDIR##/etc/apache2-}\" != \"${APACHE_CONFDIR}\" ] ; then\n\tSUFFIX=\"-${APACHE_CONFDIR##/etc/apache2-}\"\nelse\n\tSUFFIX=\nfi\n\n# Since there is no sane way to get the parsed apache2 config in scripts, some\n# settings are defined via environment variables and then used in apache2ctl,\n# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.\nexport APACHE_RUN_USER=www-data\nexport APACHE_RUN_GROUP=www-data\n# temporary state file location. This might be changed to /run in Wheezy+1\nexport APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid\nexport APACHE_RUN_DIR=/var/run/apache2$SUFFIX\nexport APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX\n# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.\nexport APACHE_LOG_DIR=/var/log/apache2$SUFFIX\n\n## The locale used by some modules like mod_dav\nexport LANG=C\n\nexport LANG\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/ports.conf",
    "content": "# If you just change the port or add more ports here, you will likely also\n# have to change the VirtualHost statement in\n# /etc/apache2/sites-enabled/000-default.conf\n\nListen 80\n\n<IfModule ssl_module>\n\tListen 443\n</IfModule>\n\n<IfModule mod_gnutls.c>\n\tListen 443\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/sites-available/default.conf",
    "content": "<VirtualHost *:80>\n\n\tServerName banana.vomit.net\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n<VirtualHost *:80>\n\n\tServerName banana.vomit.com\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multi_vhosts/apache2/sites-available/multi-vhost.conf",
    "content": "<VirtualHost *:80>\n    ServerName 1.multi.vhost.tld\n    ServerAlias first.multi.vhost.tld\n    ServerAdmin webmaster@localhost\n    DocumentRoot /var/www/html\n    ErrorLog ${APACHE_LOG_DIR}/error.log\n    CustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n\n<IfModule mod_ssl.c>\n    <VirtualHost *:80>\n        ServerName 2.multi.vhost.tld\n        ServerAlias second.multi.vhost.tld\n        ServerAdmin webmaster@localhost\n        DocumentRoot /var/www/html\n        ErrorLog ${APACHE_LOG_DIR}/error.log\n        CustomLog ${APACHE_LOG_DIR}/access.log combined\nRewriteEngine on\nRewriteCond %{DOCUMENT_ROOT}/%{REQUEST_FILENAME} !-f\nRewriteRule ^(.*)$ b://u%{REQUEST_URI} [P,NE,L]\nRewriteCond %{HTTPS} !=on\nRewriteCond %{HTTPS} !^$\nRewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,NE,R=permanent]\n    </VirtualHost>\n</IfModule>\n\n<VirtualHost *:80>\n        ServerName 3.multi.vhost.tld\n        ServerAlias third.multi.vhost.tld\n        ServerAdmin webmaster@localhost\n        DocumentRoot /var/www/html\n        ErrorLog ${APACHE_LOG_DIR}/error.log\n        CustomLog ${APACHE_LOG_DIR}/access.log combined\nRewriteEngine on\nRewriteRule \"^/secrets/(.+)\" \"https://new.example.com/docs/$1\" [R,L]\nRewriteRule \"^/docs/(.+)\"  \"http://new.example.com/docs/$1\"  [R,L]\n</VirtualHost>\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/apache2.conf",
    "content": "# This is the main Apache server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See http://httpd.apache.org/docs/2.4/ for detailed information about\n# the directives and /usr/share/doc/apache2/README.Debian about Debian specific\n# hints.\n#\n#\n# Summary of how the Apache 2 configuration works in Debian:\n# The Apache 2 web server configuration in Debian is quite different to\n# upstream's suggested way to configure the web server. This is because Debian's\n# default Apache2 installation attempts to make adding and removing modules,\n# virtual hosts, and extra configuration directives as flexible as possible, in\n# order to make automating the changes and administering the server as easy as\n# possible.\n\n# It is split into several files forming the configuration hierarchy outlined\n# below, all located in the /etc/apache2/ directory:\n#\n#\t/etc/apache2/\n#\t|-- apache2.conf\n#\t|\t`--  ports.conf\n#\t|-- mods-enabled\n#\t|\t|-- *.load\n#\t|\t`-- *.conf\n#\t|-- conf-enabled\n#\t|\t`-- *.conf\n# \t`-- sites-enabled\n#\t \t`-- *.conf\n#\n#\n# * apache2.conf is the main configuration file (this file). It puts the pieces\n#   together by including all remaining configuration files when starting up the\n#   web server.\n#\n# * ports.conf is always included from the main configuration file. It is\n#   supposed to determine listening ports for incoming connections which can be\n#   customized anytime.\n#\n# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/\n#   directories contain particular configuration snippets which manage modules,\n#   global configuration fragments, or virtual host configurations,\n#   respectively.\n#\n#   They are activated by symlinking available configuration files from their\n#   respective *-available/ counterparts. These should be managed by using our\n#   helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See\n#   their respective man pages for detailed information.\n#\n# * The binary is called apache2. Due to the use of environment variables, in\n#   the default configuration, apache2 needs to be started/stopped with\n#   /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not\n#   work with the default configuration.\n\n\n# Global configuration\n\n#\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n#\nMutex file:${APACHE_LOCK_DIR} default\n\n#\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n# This needs to be set in /etc/apache2/envvars\n#\nPidFile ${APACHE_PID_FILE}\n\n#\n# Timeout: The number of seconds before receives and sends time out.\n#\nTimeout 300\n\n#\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\n#\nKeepAlive On\n\n#\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\n#\nMaxKeepAliveRequests 100\n\n#\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\n#\nKeepAliveTimeout 5\n\n\n# These need to be set in /etc/apache2/envvars\nUser ${APACHE_RUN_USER}\nGroup ${APACHE_RUN_GROUP}\n\n#\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\n#\nHostnameLookups Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\n#\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n#\n# LogLevel: Control the severity of messages logged to the error_log.\n# Available values: trace8, ..., trace1, debug, info, notice, warn,\n# error, crit, alert, emerg.\n# It is also possible to configure the log level for particular modules, e.g.\n# \"LogLevel info ssl:warn\"\n#\nLogLevel warn\n\n# Include module configuration:\nIncludeOptional mods-enabled/*.load\nIncludeOptional mods-enabled/*.conf\n\n# Include list of ports to listen on\nInclude ports.conf\n\n\n# Sets the default security model of the Apache2 HTTPD server. It does\n# not allow access to the root filesystem outside of /usr/share and /var/www.\n# The former is used by web applications packaged in Debian,\n# the latter may be used for local directories served by the web server. If\n# your system is serving content from a sub-directory in /srv you must allow\n# access here, or in any related virtual host.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n<Directory /usr/share>\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n<Directory /var/>\n\tOptions Indexes FollowSymLinks\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\n#\nAccessFileName .htaccess\n\n#\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n#\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n# The following directives define some format nicknames for use with\n# a CustomLog directive.\n#\n# These deviate from the Common Log Format definitions in that they use %O\n# (the actual bytes sent including headers) instead of %b (the size of the\n# requested file), because the latter makes it impossible to detect partial\n# requests.\n#\n# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.\n# Use mod_remoteip instead.\n#\nLogFormat \"%v:%p %h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" vhost_combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %O\" common\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-agent}i\" agent\n\n# Include of directories ignores editors' and dpkg's backup files,\n# see README.Debian for details.\n\n# Include generic snippets of statements\nIncludeOptional conf-enabled/*.conf\n\n# Include the virtual host configurations:\nIncludeOptional sites-enabled/*.conf\n\n<VirtualHost *:80>\n\n\tServerName vhost.in.rootconf\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/conf-available/bad_conf_file.conf",
    "content": "<VirtualHost 1.1.1.1>\n\nServerName invalid.net\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/conf-available/other-vhosts-access-log.conf",
    "content": "# Define an access log for VirtualHosts that don't define their own logfile\nCustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log vhost_combined\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/conf-available/security.conf",
    "content": "# Changing the following options will not really affect the security of the\n# server, but might make attacks slightly more difficult in some cases.\n\n#\n# ServerTokens\n# This directive configures what you return as the Server HTTP response\n# Header. The default is 'Full' which sends information about the OS-Type\n# and compiled in modules.\n# Set to one of:  Full | OS | Minimal | Minor | Major | Prod\n# where Full conveys the most information, and Prod the least.\n#ServerTokens Minimal\nServerTokens OS\n#ServerTokens Full\n\n#\n# Optionally add a line containing the server version and virtual host\n# name to server-generated pages (internal error documents, FTP directory\n# listings, mod_status and mod_info output etc., but not CGI generated\n# documents or custom error documents).\n# Set to \"EMail\" to also include a mailto: link to the ServerAdmin.\n# Set to one of:  On | Off | EMail\n#ServerSignature Off\nServerSignature On\n\n#\n# Allow TRACE method\n#\n# Set to \"extended\" to also reflect the request body (only for testing and\n# diagnostic purposes).\n#\n# Set to one of:  On | Off | extended\nTraceEnable Off\n#TraceEnable On\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/conf-available/serve-cgi-bin.conf",
    "content": "<IfModule mod_alias.c>\n\t<IfModule mod_cgi.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfModule mod_cgid.c>\n\t\tDefine ENABLE_USR_LIB_CGI_BIN\n\t</IfModule>\n\n\t<IfDefine ENABLE_USR_LIB_CGI_BIN>\n\t\tScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n\t\t<Directory \"/usr/lib/cgi-bin\">\n\t\t\tAllowOverride None\n\t\t\tOptions +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n\t\t\tRequire all granted\n\t\t</Directory>\n\t</IfDefine>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/envvars",
    "content": "# envvars - default environment variables for apache2ctl\n\n# this won't be correct after changing uid\nunset HOME\n\n# for supporting multiple apache2 instances\nif [ \"${APACHE_CONFDIR##/etc/apache2-}\" != \"${APACHE_CONFDIR}\" ] ; then\n\tSUFFIX=\"-${APACHE_CONFDIR##/etc/apache2-}\"\nelse\n\tSUFFIX=\nfi\n\n# Since there is no sane way to get the parsed apache2 config in scripts, some\n# settings are defined via environment variables and then used in apache2ctl,\n# /etc/init.d/apache2, /etc/logrotate.d/apache2, etc.\nexport APACHE_RUN_USER=www-data\nexport APACHE_RUN_GROUP=www-data\n# temporary state file location. This might be changed to /run in Wheezy+1\nexport APACHE_PID_FILE=/var/run/apache2/apache2$SUFFIX.pid\nexport APACHE_RUN_DIR=/var/run/apache2$SUFFIX\nexport APACHE_LOCK_DIR=/var/lock/apache2$SUFFIX\n# Only /var/log/apache2 is handled by /etc/logrotate.d/apache2.\nexport APACHE_LOG_DIR=/var/log/apache2$SUFFIX\n\n## The locale used by some modules like mod_dav\nexport LANG=C\n\nexport LANG\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/authz_svn.load",
    "content": "# Depends: dav_svn\n<IfModule !mod_dav_svn.c>\n    Include mods-enabled/dav_svn.load\n</IfModule>\nLoadModule authz_svn_module /usr/lib/apache2/modules/mod_authz_svn.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/dav.load",
    "content": "<IfModule !mod_dav.c>\n\tLoadModule dav_module /usr/lib/apache2/modules/mod_dav.so\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/dav_svn.conf",
    "content": "# dav_svn.conf - Example Subversion/Apache configuration\n#\n# For details and further options see the Apache user manual and\n# the Subversion book.\n#\n# NOTE: for a setup with multiple vhosts, you will want to do this\n# configuration in /etc/apache2/sites-available/*, not here.\n\n# <Location URL> ... </Location>\n# URL controls how the repository appears to the outside world.\n# In this example clients access the repository as http://hostname/svn/\n# Note, a literal /svn should NOT exist in your document root.\n#<Location /svn>\n\n  # Uncomment this to enable the repository\n  #DAV svn\n\n  # Set this to the path to your repository\n  #SVNPath /var/lib/svn\n  # Alternatively, use SVNParentPath if you have multiple repositories under\n  # under a single directory (/var/lib/svn/repo1, /var/lib/svn/repo2, ...).\n  # You need either SVNPath and SVNParentPath, but not both.\n  #SVNParentPath /var/lib/svn\n\n  # Access control is done at 3 levels: (1) Apache authentication, via\n  # any of several methods.  A \"Basic Auth\" section is commented out\n  # below.  (2) Apache <Limit> and <LimitExcept>, also commented out\n  # below.  (3) mod_authz_svn is a svn-specific authorization module\n  # which offers fine-grained read/write access control for paths\n  # within a repository.  (The first two layers are coarse-grained; you\n  # can only enable/disable access to an entire repository.)  Note that\n  # mod_authz_svn is noticeably slower than the other two layers, so if\n  # you don't need the fine-grained control, don't configure it.\n\n  # Basic Authentication is repository-wide.  It is not secure unless\n  # you are using https.  See the 'htpasswd' command to create and\n  # manage the password file - and the documentation for the\n  # 'auth_basic' and 'authn_file' modules, which you will need for this\n  # (enable them with 'a2enmod').\n  #AuthType Basic\n  #AuthName \"Subversion Repository\"\n  #AuthUserFile /etc/apache2/dav_svn.passwd\n\n  # To enable authorization via mod_authz_svn (enable that module separately):\n  #<IfModule mod_authz_svn.c>\n  #AuthzSVNAccessFile /etc/apache2/dav_svn.authz\n  #</IfModule>\n\n  # The following three lines allow anonymous read, but make\n  # committers authenticate themselves.  It requires the 'authz_user'\n  # module (enable it with 'a2enmod').\n  #<LimitExcept GET PROPFIND OPTIONS REPORT>\n    #Require valid-user\n  #</LimitExcept> \n\n#</Location>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/dav_svn.load",
    "content": "# Depends: dav\n<IfModule !mod_dav_svn.c>\n    <IfModule !mod_dav.c>\n        Include mods-enabled/dav.load\n    </IfModule>\n    LoadModule dav_svn_module /usr/lib/apache2/modules/mod_dav_svn.so\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/rewrite.load",
    "content": "LoadModule rewrite_module /usr/lib/apache2/modules/mod_rewrite.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/ssl.conf",
    "content": "<IfModule mod_ssl.c>\n\n\t# Pseudo Random Number Generator (PRNG):\n\t# Configure one or more sources to seed the PRNG of the SSL library.\n\t# The seed data should be of good random quality.\n\t# WARNING! On some platforms /dev/random blocks if not enough entropy\n\t# is available. This means you then cannot use the /dev/random device\n\t# because it would lead to very long connection times (as long as\n\t# it requires to make more entropy available). But usually those\n\t# platforms additionally provide a /dev/urandom device which doesn't\n\t# block. So, if available, use this one instead. Read the mod_ssl User\n\t# Manual for more details.\n\t#\n\tSSLRandomSeed startup builtin\n\tSSLRandomSeed startup file:/dev/urandom 512\n\tSSLRandomSeed connect builtin\n\tSSLRandomSeed connect file:/dev/urandom 512\n\n\t##\n\t##  SSL Global Context\n\t##\n\t##  All SSL configuration in this context applies both to\n\t##  the main server and all SSL-enabled virtual hosts.\n\t##\n\n\t#\n\t#   Some MIME-types for downloading Certificates and CRLs\n\t#\n\tAddType application/x-x509-ca-cert .crt\n\tAddType application/x-pkcs7-crl\t.crl\n\n\t#   Pass Phrase Dialog:\n\t#   Configure the pass phrase gathering process.\n\t#   The filtering dialog program (`builtin' is an internal\n\t#   terminal dialog) has to provide the pass phrase on stdout.\n\tSSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase\n\n\t#   Inter-Process Session Cache:\n\t#   Configure the SSL Session Cache: First the mechanism \n\t#   to use and second the expiring timeout (in seconds).\n\t#   (The mechanism dbm has known memory leaks and should not be used).\n\t#SSLSessionCache\t\t dbm:${APACHE_RUN_DIR}/ssl_scache\n\tSSLSessionCache\t\tshmcb:${APACHE_RUN_DIR}/ssl_scache(512000)\n\tSSLSessionCacheTimeout  300\n\n\t#   Semaphore:\n\t#   Configure the path to the mutual exclusion semaphore the\n\t#   SSL engine uses internally for inter-process synchronization. \n\t#   (Disabled by default, the global Mutex directive consolidates by default\n\t#   this)\n\t#Mutex file:${APACHE_LOCK_DIR}/ssl_mutex ssl-cache\n\n\n\t#   SSL Cipher Suite:\n\t#   List the ciphers that the client is permitted to negotiate. See the\n\t#   ciphers(1) man page from the openssl package for list of all available\n\t#   options.\n\t#   Enable only secure ciphers:\n\tSSLCipherSuite HIGH:MEDIUM:!aNULL:!MD5\n\n\t#   Speed-optimized SSL Cipher configuration:\n\t#   If speed is your main concern (on busy HTTPS servers e.g.),\n\t#   you might want to force clients to specific, performance\n\t#   optimized ciphers. In this case, prepend those ciphers\n\t#   to the SSLCipherSuite list, and enable SSLHonorCipherOrder.\n\t#   Caveat: by giving precedence to RC4-SHA and AES128-SHA\n\t#   (as in the example below), most connections will no longer\n\t#   have perfect forward secrecy - if the server's key is\n\t#   compromised, captures of past or future traffic must be\n\t#   considered compromised, too.\n\t#SSLCipherSuite RC4-SHA:AES128-SHA:HIGH:MEDIUM:!aNULL:!MD5\n\t#SSLHonorCipherOrder on\n\n\t#   The protocols to enable.\n\t#   Available values: all, SSLv3, TLSv1, TLSv1.1, TLSv1.2\n\t#   SSL v2  is no longer supported\n\tSSLProtocol all\n\n\t#   Allow insecure renegotiation with clients which do not yet support the\n\t#   secure renegotiation protocol. Default: Off\n\t#SSLInsecureRenegotiation on\n\n\t#   Whether to forbid non-SNI clients to access name based virtual hosts.\n\t#   Default: Off\n\t#SSLStrictSNIVHostCheck On\n\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/mods-available/ssl.load",
    "content": "# Depends: setenvif mime socache_shmcb\nLoadModule ssl_module /usr/lib/apache2/modules/mod_ssl.so\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/ports.conf",
    "content": "# If you just change the port or add more ports here, you will likely also\n# have to change the VirtualHost statement in\n# /etc/apache2/sites-enabled/000-default.conf\n\nListen 80\n\n<IfModule ssl_module>\n\tListen 443\n</IfModule>\n\n<IfModule mod_gnutls.c>\n\tListen 443\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/000-default.conf",
    "content": "<VirtualHost *:80 [::]:80>\n\n\tServerName ip-172-30-0-17\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/certbot.conf",
    "content": "<VirtualHost *:80>\nServerName certbot.demo\nServerAlias www.certbot.demo\nServerAdmin webmaster@localhost\n\nDocumentRoot /var/www-certbot-reworld/static/\n<Directory />\nOptions FollowSymLinks\nAllowOverride None\n</Directory>\n<Directory /var/www/>\nOptions Indexes FollowSymLinks MultiViews\nAllowOverride None\nOrder allow,deny\nallow from all\n</Directory>\n\nScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n<Directory \"/usr/lib/cgi-bin\">\nAllowOverride None\nOptions +ExecCGI -MultiViews +SymLinksIfOwnerMatch\nOrder allow,deny\nAllow from all\n</Directory>\n\nErrorLog ${APACHE_LOG_DIR}/error.log\n\n# Possible values include: debug, info, notice, warn, error, crit,\n# alert, emerg.\nLogLevel warn\n\nCustomLog ${APACHE_LOG_DIR}/access.log combined\n\nAlias /doc/ \"/usr/share/doc/\"\n<Directory \"/usr/share/doc/\">\nOptions Indexes MultiViews FollowSymLinks\nAllowOverride None\nOrder deny,allow\nDeny from all\nAllow from 127.0.0.0/255.0.0.0 ::1/128\n</Directory>\n\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/default-ssl-port-only.conf",
    "content": "<IfModule mod_ssl.c>\n\t<VirtualHost _default_:443>\n\t\tServerAdmin webmaster@localhost\n\n\t\tDocumentRoot /var/www/html\n\n\t\tErrorLog ${APACHE_LOG_DIR}/error.log\n\t\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t\t#   A self-signed (snakeoil) certificate can be created by installing\n\t\t#   the ssl-cert package. See\n\t\t#   /usr/share/doc/apache2/README.Debian.gz for more info.\n\t\t#   If both key and certificate are stored in the same file, only the\n\t\t#   SSLCertificateFile directive is needed.\n\t\tSSLCertificateFile\t/etc/apache2/certs/certbot-cert_5.pem\n\t\tSSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem\n\n\n\t\t#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire\n\t\t<FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</FilesMatch>\n\t\t<Directory /usr/lib/cgi-bin>\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</Directory>\n\n\t\tBrowserMatch \"MSIE [2-6]\" \\\n\t\t\t\tnokeepalive ssl-unclean-shutdown \\\n\t\t\t\tdowngrade-1.0 force-response-1.0\n\t\t# MSIE 7 and newer should be able to use keepalive\n\t\tBrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n\n\t</VirtualHost>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/default-ssl.conf",
    "content": "<IfModule mod_ssl.c>\n\t<VirtualHost _default_:443>\n\t\tServerAdmin webmaster@localhost\n\n\t\tDocumentRoot /var/www/html\n\n\t\tErrorLog ${APACHE_LOG_DIR}/error.log\n\t\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t\t#   SSL Engine Switch:\n\t\t#   Enable/Disable SSL for this virtual host.\n\t\tSSLEngine on\n\n\t\t#   A self-signed (snakeoil) certificate can be created by installing\n\t\t#   the ssl-cert package. See\n\t\t#   /usr/share/doc/apache2/README.Debian.gz for more info.\n\t\t#   If both key and certificate are stored in the same file, only the\n\t\t#   SSLCertificateFile directive is needed.\n\t\tSSLCertificateFile\t/etc/apache2/certs/certbot-cert_5.pem\n\t\tSSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem\n\n\n\t\t#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire\n\t\t<FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</FilesMatch>\n\t\t<Directory /usr/lib/cgi-bin>\n\t\t\t\tSSLOptions +StdEnvVars\n\t\t</Directory>\n\n\t\tBrowserMatch \"MSIE [2-6]\" \\\n\t\t\t\tnokeepalive ssl-unclean-shutdown \\\n\t\t\t\tdowngrade-1.0 force-response-1.0\n\t\t# MSIE 7 and newer should be able to use keepalive\n\t\tBrowserMatch \"MSIE [17-9]\" ssl-unclean-shutdown\n\n\t</VirtualHost>\n</IfModule>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/duplicatehttp.conf",
    "content": "<VirtualHost 10.2.3.4:80>\n\tServerName duplicate.example.com\n\n\tServerAdmin webmaster@certbot.demo\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/duplicatehttps.conf",
    "content": "<IfModule mod_ssl.c>\n<VirtualHost 10.2.3.4:443>\n\tServerName duplicate.example.com\n\n\tServerAdmin webmaster@certbot.demo\n\tDocumentRoot /var/www/html\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\nSSLCertificateFile\t/etc/apache2/certs/certbot-cert_5.pem\nSSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem\n</VirtualHost>\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/empty.conf",
    "content": ""
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/encryption-example.conf",
    "content": "<Virtualhost *:80>\n             ServerName encryption-example.demo\n\t     ServerAdmin webmaster@localhost\n\n\t      DocumentRoot /var/www-encryption-example/static/\n\t      <Directory />\n\t      Options FollowSymLinks\n\t      AllowOverride None\n\t      </Directory>\n\t      <Directory /var/www/>\n\t      Options Indexes FollowSymLinks MultiViews\n\t      AllowOverride None\n\t      Order allow,deny\n\t      allow from all\n\t      </Directory>\n\n\t      ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/\n\t      <Directory \"/usr/lib/cgi-bin\">\n\t      AllowOverride None\n\t      Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch\n\t      Order allow,deny\n\t      Allow from all\n\t      </Directory>\n\n\t      ErrorLog ${APACHE_LOG_DIR}/error.log\n\n\t      # Possible values include: debug, info, notice, warn, error, crit,\n\t      # alert, emerg.\n\t      LogLevel warn\n\n\t      CustomLog ${APACHE_LOG_DIR}/access.log combined\n\n    Alias /doc/ \"/usr/share/doc/\"\n    <Directory \"/usr/share/doc/\">\n        Options Indexes MultiViews FollowSymLinks\n        AllowOverride None\n        Order deny,allow\n        Deny from all\n\t      Allow from 127.0.0.0/255.0.0.0 ::1/128\n    </Directory>\n\n</Virtualhost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/mod_macro-example.conf",
    "content": "<Macro VHost $name $domain>\n<VirtualHost *:80>\n    ServerName $domain\n    ServerAlias www.$domain\n    DocumentRoot /var/www/html\n\n    ErrorLog ${APACHE_LOG_DIR}/error.log\n    CustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n</Macro>\nUse VHost macro1 test.com\nUse VHost macro2 hostname.org\nUse VHost macro3 apache.org\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/no-directives.conf",
    "content": "<VirtualHost *:80>\n  <Location />\n        Require all denied\n  </Location>\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/ocsp-ssl.conf",
    "content": "<IfModule mod_ssl.c>\nSSLStaplingCache shmcb:/var/run/apache2/stapling_cache(128000)\n<VirtualHost 10.2.3.4:443>\n\t# The ServerName directive sets the request scheme, hostname and port that\n\t# the server uses to identify itself. This is used when creating\n\t# redirection URLs. In the context of virtual hosts, the ServerName\n\t# specifies what hostname must appear in the request's Host: header to\n\t# match this virtual host. For the default virtual host (this file) this\n\t# value is not decisive as it is used as a last resort host regardless.\n\t# However, you must set it for any further virtual host explicitly.\n\tServerName ocspvhost.com\n\n\tServerAdmin webmaster@dumpbits.com\n\tDocumentRoot /var/www/html\n\n\t# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,\n\t# error, crit, alert, emerg.\n\t# It is also possible to configure the loglevel for particular\n\t# modules, e.g.\n\t#LogLevel info ssl:warn\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n\t# For most configuration files from conf-available/, which are\n\t# enabled or disabled at a global level, it is possible to\n\t# include a line for only one particular virtual host. For example the\n\t# following line enables the CGI configuration for this host only\n\t# after it has been globally disabled with \"a2disconf\".\n\t#Include conf-available/serve-cgi-bin.conf\nSSLCertificateFile\t/etc/apache2/certs/certbot-cert_5.pem\nSSLCertificateKeyFile /etc/apache2/ssl/key-certbot_15.pem\nSSLUseStapling on\n</VirtualHost>\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n</IfModule>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-available/wildcard.conf",
    "content": "<VirtualHost *:80>\n\n\tServerName ip-172-30-0-17\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\tServerAlias *.blue.purple.com\n\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n\n</VirtualHost>\n\n# vim: syntax=apache ts=4 sw=4 sts=4 sr noet\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/apache2/sites-enabled/non-symlink.conf",
    "content": "<VirtualHost *:80>\nServerName nonsym.link\nServerAdmin webmaster@localhost\n\nDocumentRoot /var/www-certbot-reworld/static/\n\nErrorLog ${APACHE_LOG_DIR}/error.log\nCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/debian_apache_2_4/multiple_vhosts/sites",
    "content": "sites-available/certbot.conf, certbot.demo\nsites-available/encryption-example.conf, encryption-example.demo\nsites-available/ocsp-ssl.conf, ocspvhost.com\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/httpd.conf",
    "content": "# This is a modification of the default Apache 2.4 configuration file\n# for Gentoo Linux.\n#\n# Support:\n#   http://www.gentoo.org/main/en/lists.xml   [mailing lists]\n#   http://forums.gentoo.org/                 [web forums]\n#   irc://irc.freenode.net#gentoo-apache      [irc chat]\n#\n# Bug Reports:\n#   http://bugs.gentoo.org                    [gentoo related bugs]\n#   http://httpd.apache.org/bug_report.html   [apache httpd related bugs]\n#\n#\n# This is the main Apache HTTP server configuration file.  It contains the\n# configuration directives that give the server its instructions.\n# See <URL:http://httpd.apache.org/docs/2.4> for detailed information.\n# In particular, see\n# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html>\n# for a discussion of each configuration directive.\n#\n# Do NOT simply read the instructions in here without understanding\n# what they do.  They're here only as hints or reminders.  If you are unsure\n# consult the online docs. You have been warned.\n#\n# Configuration and logfile names: If the filenames you specify for many\n# of the server's control files begin with \"/\" (or \"drive:/\" for Win32), the\n# server will use that explicit path.  If the filenames do *not* begin\n# with \"/\", the value of ServerRoot is prepended -- so \"var/log/apache2/foo_log\"\n# with ServerRoot set to \"/usr\" will be interpreted by the\n# server as \"/usr/var/log/apache2/foo.log\".\n\n# ServerRoot: The top of the directory tree under which the server's\n# configuration, error, and log files are kept.\n#\n# Do not add a slash at the end of the directory path.  If you point\n# ServerRoot at a non-local disk, be sure to point the LockFile directive\n# at a local disk.  If you wish to share the same ServerRoot for multiple\n# httpd daemons, you will need to change at least LockFile and PidFile.\n# Comment: The LockFile directive has been replaced by the Mutex directive\nServerRoot \"/usr/lib64/apache2\"\n\n# Dynamic Shared Object (DSO) Support\n#\n# To be able to use the functionality of a module which was built as a DSO you\n# have to place corresponding `LoadModule' lines at this location so the\n# directives contained in it are actually available _before_ they are used.\n# Statically compiled modules (those listed by `httpd -l') do not need\n# to be loaded here.\n#\n# Example:\n# LoadModule foo_module modules/mod_foo.so\n#\n# GENTOO: Automatically defined based on APACHE2_MODULES USE_EXPAND variable.\n#         Do not change manually, it will be overwritten on upgrade.\n#\n# The following modules are considered as the default configuration.\n# If you wish to disable one of them, you may have to alter other\n# configuration directives.\n#\n# Change these at your own risk!\n\nLoadModule actions_module modules/mod_actions.so\nLoadModule alias_module modules/mod_alias.so\nLoadModule auth_basic_module modules/mod_auth_basic.so\nLoadModule authn_anon_module modules/mod_authn_anon.so\nLoadModule authn_core_module modules/mod_authn_core.so\nLoadModule authn_dbm_module modules/mod_authn_dbm.so\nLoadModule authn_file_module modules/mod_authn_file.so\nLoadModule authz_core_module modules/mod_authz_core.so\nLoadModule authz_dbm_module modules/mod_authz_dbm.so\nLoadModule authz_groupfile_module modules/mod_authz_groupfile.so\nLoadModule authz_host_module modules/mod_authz_host.so\nLoadModule authz_owner_module modules/mod_authz_owner.so\nLoadModule authz_user_module modules/mod_authz_user.so\nLoadModule autoindex_module modules/mod_autoindex.so\n<IfDefine CACHE>\nLoadModule cache_module modules/mod_cache.so\n</IfDefine>\nLoadModule cgi_module modules/mod_cgi.so\nLoadModule cgid_module modules/mod_cgid.so\n<IfDefine DAV>\nLoadModule dav_module modules/mod_dav.so\n</IfDefine>\n<IfDefine DAV>\nLoadModule dav_fs_module modules/mod_dav_fs.so\n</IfDefine>\n<IfDefine DAV>\nLoadModule dav_lock_module modules/mod_dav_lock.so\n</IfDefine>\nLoadModule deflate_module modules/mod_deflate.so\nLoadModule dir_module modules/mod_dir.so\nLoadModule env_module modules/mod_env.so\nLoadModule expires_module modules/mod_expires.so\nLoadModule ext_filter_module modules/mod_ext_filter.so\n<IfDefine CACHE>\nLoadModule file_cache_module modules/mod_file_cache.so\n</IfDefine>\nLoadModule filter_module modules/mod_filter.so\nLoadModule headers_module modules/mod_headers.so\nLoadModule include_module modules/mod_include.so\n<IfDefine INFO>\nLoadModule info_module modules/mod_info.so\n</IfDefine>\nLoadModule log_config_module modules/mod_log_config.so\nLoadModule logio_module modules/mod_logio.so\nLoadModule mime_module modules/mod_mime.so\nLoadModule mime_magic_module modules/mod_mime_magic.so\nLoadModule negotiation_module modules/mod_negotiation.so\nLoadModule rewrite_module modules/mod_rewrite.so\nLoadModule setenvif_module modules/mod_setenvif.so\n<IfDefine SSL>\nLoadModule socache_shmcb_module modules/mod_socache_shmcb.so\n</IfDefine>\nLoadModule speling_module modules/mod_speling.so\n<IfDefine SSL>\nLoadModule ssl_module modules/mod_ssl.so\n</IfDefine>\n<IfDefine STATUS>\nLoadModule status_module modules/mod_status.so\n</IfDefine>\nLoadModule unique_id_module modules/mod_unique_id.so\nLoadModule unixd_module modules/mod_unixd.so\n<IfDefine USERDIR>\nLoadModule userdir_module modules/mod_userdir.so\n</IfDefine>\nLoadModule usertrack_module modules/mod_usertrack.so\nLoadModule vhost_alias_module modules/mod_vhost_alias.so\n\n# If you wish httpd to run as a different user or group, you must run\n# httpd as root initially and it will switch.\n#\n# User/Group: The name (or #number) of the user/group to run httpd as.\n# It is usually good practice to create a dedicated user and group for\n# running httpd, as with most system services.\nUser apache\nGroup apache\n\n# Supplemental configuration\n#\n# Most of the configuration files in the /etc/apache2/modules.d/ directory can\n# be turned on using APACHE2_OPTS in /etc/conf.d/apache2 to add extra features\n# or to modify the default configuration of the server.\n#\n# To know which flag to add to APACHE2_OPTS, look at the first line of the\n# the file, which will usually be an <IfDefine OPTION> where OPTION is the\n# flag to use.\n\nInclude modules.d/*.conf\n\n# Virtual-host support\n#\n# Gentoo has made using virtual-hosts easy. In /etc/apache2/vhosts.d/ we\n# include a default vhost (enabled by adding -D DEFAULT_VHOST to\n# APACHE2_OPTS in /etc/conf.d/apache2).\nInclude vhosts.d/*.conf\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/magic",
    "content": "# Magic data for mod_mime_magic Apache module (originally for file(1) command)\n# The module is described in /manual/mod/mod_mime_magic.html\n#\n# The format is 4-5 columns:\n#    Column #1: byte number to begin checking from, \">\" indicates continuation\n#    Column #2: type of data to match\n#    Column #3: contents of data to match\n#    Column #4: MIME type of result\n#    Column #5: MIME encoding of result (optional)\n\n#------------------------------------------------------------------------------\n# Localstuff:  file(1) magic for locally observed files\n# Add any locally observed files here.\n\n#------------------------------------------------------------------------------\n# end local stuff\n#------------------------------------------------------------------------------\n\n#------------------------------------------------------------------------------\n# Java\n\n0\tshort\t\t0xcafe\n>2\tshort\t\t0xbabe\t\tapplication/java\n\n#------------------------------------------------------------------------------\n# audio:  file(1) magic for sound formats\n#\n# from Jan Nicolai Langfeldt <janl@ifi.uio.no>,\n#\n\n# Sun/NeXT audio data\n0\tstring\t\t.snd\n>12\tbelong\t\t1\t\taudio/basic\n>12\tbelong\t\t2\t\taudio/basic\n>12\tbelong\t\t3\t\taudio/basic\n>12\tbelong\t\t4\t\taudio/basic\n>12\tbelong\t\t5\t\taudio/basic\n>12\tbelong\t\t6\t\taudio/basic\n>12\tbelong\t\t7\t\taudio/basic\n\n>12\tbelong\t\t23\t\taudio/x-adpcm\n\n# DEC systems (e.g. DECstation 5000) use a variant of the Sun/NeXT format\n# that uses little-endian encoding and has a different magic number\n# (0x0064732E in little-endian encoding).\n0\tlelong\t\t0x0064732E\t\n>12\tlelong\t\t1\t\taudio/x-dec-basic\n>12\tlelong\t\t2\t\taudio/x-dec-basic\n>12\tlelong\t\t3\t\taudio/x-dec-basic\n>12\tlelong\t\t4\t\taudio/x-dec-basic\n>12\tlelong\t\t5\t\taudio/x-dec-basic\n>12\tlelong\t\t6\t\taudio/x-dec-basic\n>12\tlelong\t\t7\t\taudio/x-dec-basic\n#                                       compressed (G.721 ADPCM)\n>12\tlelong\t\t23\t\taudio/x-dec-adpcm\n\n# Bytes 0-3 of AIFF, AIFF-C, & 8SVX audio files are \"FORM\"\n#\t\t\t\t\tAIFF audio data\n8\tstring\t\tAIFF\t\taudio/x-aiff\t\n#\t\t\t\t\tAIFF-C audio data\n8\tstring\t\tAIFC\t\taudio/x-aiff\t\n#\t\t\t\t\tIFF/8SVX audio data\n8\tstring\t\t8SVX\t\taudio/x-aiff\t\n\n# Creative Labs AUDIO stuff\n#\t\t\t\t\tStandard MIDI data\n0\tstring\tMThd\t\t\taudio/unknown\t\n#>9 \tbyte\t>0\t\t\t(format %d)\n#>11\tbyte\t>1\t\t\tusing %d channels\n#\t\t\t\t\tCreative Music (CMF) data\n0\tstring\tCTMF\t\t\taudio/unknown\t\n#\t\t\t\t\tSoundBlaster instrument data\n0\tstring\tSBI\t\t\taudio/unknown\t\n#\t\t\t\t\tCreative Labs voice data\n0\tstring\tCreative\\ Voice\\ File\taudio/unknown\t\n## is this next line right?  it came this way...\n#>19\tbyte\t0x1A\n#>23\tbyte\t>0\t\t\t- version %d\n#>22\tbyte\t>0\t\t\t\\b.%d\n\n# [GRR 950115:  is this also Creative Labs?  Guessing that first line\n#  should be string instead of unknown-endian long...]\n#0\tlong\t\t0x4e54524b\tMultiTrack sound data\n#0\tstring\t\tNTRK\t\tMultiTrack sound data\n#>4\tlong\t\tx\t\t- version %ld\n\n# Microsoft WAVE format (*.wav)\n# [GRR 950115:  probably all of the shorts and longs should be leshort/lelong]\n#\t\t\t\t\tMicrosoft RIFF\n0\tstring\t\tRIFF\t\taudio/unknown\n#\t\t\t\t\t- WAVE format\n>8\tstring\t\tWAVE\t\taudio/x-wav\n# MPEG audio.\n0   beshort&0xfff0  0xfff0  audio/mpeg\n# C64 SID Music files, from Linus Walleij <triad@df.lth.se>\n0   string      PSID        audio/prs.sid\n\n#------------------------------------------------------------------------------\n# c-lang:  file(1) magic for C programs or various scripts\n#\n\n# XPM icons (Greg Roelofs, newt@uchicago.edu)\n# ideally should go into \"images\", but entries below would tag XPM as C source\n0\tstring\t\t/*\\ XPM\t\timage/x-xbm\t7bit\n\n# this first will upset you if you're a PL/1 shop... (are there any left?)\n# in which case rm it; ascmagic will catch real C programs\n#\t\t\t\t\tC or REXX program text\n0\tstring\t\t/*\t\ttext/plain\n#\t\t\t\t\tC++ program text\n0\tstring\t\t//\t\ttext/plain\n\n#------------------------------------------------------------------------------\n# compress:  file(1) magic for pure-compression formats (no archives)\n#\n# compress, gzip, pack, compact, huf, squeeze, crunch, freeze, yabba, whap, etc.\n#\n# Formats for various forms of compressed data\n# Formats for \"compress\" proper have been moved into \"compress.c\",\n# because it tries to uncompress it to figure out what's inside.\n\n# standard unix compress\n0\tstring\t\t\\037\\235\tapplication/octet-stream\tx-compress\n\n# gzip (GNU zip, not to be confused with [Info-ZIP/PKWARE] zip archiver)\n0       string          \\037\\213        application/octet-stream\tx-gzip\n\n# According to gzip.h, this is the correct byte order for packed data.\n0\tstring\t\t\\037\\036\tapplication/octet-stream\n#\n# This magic number is byte-order-independent.\n#\n0\tshort\t\t017437\t\tapplication/octet-stream\n\n# XXX - why *two* entries for \"compacted data\", one of which is\n# byte-order independent, and one of which is byte-order dependent?\n#\n# compacted data\n0\tshort\t\t0x1fff\t\tapplication/octet-stream\n0\tstring\t\t\\377\\037\tapplication/octet-stream\n# huf output\n0\tshort\t\t0145405\t\tapplication/octet-stream\n\n# Squeeze and Crunch...\n# These numbers were gleaned from the Unix versions of the programs to\n# handle these formats.  Note that I can only uncrunch, not crunch, and\n# I didn't have a crunched file handy, so the crunch number is untested.\n#\t\t\t\tKeith Waclena <keith@cerberus.uchicago.edu>\n#0\tleshort\t\t0x76FF\t\tsqueezed data (CP/M, DOS)\n#0\tleshort\t\t0x76FE\t\tcrunched data (CP/M, DOS)\n\n# Freeze\n#0\tstring\t\t\\037\\237\tFrozen file 2.1\n#0\tstring\t\t\\037\\236\tFrozen file 1.0 (or gzip 0.5)\n\n# lzh?\n#0\tstring\t\t\\037\\240\tLZH compressed data\n\n#------------------------------------------------------------------------------\n# frame:  file(1) magic for FrameMaker files\n#\n# This stuff came on a FrameMaker demo tape, most of which is\n# copyright, but this file is \"published\" as witness the following:\n#\n0\tstring\t\t\\<MakerFile\tapplication/x-frame\n0\tstring\t\t\\<MIFFile\tapplication/x-frame\n0\tstring\t\t\\<MakerDictionary\tapplication/x-frame\n0\tstring\t\t\\<MakerScreenFon\tapplication/x-frame\n0\tstring\t\t\\<MML\t\tapplication/x-frame\n0\tstring\t\t\\<Book\t\tapplication/x-frame\n0\tstring\t\t\\<Maker\t\tapplication/x-frame\n\n#------------------------------------------------------------------------------\n# html:  file(1) magic for HTML (HyperText Markup Language) docs\n#\n# from Daniel Quinlan <quinlan@yggdrasil.com>\n# and Anna Shergold <anna@inext.co.uk>\n#\n0   string      \\<!DOCTYPE\\ HTML    text/html\n0   string      \\<!doctype\\ html    text/html\n0   string      \\<HEAD      text/html\n0   string      \\<head      text/html\n0   string      \\<TITLE     text/html\n0   string      \\<title     text/html\n0   string      \\<html      text/html\n0   string      \\<HTML      text/html\n0   string      \\<!--       text/html\n0   string      \\<h1        text/html\n0   string      \\<H1        text/html\n\n# XML eXtensible Markup Language, from Linus Walleij <triad@df.lth.se>\n0   string      \\<?xml      text/xml\n\n#------------------------------------------------------------------------------\n# images:  file(1) magic for image formats (see also \"c-lang\" for XPM bitmaps)\n#\n# originally from jef@helios.ee.lbl.gov (Jef Poskanzer),\n# additions by janl@ifi.uio.no as well as others. Jan also suggested\n# merging several one- and two-line files into here.\n#\n# XXX - byte order for GIF and TIFF fields?\n# [GRR:  TIFF allows both byte orders; GIF is probably little-endian]\n#\n\n# [GRR:  what the hell is this doing in here?]\n#0\tstring\t\txbtoa\t\tbtoa'd file\n\n# PBMPLUS\n#\t\t\t\t\tPBM file\n0\tstring\t\tP1\t\timage/x-portable-bitmap\t7bit\n#\t\t\t\t\tPGM file\n0\tstring\t\tP2\t\timage/x-portable-greymap\t7bit\n#\t\t\t\t\tPPM file\n0\tstring\t\tP3\t\timage/x-portable-pixmap\t7bit\n#\t\t\t\t\tPBM \"rawbits\" file\n0\tstring\t\tP4\t\timage/x-portable-bitmap\n#\t\t\t\t\tPGM \"rawbits\" file\n0\tstring\t\tP5\t\timage/x-portable-greymap\n#\t\t\t\t\tPPM \"rawbits\" file\n0\tstring\t\tP6\t\timage/x-portable-pixmap\n\n# NIFF (Navy Interchange File Format, a modification of TIFF)\n# [GRR:  this *must* go before TIFF]\n0\tstring\t\tIIN1\t\timage/x-niff\n\n# TIFF and friends\n#\t\t\t\t\tTIFF file, big-endian\n0\tstring\t\tMM\t\timage/tiff\n#\t\t\t\t\tTIFF file, little-endian\n0\tstring\t\tII\t\timage/tiff\n\n# possible GIF replacements; none yet released!\n# (Greg Roelofs, newt@uchicago.edu)\n#\n# GRR 950115:  this was mine (\"Zip GIF\"):\n#\t\t\t\t\tZIF image (GIF+deflate alpha)\n0\tstring\t\tGIF94z\t\timage/unknown\n#\n# GRR 950115:  this is Jeremy Wohl's Free Graphics Format (better):\n#\t\t\t\t\tFGF image (GIF+deflate beta)\n0\tstring\t\tFGF95a\t\timage/unknown\n#\n# GRR 950115:  this is Thomas Boutell's Portable Bitmap Format proposal\n# (best; not yet implemented):\n#\t\t\t\t\tPBF image (deflate compression)\n0\tstring\t\tPBF\t\timage/unknown\n\n# GIF\n0\tstring\t\tGIF\t\timage/gif\n\n# JPEG images\n0\tbeshort\t\t0xffd8\t\timage/jpeg\n\n# PC bitmaps (OS/2, Windoze BMP files)  (Greg Roelofs, newt@uchicago.edu)\n0\tstring\t\tBM\t\timage/bmp\n#>14\tbyte\t\t12\t\t(OS/2 1.x format)\n#>14\tbyte\t\t64\t\t(OS/2 2.x format)\n#>14\tbyte\t\t40\t\t(Windows 3.x format)\n#0\tstring\t\tIC\t\ticon\n#0\tstring\t\tPI\t\tpointer\n#0\tstring\t\tCI\t\tcolor icon\n#0\tstring\t\tCP\t\tcolor pointer\n#0\tstring\t\tBA\t\tbitmap array\n\n0\tstring\t\t\\x89PNG\t\timage/png\n0\tstring\t\tFWS\t\tapplication/x-shockwave-flash\n0\tstring\t\tCWS\t\tapplication/x-shockwave-flash\n\n#------------------------------------------------------------------------------\n# lisp:  file(1) magic for lisp programs\n#\n# various lisp types, from Daniel Quinlan (quinlan@yggdrasil.com)\n0\tstring\t;;\t\t\ttext/plain\t8bit\n# Emacs 18 - this is always correct, but not very magical.\n0\tstring\t\\012(\t\t\tapplication/x-elc\n# Emacs 19\n0\tstring\t;ELC\\023\\000\\000\\000\tapplication/x-elc\n\n#------------------------------------------------------------------------------\n# mail.news:  file(1) magic for mail and news\n#\n# There are tests to ascmagic.c to cope with mail and news.\n0\tstring\t\tRelay-Version: \tmessage/rfc822\t7bit\n0\tstring\t\t#!\\ rnews\tmessage/rfc822\t7bit\n0\tstring\t\tN#!\\ rnews\tmessage/rfc822\t7bit\n0\tstring\t\tForward\\ to \tmessage/rfc822\t7bit\n0\tstring\t\tPipe\\ to \tmessage/rfc822\t7bit\n0\tstring\t\tReturn-Path:\tmessage/rfc822\t7bit\n0\tstring\t\tPath:\t\tmessage/news\t8bit\n0\tstring\t\tXref:\t\tmessage/news\t8bit\n0\tstring\t\tFrom:\t\tmessage/rfc822\t7bit\n0\tstring\t\tArticle \tmessage/news\t8bit\n#------------------------------------------------------------------------------\n# msword: file(1) magic for MS Word files\n#\n# Contributor claims:\n# Reversed-engineered MS Word magic numbers\n#\n\n0\tstring\t\t\\376\\067\\0\\043\t\t\tapplication/msword\n0\tstring\t\t\\333\\245-\\0\\0\\0\t\t\tapplication/msword\n\n# disable this one because it applies also to other\n# Office/OLE documents for which msword is not correct. See PR#2608.\n#0\tstring\t\t\\320\\317\\021\\340\\241\\261\tapplication/msword\n\n\n\n#------------------------------------------------------------------------------\n# printer:  file(1) magic for printer-formatted files\n#\n\n# PostScript\n0\tstring\t\t%!\t\tapplication/postscript\n0\tstring\t\t\\004%!\t\tapplication/postscript\n\n# Acrobat\n# (due to clamen@cs.cmu.edu)\n0\tstring\t\t%PDF-\t\tapplication/pdf\n\n#------------------------------------------------------------------------------\n# sc:  file(1) magic for \"sc\" spreadsheet\n#\n38\tstring\t\tSpreadsheet\tapplication/x-sc\n\n#------------------------------------------------------------------------------\n# tex:  file(1) magic for TeX files\n#\n# XXX - needs byte-endian stuff (big-endian and little-endian DVI?)\n#\n# From <conklin@talisman.kaleida.com>\n\n# Although we may know the offset of certain text fields in TeX DVI\n# and font files, we can't use them reliably because they are not\n# zero terminated. [but we do anyway, christos]\n0\tstring\t\t\\367\\002\tapplication/x-dvi\n#0\tstring\t\t\\367\\203\tTeX generic font data\n#0\tstring\t\t\\367\\131\tTeX packed font data\n#0\tstring\t\t\\367\\312\tTeX virtual font data\n#0\tstring\t\tThis\\ is\\ TeX,\tTeX transcript text\t\n#0\tstring\t\tThis\\ is\\ METAFONT,\tMETAFONT transcript text\n\n# There is no way to detect TeX Font Metric (*.tfm) files without\n# breaking them apart and reading the data.  The following patterns\n# match most *.tfm files generated by METAFONT or afm2tfm.\n#2\tstring\t\t\\000\\021\tTeX font metric data\n#2\tstring\t\t\\000\\022\tTeX font metric data\n#>34\tstring\t\t>\\0\t\t(%s)\n\n# Texinfo and GNU Info, from Daniel Quinlan (quinlan@yggdrasil.com)\n#0\tstring\t\t\\\\input\\ texinfo\tTexinfo source text\n#0\tstring\t\tThis\\ is\\ Info\\ file\tGNU Info text\n\n# correct TeX magic for Linux (and maybe more)\n# from Peter Tobias (tobias@server.et-inf.fho-emden.de)\n#\n0\tleshort\t\t0x02f7\t\tapplication/x-dvi\n\n# RTF - Rich Text Format\n0\tstring\t\t{\\\\rtf\t\tapplication/rtf\n\n#------------------------------------------------------------------------------\n# animation:  file(1) magic for animation/movie formats\n#\n# animation formats, originally from vax@ccwf.cc.utexas.edu (VaX#n8)\n#\t\t\t\t\t\tMPEG file\n0\tstring\t\t\\000\\000\\001\\263\tvideo/mpeg\n#\n# The contributor claims:\n#   I couldn't find a real magic number for these, however, this\n#   -appears- to work.  Note that it might catch other files, too,\n#   so BE CAREFUL!\n#\n# Note that title and author appear in the two 20-byte chunks\n# at decimal offsets 2 and 22, respectively, but they are XOR'ed with\n# 255 (hex FF)! DL format SUCKS BIG ROCKS.\n#\n#\t\t\t\t\t\tDL file version 1 , medium format (160x100, 4 images/screen)\n0\tbyte\t\t1\t\t\tvideo/unknown\n0\tbyte\t\t2\t\t\tvideo/unknown\n# Quicktime video, from Linus Walleij <triad@df.lth.se>\n# from Apple quicktime file format documentation.\n4   string      moov        video/quicktime\n4   string      mdat        video/quicktime\n\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_default_settings.conf",
    "content": "# This configuration file reflects default settings for Apache HTTP Server.\n# You may change these, but chances are that you may not need to.\n\n# Timeout: The number of seconds before receives and sends time out.\nTimeout 300\n\n# KeepAlive: Whether or not to allow persistent connections (more than\n# one request per connection). Set to \"Off\" to deactivate.\nKeepAlive On\n\n# MaxKeepAliveRequests: The maximum number of requests to allow\n# during a persistent connection. Set to 0 to allow an unlimited amount.\n# We recommend you leave this number high, for maximum performance.\nMaxKeepAliveRequests 100\n\n# KeepAliveTimeout: Number of seconds to wait for the next request from the\n# same client on the same connection.\nKeepAliveTimeout 15\n\n# UseCanonicalName: Determines how Apache constructs self-referencing\n# URLs and the SERVER_NAME and SERVER_PORT variables.\n# When set \"Off\", Apache will use the Hostname and Port supplied\n# by the client.  When set \"On\", Apache will use the value of the\n# ServerName directive.\nUseCanonicalName Off\n\n# AccessFileName: The name of the file to look for in each directory\n# for additional configuration directives.  See also the AllowOverride\n# directive.\nAccessFileName .htaccess\n\n# ServerTokens\n# This directive configures what you return as the Server HTTP response\n# Header. The default is 'Full' which sends information about the OS-Type\n# and compiled in modules.\n# Set to one of:  Full | OS | Minor | Minimal | Major | Prod\n# where Full conveys the most information, and Prod the least.\nServerTokens Prod\n\n# TraceEnable\n# This directive overrides the behavior of TRACE for both the core server and\n# mod_proxy. The default TraceEnable on permits TRACE requests per RFC 2616,\n# which disallows any request body to accompany the request. TraceEnable off\n# causes the core server and mod_proxy to return a 405 (Method not allowed)\n# error to the client.\n# For security reasons this is turned off by default. (bug #240680)\nTraceEnable off\n\n# Optionally add a line containing the server version and virtual host\n# name to server-generated pages (internal error documents, FTP directory\n# listings, mod_status and mod_info output etc., but not CGI generated\n# documents or custom error documents).\n# Set to \"EMail\" to also include a mailto: link to the ServerAdmin.\n# Set to one of:  On | Off | EMail\nServerSignature On\n\n# HostnameLookups: Log the names of clients or just their IP addresses\n# e.g., www.apache.org (on) or 204.62.129.132 (off).\n# The default is off because it'd be overall better for the net if people\n# had to knowingly turn this feature on, since enabling it means that\n# each client request will result in AT LEAST one lookup request to the\n# nameserver.\nHostnameLookups Off\n\n# EnableMMAP and EnableSendfile: On systems that support it,\n# memory-mapping or the sendfile syscall is used to deliver\n# files.  This usually improves server performance, but must\n# be turned off when serving from networked-mounted \n# filesystems or if support for these functions is otherwise\n# broken on your system.\nEnableMMAP On\nEnableSendfile Off\n\n# FileETag: Configures the file attributes that are used to create\n# the ETag (entity tag) response header field when the document is\n# based on a static file. (The ETag value is used in cache management\n# to save network bandwidth.)\nFileETag MTime Size\n\n# ContentDigest: This directive enables the generation of Content-MD5\n# headers as defined in RFC1864 respectively RFC2616.\n# The Content-MD5 header provides an end-to-end message integrity\n# check (MIC) of the entity-body. A proxy or client may check this\n# header for detecting accidental modification of the entity-body\n# in transit.\n# Note that this can cause performance problems on your server since\n# the message digest is computed on every request (the values are\n# not cached).\n# Content-MD5 is only sent for documents served by the core, and not\n# by any module. For example, SSI documents, output from CGI scripts,\n# and byte range responses do not have this header.\nContentDigest Off\n\n# ErrorLog: The location of the error log file.\n# If you do not specify an ErrorLog directive within a <VirtualHost>\n# container, error messages relating to that virtual host will be\n# logged here.  If you *do* define an error logfile for a <VirtualHost>\n# container, that host's errors will be logged there and not here.\nErrorLog /var/log/apache2/error_log\n\n# LogLevel: Control the number of messages logged to the error_log.\n# Possible values include: debug, info, notice, warn, error, crit,\n# alert, emerg.\nLogLevel warn\n\n# We configure the \"default\" to be a very restrictive set of features.\n<Directory />\n\tOptions FollowSymLinks\n\tAllowOverride None\n\tRequire all denied\n</Directory>\n\n# DirectoryIndex: sets the file that Apache will serve if a directory\n# is requested.\n#\n# The index.html.var file (a type-map) is used to deliver content-\n# negotiated documents. The MultiViews Options can be used for the\n# same purpose, but it is much slower.\n#\n# Do not change this entry unless you know what you are doing.\n<IfModule dir_module>\n\tDirectoryIndex index.html index.html.var\n</IfModule>\n\n# The following lines prevent .htaccess and .htpasswd files from being\n# viewed by Web clients.\n<FilesMatch \"^\\.ht\">\n\tRequire all denied\n</FilesMatch>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_error_documents.conf",
    "content": "# The configuration below implements multi-language error documents through\n# content-negotiation.\n\n# Customizable error responses come in three flavors:\n# 1) plain text 2) local redirects 3) external redirects\n# Some examples:\n#ErrorDocument 500 \"The server made a boo boo.\"\n#ErrorDocument 404 /missing.html\n#ErrorDocument 404 \"/cgi-bin/missing_handler.pl\"\n#ErrorDocument 402 http://www.example.com/subscription_info.html\n\n# Required modules: mod_alias, mod_include, mod_negotiation\n# We use Alias to redirect any /error/HTTP_<error>.html.var response to\n# our collection of by-error message multi-language collections. We use\n# includes to substitute the appropriate text.\n# You can modify the messages' appearance without changing any of the\n# default HTTP_<error>.html.var files by adding the line:\n#   Alias /error/include/ \"/your/include/path/\"\n# which allows you to create your own set of files by starting with the\n# /var/www/localhost/error/include/ files and copying them to /your/include/path/, \n# even on a per-VirtualHost basis. The default include files will display\n# your Apache version number and your ServerAdmin email address regardless\n# of the setting of ServerSignature.\n\n<IfDefine ERRORDOCS>\nAlias /error/ \"/usr/share/apache2/error/\"\n\n<Directory \"/usr/share/apache2/error\">\n\tAllowOverride None\n\tOptions IncludesNoExec\n\tAddOutputFilter Includes html\n\tAddHandler type-map var\n\tRequire all granted\n\tLanguagePriority en cs de es fr it ja ko nl pl pt-br ro sv tr\n\tForceLanguagePriority Prefer Fallback\n</Directory>\n\nErrorDocument 400 /error/HTTP_BAD_REQUEST.html.var\nErrorDocument 401 /error/HTTP_UNAUTHORIZED.html.var\nErrorDocument 403 /error/HTTP_FORBIDDEN.html.var\nErrorDocument 404 /error/HTTP_NOT_FOUND.html.var\nErrorDocument 405 /error/HTTP_METHOD_NOT_ALLOWED.html.var\nErrorDocument 408 /error/HTTP_REQUEST_TIME_OUT.html.var\nErrorDocument 410 /error/HTTP_GONE.html.var\nErrorDocument 411 /error/HTTP_LENGTH_REQUIRED.html.var\nErrorDocument 412 /error/HTTP_PRECONDITION_FAILED.html.var\nErrorDocument 413 /error/HTTP_REQUEST_ENTITY_TOO_LARGE.html.var\nErrorDocument 414 /error/HTTP_REQUEST_URI_TOO_LARGE.html.var\nErrorDocument 415 /error/HTTP_UNSUPPORTED_MEDIA_TYPE.html.var\nErrorDocument 500 /error/HTTP_INTERNAL_SERVER_ERROR.html.var\nErrorDocument 501 /error/HTTP_NOT_IMPLEMENTED.html.var\nErrorDocument 502 /error/HTTP_BAD_GATEWAY.html.var\nErrorDocument 503 /error/HTTP_SERVICE_UNAVAILABLE.html.var\nErrorDocument 506 /error/HTTP_VARIANT_ALSO_VARIES.html.var\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_languages.conf",
    "content": "# Settings for hosting different languages.\n<IfDefine LANGUAGE>\n# DefaultLanguage and AddLanguage allows you to specify the language of\n# a document. You can then use content negotiation to give a browser a\n# file in a language the user can understand.\n#\n# Specify a default language. This means that all data\n# going out without a specific language tag (see below) will\n# be marked with this one. You probably do NOT want to set\n# this unless you are sure it is correct for all cases.\n#\n# It is generally better to not mark a page as\n# being a certain language than marking it with the wrong\n# language!\n#\n# DefaultLanguage nl\n#\n# Note 1: The suffix does not have to be the same as the language\n# keyword --- those with documents in Polish (whose net-standard\n# language code is pl) may wish to use \"AddLanguage pl .po\" to\n# avoid the ambiguity with the common suffix for perl scripts.\n#\n# Note 2: The example entries below illustrate that in some cases\n# the two character 'Language' abbreviation is not identical to\n# the two character 'Country' code for its country,\n# E.g. 'Danmark/dk' versus 'Danish/da'.\n#\n# Note 3: In the case of 'ltz' we violate the RFC by using a three char\n# specifier. There is 'work in progress' to fix this and get\n# the reference data for rfc1766 cleaned up.\n#\n# Catalan (ca) - Croatian (hr) - Czech (cs) - Danish (da) - Dutch (nl)\n# English (en) - Esperanto (eo) - Estonian (et) - French (fr) - German (de)\n# Greek-Modern (el) - Hebrew (he) - Italian (it) - Japanese (ja)\n# Korean (ko) - Luxembourgeois* (ltz) - Norwegian Nynorsk (nn)\n# Norwegian (no) - Polish (pl) - Portuguese (pt)\n# Brazilian Portuguese (pt-BR) - Russian (ru) - Swedish (sv)\n# Simplified Chinese (zh-CN) - Spanish (es) - Traditional Chinese (zh-TW)\nAddLanguage ca .ca\nAddLanguage cs .cz .cs\nAddLanguage da .dk\nAddLanguage de .de\nAddLanguage el .el\nAddLanguage en .en\nAddLanguage eo .eo\nAddLanguage es .es\nAddLanguage et .et\nAddLanguage fr .fr\nAddLanguage he .he\nAddLanguage hr .hr\nAddLanguage it .it\nAddLanguage ja .ja\nAddLanguage ko .ko\nAddLanguage ltz .ltz\nAddLanguage nl .nl\nAddLanguage nn .nn\nAddLanguage no .no\nAddLanguage pl .po\nAddLanguage pt .pt\nAddLanguage pt-BR .pt-br\nAddLanguage ru .ru\nAddLanguage sv .sv\nAddLanguage zh-CN .zh-cn\nAddLanguage zh-TW .zh-tw\n\n# LanguagePriority allows you to give precedence to some languages\n# in case of a tie during content negotiation.\n#\n# Just list the languages in decreasing order of preference. We have\n# more or less alphabetized them here. You probably want to change this.\nLanguagePriority en ca cs da de el eo es et fr he hr it ja ko ltz nl nn no pl pt pt-BR ru sv zh-CN zh-TW\n\n# ForceLanguagePriority allows you to serve a result page rather than\n# MULTIPLE CHOICES (Prefer) [in case of a tie] or NOT ACCEPTABLE (Fallback)\n# [in case no accepted languages matched the available variants]\nForceLanguagePriority Prefer Fallback\n\n# Commonly used filename extensions to character sets. You probably\n# want to avoid clashes with the language extensions, unless you\n# are good at carefully testing your setup after each change.\n# See http://www.iana.org/assignments/character-sets for the\n# official list of charset names and their respective RFCs.\nAddCharset us-ascii.ascii\t.us-ascii\nAddCharset ISO-8859-1\t\t.iso8859-1 .latin1\nAddCharset ISO-8859-2\t\t.iso8859-2 .latin2 .cen\nAddCharset ISO-8859-3\t\t.iso8859-3 .latin3\nAddCharset ISO-8859-4\t\t.iso8859-4 .latin4\nAddCharset ISO-8859-5\t\t.iso8859-5 .cyr .iso-ru\nAddCharset ISO-8859-6\t\t.iso8859-6 .arb .arabic\nAddCharset ISO-8859-7\t\t.iso8859-7 .grk .greek\nAddCharset ISO-8859-8\t\t.iso8859-8 .heb .hebrew\nAddCharset ISO-8859-9\t\t.iso8859-9 .latin5 .trk\nAddCharset ISO-8859-10\t\t.iso8859-10 .latin6\nAddCharset ISO-8859-13\t\t.iso8859-13\nAddCharset ISO-8859-14\t\t.iso8859-14 .latin8\nAddCharset ISO-8859-15\t\t.iso8859-15 .latin9\nAddCharset ISO-8859-16\t\t.iso8859-16 .latin10\nAddCharset ISO-2022-JP\t\t.iso2022-jp .jis\nAddCharset ISO-2022-KR\t\t.iso2022-kr .kis\nAddCharset ISO-2022-CN\t\t.iso2022-cn .cis\nAddCharset Big5.Big5\t\t.big5 .b5\nAddCharset cn-Big5\t\t\t.cn-big5\n# For russian, more than one charset is used (depends on client, mostly):\nAddCharset WINDOWS-1251\t\t.cp-1251 .win-1251\nAddCharset CP866\t\t\t.cp866\nAddCharset KOI8\t\t\t\t.koi8\nAddCharset KOI8-E\t\t\t.koi8-e\nAddCharset KOI8-r\t\t\t.koi8-r .koi8-ru\nAddCharset KOI8-U\t\t\t.koi8-u\nAddCharset KOI8-ru\t\t\t.koi8-uk .ua\nAddCharset ISO-10646-UCS-2\t.ucs2\nAddCharset ISO-10646-UCS-4\t.ucs4\nAddCharset UTF-7\t\t\t.utf7\nAddCharset UTF-8\t\t\t.utf8\nAddCharset UTF-16\t\t\t.utf16\nAddCharset UTF-16BE\t\t\t.utf16be\nAddCharset UTF-16LE\t\t\t.utf16le\nAddCharset UTF-32\t\t\t.utf32\nAddCharset UTF-32BE\t\t\t.utf32be\nAddCharset UTF-32LE\t\t\t.utf32le\nAddCharset euc-cn\t\t\t.euc-cn\nAddCharset euc-gb\t\t\t.euc-gb\nAddCharset euc-jp\t\t\t.euc-jp\nAddCharset euc-kr\t\t\t.euc-kr\n# Not sure how euc-tw got in - IANA doesn't list it???\nAddCharset EUC-TW\t\t\t.euc-tw\nAddCharset gb2312\t\t\t.gb2312 .gb\nAddCharset iso-10646-ucs-2\t.ucs-2 .iso-10646-ucs-2\nAddCharset iso-10646-ucs-4\t.ucs-4 .iso-10646-ucs-4\nAddCharset shift_jis\t\t.shift_jis .sjis\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_autoindex.conf",
    "content": "<IfModule autoindex_module>\n<IfDefine !NO_AUTOINDEX_CONF>\n\n<IfModule alias_module>\n# We include the /icons/ alias for FancyIndexed directory listings.  If\n# you do not use FancyIndexing, you may comment this out.\nAlias /icons/ \"/usr/share/apache2/icons/\"\n\n<Directory \"/usr/share/apache2/icons\">\n\tOptions Indexes MultiViews\n\tAllowOverride None\n\tRequire all granted\n</Directory>\n</IfModule>\n\n# Directives controlling the display of server-generated directory listings.\n#\n# To see the listing of a directory, the Options directive for the\n# directory must include \"Indexes\", and the directory must not contain\n# a file matching those listed in the DirectoryIndex directive.\n\n# IndexOptions: Controls the appearance of server-generated directory\n# listings.\nIndexOptions FancyIndexing VersionSort\n\n# AddIcon* directives tell the server which icon to show for different\n# files or filename extensions.  These are only displayed for\n# FancyIndexed directories.\nAddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip\n\nAddIconByType (TXT,/icons/text.gif) text/*\nAddIconByType (IMG,/icons/image2.gif) image/*\nAddIconByType (SND,/icons/sound2.gif) audio/*\nAddIconByType (VID,/icons/movie.gif) video/*\n\nAddIcon /icons/binary.gif .bin .exe\nAddIcon /icons/binhex.gif .hqx\nAddIcon /icons/tar.gif .tar\nAddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv\nAddIcon /icons/compressed.gif .Z .z .tgz .gz .zip\nAddIcon /icons/a.gif .ps .ai .eps\nAddIcon /icons/layout.gif .html .shtml .htm .pdf\nAddIcon /icons/text.gif .txt\nAddIcon /icons/c.gif .c\nAddIcon /icons/p.gif .pl .py\nAddIcon /icons/f.gif .for\nAddIcon /icons/dvi.gif .dvi\nAddIcon /icons/uuencoded.gif .uu\nAddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl\nAddIcon /icons/tex.gif .tex\nAddIcon /icons/bomb.gif core\n\nAddIcon /icons/back.gif ..\nAddIcon /icons/hand.right.gif README\nAddIcon /icons/folder.gif ^^DIRECTORY^^\nAddIcon /icons/blank.gif ^^BLANKICON^^\n\n# DefaultIcon is which icon to show for files which do not have an icon\n# explicitly set.\nDefaultIcon /icons/unknown.gif\n\n# AddDescription allows you to place a short description after a file in\n# server-generated indexes.  These are only displayed for FancyIndexed\n# directories.\n# Format: AddDescription \"description\" filename\n\n#AddDescription \"GZIP compressed document\" .gz\n#AddDescription \"tar archive\" .tar\n#AddDescription \"GZIP compressed tar archive\" .tgz\n\n# ReadmeName is the name of the README file the server will look for by\n# default, and append to directory listings.\n\n# HeaderName is the name of a file which should be prepended to \n# directory indexes. \nReadmeName README.html\nHeaderName HEADER.html\n\n# IndexIgnore is a set of filenames which directory indexing should ignore\n# and not include in the listing.  Shell-style wildcarding is permitted.\nIndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t\n</IfDefine>\n</IfModule>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_info.conf",
    "content": "<IfDefine INFO>\n# Allow remote server configuration reports, with the URL of\n# http://servername/server-info\n<Location /server-info>\n\tSetHandler server-info\n\tRequire local\n</Location>\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_log_config.conf",
    "content": "<IfModule log_config_module>\n# The following directives define some format nicknames for use with\n# a CustomLog directive (see below).\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\"\" combined\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %b\" common\n\nLogFormat \"%{Referer}i -> %U\" referer\nLogFormat \"%{User-Agent}i\" agent\nLogFormat \"%v %h %l %u %t \\\"%r\\\" %>s %b %T\" script\nLogFormat \"%v %h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" VLOG=%{VLOG}e\" vhost\n\n<IfModule logio_module>\n# You need to enable mod_logio.c to use %I and %O\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O\" combinedio\nLogFormat \"%v %h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-Agent}i\\\" %I %O\" vhostio\n</IfModule>\n\n# The location and format of the access logfile (Common Logfile Format).\n# If you do not define any access logfiles within a <VirtualHost>\n# container, they will be logged here.  Contrariwise, if you *do*\n# define per-<VirtualHost> access logfiles, transactions will be\n# logged therein and *not* in this file.\nCustomLog /var/log/apache2/access_log common\n\n# If you would like to have agent and referer logfiles,\n# uncomment the following directives.\n#CustomLog /var/log/apache2/referer_log referer\n#CustomLog /var/log/apache2/agent_logs agent\n\n# If you prefer a logfile with access, agent, and referer information\n# (Combined Logfile Format) you can use the following directive.\n#CustomLog /var/log/apache2/access_log combined\n</IfModule>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_mime.conf",
    "content": "<IfModule mime_module>\n# TypesConfig points to the file containing the list of mappings from\n# filename extension to MIME-type.\nTypesConfig /etc/mime.types\n\n# AddType allows you to add to or override the MIME configuration\n# file specified in TypesConfig for specific file types.\n#AddType application/x-gzip .tgz\n\n# AddEncoding allows you to have certain browsers uncompress\n# information on the fly. Note: Not all browsers support this.\n#AddEncoding x-compress .Z\n#AddEncoding x-gzip .gz .tgz\n\n# If the AddEncoding directives above are commented-out, then you\n# probably should define those extensions to indicate media types:\nAddType application/x-compress .Z\nAddType application/x-gzip .gz .tgz\n\n# AddHandler allows you to map certain file extensions to \"handlers\":\n# actions unrelated to filetype. These can be either built into the server\n# or added with the Action directive (see below)\n\n# To use CGI scripts outside of ScriptAliased directories:\n# (You will also need to add \"ExecCGI\" to the \"Options\" directive.)\n#AddHandler cgi-script .cgi\n\n# For type maps (negotiated resources):\n#AddHandler type-map var\n\n# Filters allow you to process content before it is sent to the client.\n#\n# To parse .shtml files for server-side includes (SSI):\n# (You will also need to add \"Includes\" to the \"Options\" directive.)\n#AddType text/html .shtml\n#AddOutputFilter INCLUDES .shtml\n</IfModule>\n\n<IfModule mime_magic_module>\n# The mod_mime_magic module allows the server to use various hints from the\n# contents of the file itself to determine its type.  The MIMEMagicFile\n# directive tells the module where the hint definitions are located.\nMIMEMagicFile /etc/apache2/magic\n</IfModule>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_status.conf",
    "content": "<IfDefine STATUS>\n# Allow server status reports generated by mod_status,\n# with the URL of http://servername/server-status\n<Location /server-status>\n\tSetHandler server-status\n\tRequire local\n</Location>\n\n# ExtendedStatus controls whether Apache will generate \"full\" status\n# information (ExtendedStatus On) or just basic information (ExtendedStatus\n# Off) when the \"server-status\" handler is called.\nExtendedStatus On\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mod_userdir.conf",
    "content": "# Settings for user home directories\n<IfDefine USERDIR>\n# UserDir: The name of the directory that is appended onto a user's home\n# directory if a ~user request is received.  Note that you must also set\n# the default access control for these directories, as in the example below.\nUserDir public_html\n\n# Control access to UserDir directories.  The following is an example\n# for a site where these directories are restricted to read-only.\n<Directory /home/*/public_html>\n\tAllowOverride FileInfo AuthConfig Limit Indexes\n\tOptions MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec\n\t<Limit GET POST OPTIONS>\n\t\tRequire all granted\n\t</Limit>\n\t<LimitExcept GET POST OPTIONS>\n\t\tRequire all denied\n\t</LimitExcept>\n</Directory>\n\n# Suexec isn't really required to run cgi-scripts, but it's a really good\n# idea if you have multiple users serving websites...\n<IfDefine SUEXEC>\n<Directory /home/*/public_html/cgi-bin>\n\tOptions ExecCGI\n\tSetHandler cgi-script\n</Directory>\n</IfDefine>\n\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/00_mpm.conf",
    "content": "# Server-Pool Management (MPM specific)\n\n# PidFile: The file in which the server should record its process\n# identification number when it starts.\n#\n# DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING\nPidFile /run/apache2.pid\n\n# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.\n# Mutex file:/run/apache_mpm_mutex\n\n# Only one of the below sections will be relevant on your\n# installed httpd.  Use \"/usr/sbin/apache2 -l\" to find out the\n# active mpm.\n\n# common MPM configuration\n# These configuration directives apply to all MPMs\n#\n# StartServers: Number of child server processes created at startup\n# MaxRequestWorkers: Maximum number of child processes to serve requests\n# MaxConnectionsPerChild: Limit on the number of connections that an individual\n#                         child server will handle during its life\n\n\n# prefork MPM\n# This is the default MPM if USE=-threads\n#\n# MinSpareServers: Minimum number of idle child server processes\n# MaxSpareServers: Maximum number of idle child server processes\n<IfModule mpm_prefork_module>\n\tStartServers\t\t5\n\tMinSpareServers\t\t5\n\tMaxSpareServers\t\t10\n\tMaxRequestWorkers\t150\n\tMaxConnectionsPerChild\t10000\n</IfModule>\n\n# worker MPM\n# This is the default MPM if USE=threads\n#\n# MinSpareThreads: Minimum number of idle threads available to handle request spikes\n# MaxSpareThreads: Maximum number of idle threads\n# ThreadsPerChild: Number of threads created by each child process\n<IfModule mpm_worker_module>\n\tStartServers\t\t2\n\tMinSpareThreads\t\t25\n\tMaxSpareThreads\t\t75\n\tThreadsPerChild\t\t25\n\tMaxRequestWorkers\t150\n\tMaxConnectionsPerChild\t10000\n</IfModule>\n\n# event MPM\n#\n# MinSpareThreads: Minimum number of idle threads available to handle request spikes\n# MaxSpareThreads: Maximum number of idle threads\n# ThreadsPerChild: Number of threads created by each child process\n<IfModule mpm_event_module>\n\tStartServers\t\t2\n\tMinSpareThreads\t\t25\n\tMaxSpareThreads\t\t75\n\tThreadsPerChild\t\t25\n\tMaxRequestWorkers\t150\n\tMaxConnectionsPerChild\t10000\n</IfModule>\n\n# peruser MPM\n#\n# MinSpareProcessors: Minimum number of idle child server processes\n# MinProcessors: Minimum number of processors per virtual host\n# MaxProcessors: Maximum number of processors per virtual host\n# ExpireTimeout: Maximum idle time before a child is killed, 0 to disable\n# Multiplexer: Specify a Multiplexer child configuration.\n# Processor: Specify a user and group for a specific child process\n<IfModule mpm_peruser_module>\n\tMinSpareProcessors\t2\n\tMinProcessors\t\t2\n\tMaxProcessors\t\t10\n\tMaxRequestWorkers\t150\n\tMaxConnectionsPerChild\t1000\n\tExpireTimeout\t\t1800\n\n\tMultiplexer nobody nobody\n\tProcessor apache apache\n</IfModule>\n\n# itk MPM\n#\n# MinSpareServers: Minimum number of idle child server processes\n# MaxSpareServers: Maximum number of idle child server processes\n<IfModule mpm_itk_module>\n\tStartServers\t\t5\n\tMinSpareServers\t\t5\n\tMaxSpareServers\t\t10\n\tMaxRequestWorkers\t150\n\tMaxConnectionsPerChild\t10000\n</IfModule>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/10_mod_mem_cache.conf",
    "content": "<IfDefine MEM_CACHE>\n# 128MB cache for objects < 2MB\nCacheEnable mem /\nMCacheSize 131072\nMCacheMaxObjectCount 1000\nMCacheMinObjectSize 1\nMCacheMaxObjectSize 2097152\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/40_mod_ssl.conf",
    "content": "# Note: The following must must be present to support\n# starting without SSL on platforms with no /dev/random equivalent\n# but a statically compiled-in mod_ssl.\n<IfModule ssl_module>\nSSLRandomSeed startup builtin\nSSLRandomSeed connect builtin\n</IfModule>\n\n<IfDefine SSL>\n# This is the Apache server configuration file providing SSL support.\n# It contains the configuration directives to instruct the server how to\n# serve pages over an https connection. For detailing information about these \n# directives see <URL:http://httpd.apache.org/docs/2.4/mod/mod_ssl.html>\n\n# Do NOT simply read the instructions in here without understanding\n# what they do.  They're here only as hints or reminders. If you are unsure\n# consult the online docs. You have been warned.\n\n## Pseudo Random Number Generator (PRNG):\n# Configure one or more sources to seed the PRNG of the SSL library.\n# The seed data should be of good random quality.\n# WARNING! On some platforms /dev/random blocks if not enough entropy\n# is available. This means you then cannot use the /dev/random device\n# because it would lead to very long connection times (as long as\n# it requires to make more entropy available). But usually those\n# platforms additionally provide a /dev/urandom device which doesn't\n# block. So, if available, use this one instead. Read the mod_ssl User\n# Manual for more details.\n#SSLRandomSeed startup file:/dev/random  512\n#SSLRandomSeed startup file:/dev/urandom 512\n#SSLRandomSeed connect file:/dev/random  512\n#SSLRandomSeed connect file:/dev/urandom 512\n\n## SSL Global Context:\n# All SSL configuration in this context applies both to the main server and \n# all SSL-enabled virtual hosts.\n\n# Some MIME-types for downloading Certificates and CRLs\n<IfModule mime_module>\n\tAddType application/x-x509-ca-cert .crt\n\tAddType application/x-pkcs7-crl    .crl\n</IfModule>\n\n## Pass Phrase Dialog:\n# Configure the pass phrase gathering process. The filtering dialog program \n# (`builtin' is an internal terminal dialog) has to provide the pass phrase on\n# stdout.\nSSLPassPhraseDialog  builtin\n\n## Inter-Process Session Cache:\n# Configure the SSL Session Cache: First the mechanism  to use and second the\n# expiring timeout (in seconds).\n#SSLSessionCache\t\tdbm:/run/ssl_scache\nSSLSessionCache\t\t\tshmcb:/run/ssl_scache(512000)\nSSLSessionCacheTimeout  300\n\n## Semaphore:\n# Configure the path to the mutual exclusion semaphore the SSL engine uses\n# internally for inter-process synchronization.\nMutex file:/run/apache_ssl_mutex ssl-cache\n\n## SSL Compression:\n# Known to be vulnerable thus disabled by default (bug #507324).\nSSLCompression off\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/41_mod_http2.conf",
    "content": "<IfDefine SSL>\n  <IfModule http2_module>\n    # enable debugging for this module\n    #LogLevel http2:info\n\n    #Enable HTTP/2 support\n    Protocols h2 h2c http/1.1\n  </IfModule>\n</IfDefine>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/45_mod_dav.conf",
    "content": "<IfDefine DAV>\nDavLockDB \"/var/lib/dav/lockdb\"\n\n# The following directives disable redirects on non-GET requests for\n# a directory that does not include the trailing slash.  This fixes a\n# problem with several clients that do not appropriately handle\n# redirects for folders with DAV methods.\n<IfModule setenvif_module>\nBrowserMatch \"Microsoft Data Access Internet Publishing Provider\" redirect-carefully\nBrowserMatch \"MS FrontPage\" redirect-carefully\nBrowserMatch \"^WebDrive\" redirect-carefully\nBrowserMatch \"^WebDAVFS/1.[012345678]\" redirect-carefully\nBrowserMatch \"^gnome-vfs/1.0\" redirect-carefully\nBrowserMatch \"^XML Spy\" redirect-carefully\nBrowserMatch \"^Dreamweaver-WebDAV-SCM1\" redirect-carefully\n</IfModule>\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/modules.d/46_mod_ldap.conf",
    "content": "# Examples below are taken from the online documentation\n# Refer to:\n# http://localhost/manual/mod/mod_ldap.html\n# http://localhost/manual/mod/mod_auth_ldap.html\n<IfDefine LDAP>\nLDAPSharedCacheSize\t200000\nLDAPCacheEntries\t1024\nLDAPCacheTTL\t\t600\nLDAPOpCacheEntries\t1024\nLDAPOpCacheTTL\t\t600\n\n<Location /ldap-status>\n\tSetHandler ldap-status\n\tRequire local\n</Location>\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_ssl_vhost.conf",
    "content": "<IfDefine SSL>\n<IfDefine SSL_DEFAULT_VHOST>\n<IfModule ssl_module>\n# see bug #178966 why this is in here\n\n# When we also provide SSL we have to listen to the HTTPS port\n# Note: Configurations that use IPv6 but not IPv4-mapped addresses need two\n# Listen directives: \"Listen [::]:443\" and \"Listen 0.0.0.0:443\"\nListen 443\n\n<VirtualHost _default_:443>\n\tServerName localhost\n\tInclude /etc/apache2/vhosts.d/default_vhost.include\n\tErrorLog /var/log/apache2/ssl_error_log\n\n\t<IfModule log_config_module>\n\t\tTransferLog /var/log/apache2/ssl_access_log\n\t</IfModule>\n\n\t## SSL Engine Switch:\n\t# Enable/Disable SSL for this virtual host.\n\tSSLEngine on\n\n\t## SSLProtocol:\n\t# Don't use SSLv2 anymore as it's considered to be broken security-wise.\n\t# Also disable SSLv3 as most modern browsers are capable of TLS.\n\tSSLProtocol ALL -SSLv2 -SSLv3\n\n\t## SSL Cipher Suite:\n\t# List the ciphers that the client is permitted to negotiate.\n\t# See the mod_ssl documentation for a complete list.\n\t# This list of ciphers is recommended by mozilla and was stripped off\n\t# its RC4 ciphers. (bug #506924)\n\tSSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128:AES256:HIGH:!RC4:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK\n\n\t## SSLHonorCipherOrder:\n\t# Prefer the server's cipher preference order as the client may have a\n\t# weak default order.\n\tSSLHonorCipherOrder On\n\n\t## Server Certificate:\n\t# Point SSLCertificateFile at a PEM encoded certificate. If the certificate\n\t# is encrypted, then you will be prompted for a pass phrase. Note that a \n\t# kill -HUP will prompt again. Keep in mind that if you have both an RSA\n\t# and a DSA certificate you can configure both in parallel (to also allow\n\t# the use of DSA ciphers, etc.)\n\tSSLCertificateFile /etc/ssl/apache2/server.crt\n\n\t## Server Private Key:\n\t# If the key is not combined with the certificate, use this directive to\n\t# point at the key file. Keep in mind that if you've both a RSA and a DSA\n\t# private key you can configure both in parallel (to also allow the use of\n\t# DSA ciphers, etc.)\n\tSSLCertificateKeyFile /etc/ssl/apache2/server.key\n\n\t## Server Certificate Chain:\n\t# Point SSLCertificateChainFile at a file containing the concatenation of \n\t# PEM encoded CA certificates which form the certificate chain for the\n\t# server certificate. Alternatively the referenced file can be the same as\n\t# SSLCertificateFile when the CA certificates are directly appended to the\n\t# server certificate for convenience.\n\t#SSLCertificateChainFile /etc/ssl/apache2/ca.crt\n\n\t## Certificate Authority (CA):\n\t# Set the CA certificate verification path where to find CA certificates\n\t# for client authentication or alternatively one huge file containing all\n\t# of them (file must be PEM encoded).\n\t# Note: Inside SSLCACertificatePath you need hash symlinks to point to the\n\t# certificate files. Use the provided Makefile to update the hash symlinks\n\t# after changes.\n\t#SSLCACertificatePath /etc/ssl/apache2/ssl.crt\n\t#SSLCACertificateFile /etc/ssl/apache2/ca-bundle.crt\n\n\t## Certificate Revocation Lists (CRL):\n\t# Set the CA revocation path where to find CA CRLs for client authentication\n\t# or alternatively one huge file containing all of them (file must be PEM \n\t# encoded).\n\t# Note: Inside SSLCARevocationPath you need hash symlinks to point to the\n\t# certificate files. Use the provided Makefile to update the hash symlinks\n\t# after changes.\n\t#SSLCARevocationPath /etc/ssl/apache2/ssl.crl\n\t#SSLCARevocationFile /etc/ssl/apache2/ca-bundle.crl\n\n\t## Client Authentication (Type):\n\t# Client certificate verification type and depth. Types are none, optional,\n\t# require and optional_no_ca. Depth is a number which specifies how deeply\n\t# to verify the certificate issuer chain before deciding the certificate is\n\t# not valid.\n\t#SSLVerifyClient require\n\t#SSLVerifyDepth  10\n\n\t## Access Control:\n\t# With SSLRequire you can do per-directory access control based on arbitrary\n\t# complex boolean expressions containing server variable checks and other\n\t# lookup directives. The syntax is a mixture between C and Perl. See the\n\t# mod_ssl documentation for more details.\n\t#<Location />\n\t#\t#SSLRequire (    %{SSL_CIPHER} !~ m/^(EXP|NULL)/ \\\n\t#\tand %{SSL_CLIENT_S_DN_O} eq \"Snake Oil, Ltd.\" \\\n\t#\tand %{SSL_CLIENT_S_DN_OU} in {\"Staff\", \"CA\", \"Dev\"} \\\n\t#\tand %{TIME_WDAY} >= 1 and %{TIME_WDAY} <= 5 \\\n\t#\tand %{TIME_HOUR} >= 8 and %{TIME_HOUR} <= 20       ) \\\n\t#\tor %{REMOTE_ADDR} =~ m/^192\\.76\\.162\\.[0-9]+$/\n\t#</Location>\n\n\t## SSL Engine Options:\n\t# Set various options for the SSL engine.\n\n\t## FakeBasicAuth:\n\t# Translate the client X.509 into a Basic Authorisation. This means that the\n\t# standard Auth/DBMAuth methods can be used for access control. The user \n\t# name is the `one line' version of the client's X.509 certificate. \n\t# Note that no password is obtained from the user. Every entry in the user \n\t# file needs this password: `xxj31ZMTZzkVA'.\n\n\t## ExportCertData:\n\t# This exports two additional environment variables: SSL_CLIENT_CERT and \n\t# SSL_SERVER_CERT. These contain the PEM-encoded certificates of the server\n\t# (always existing) and the client (only existing when client \n\t# authentication is used). This can be used to import the certificates into\n\t# CGI scripts.\n\n\t## StdEnvVars:\n\t# This exports the standard SSL/TLS related `SSL_*' environment variables. \n\t# Per default this exportation is switched off for performance reasons, \n\t# because the extraction step is an expensive operation and is usually \n\t# useless for serving static content. So one usually enables the exportation\n\t# for CGI and SSI requests only.\n\n\t## StrictRequire:\n\t# This denies access when \"SSLRequireSSL\" or \"SSLRequire\" applied even under\n\t# a \"Satisfy any\" situation, i.e. when it applies access is denied and no\n\t# other module can change it.\n\n\t## OptRenegotiate:\n\t# This enables optimized SSL connection renegotiation handling when SSL \n\t# directives are used in per-directory context.\n\t#SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire\n\t<FilesMatch \"\\.(cgi|shtml|phtml|php)$\">\n\t\tSSLOptions +StdEnvVars\n\t</FilesMatch>\n\n\t<Directory \"/var/www/localhost/cgi-bin\">\n\t\tSSLOptions +StdEnvVars\n\t</Directory>\n\n\t## SSL Protocol Adjustments:\n\t# The safe and default but still SSL/TLS standard compliant shutdown\n\t# approach is that mod_ssl sends the close notify alert but doesn't wait\n\t# for the close notify alert from client. When you need a different\n\t# shutdown approach you can use one of the following variables:\n\n\t## ssl-unclean-shutdown:\n\t# This forces an unclean shutdown when the connection is closed, i.e. no\n\t# SSL close notify alert is send or allowed to received.  This violates the\n\t# SSL/TLS standard but is needed for some brain-dead browsers. Use this when\n\t# you receive I/O errors because of the standard approach where mod_ssl\n\t# sends the close notify alert.\n\n\t## ssl-accurate-shutdown:\n\t# This forces an accurate shutdown when the connection is closed, i.e. a\n\t# SSL close notify alert is send and mod_ssl waits for the close notify\n\t# alert of the client. This is 100% SSL/TLS standard compliant, but in\n\t# practice often causes hanging connections with brain-dead browsers. Use\n\t# this only for browsers where you know that their SSL implementation works\n\t# correctly. \n\t# Notice: Most problems of broken clients are also related to the HTTP \n\t# keep-alive facility, so you usually additionally want to disable \n\t# keep-alive for those clients, too. Use variable \"nokeepalive\" for this.\n\t# Similarly, one has to force some clients to use HTTP/1.0 to workaround\n\t# their broken HTTP/1.1 implementation. Use variables \"downgrade-1.0\" and\n\t# \"force-response-1.0\" for this.\n\t<IfModule setenvif_module>\n\t\tBrowserMatch \".*MSIE.*\" \\\n\t\t\tnokeepalive ssl-unclean-shutdown \\\n\t\t\tdowngrade-1.0 force-response-1.0\n\t</IfModule>\n\n\t## Per-Server Logging:\n\t# The home of a custom SSL log file. Use this when you want a compact \n\t# non-error SSL logfile on a virtual host basis.\n\t<IfModule log_config_module>\n\t\tCustomLog /var/log/apache2/ssl_request_log \\\n\t\t\t\"%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \\\"%r\\\" %b\"\n\t</IfModule>\n</VirtualHost>\n</IfModule>\n</IfDefine>\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/00_default_vhost.conf",
    "content": "# Virtual Hosts\n#\n# If you want to maintain multiple domains/hostnames on your\n# machine you can setup VirtualHost containers for them. Most configurations\n# use only name-based virtual hosts so the server doesn't need to worry about\n# IP addresses. This is indicated by the asterisks in the directives below.\n#\n# Please see the documentation at\n# <URL:http://httpd.apache.org/docs/2.4/vhosts/>\n# for further details before you try to setup virtual hosts.\n#\n# You may use the command line option '-S' to verify your virtual host\n# configuration.\n\n<IfDefine DEFAULT_VHOST>\n# see bug #178966 why this is in here\n\n# Listen: Allows you to bind Apache to specific IP addresses and/or\n# ports, instead of the default. See also the <VirtualHost>\n# directive.\n#\n# Change this to Listen on specific IP addresses as shown below to\n# prevent Apache from glomming onto all bound IP addresses.\n#\n#Listen 12.34.56.78:80\nListen 80\n\n# When virtual hosts are enabled, the main host defined in the default\n# httpd.conf configuration will go away. We redefine it here so that it is\n# still available.\n#\n# If you disable this vhost by removing -D DEFAULT_VHOST from\n# /etc/conf.d/apache2, the first defined virtual host elsewhere will be\n# the default.\n<VirtualHost *:80>\n\tServerName localhost\n\tInclude /etc/apache2/vhosts.d/default_vhost.include\n\n\t<IfModule mpm_peruser_module>\n\t\tServerEnvironment apache apache\n\t</IfModule>\n</VirtualHost>\n</IfDefine>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/default_vhost.include",
    "content": "# ServerAdmin: Your address, where problems with the server should be\n# e-mailed.  This address appears on some server-generated pages, such\n# as error documents.  e.g. admin@your-domain.com\nServerAdmin root@localhost\n\n# DocumentRoot: The directory out of which you will serve your\n# documents. By default, all requests are taken from this directory, but\n# symbolic links and aliases may be used to point to other locations.\n#\n# If you change this to something that isn't under /var/www then suexec\n# will no longer work.\nDocumentRoot \"/var/www/localhost/htdocs\"\n\n# This should be changed to whatever you set DocumentRoot to.\n<Directory \"/var/www/localhost/htdocs\">\n\t# Possible values for the Options directive are \"None\", \"All\",\n\t# or any combination of:\n\t#   Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews\n\t#\n\t# Note that \"MultiViews\" must be named *explicitly* --- \"Options All\"\n\t# doesn't give it to you.\n\t#\n\t# The Options directive is both complicated and important.  Please see\n\t# http://httpd.apache.org/docs/2.4/mod/core.html#options\n\t# for more information.\n\tOptions Indexes FollowSymLinks\n\n\t# AllowOverride controls what directives may be placed in .htaccess files.\n\t# It can be \"All\", \"None\", or any combination of the keywords:\n\t#   Options FileInfo AuthConfig Limit\n\tAllowOverride All\n\n\t# Controls who can get stuff from this server.\n\tRequire all granted\n</Directory>\n\n<IfModule alias_module>\n\t# Redirect: Allows you to tell clients about documents that used to\n\t# exist in your server's namespace, but do not anymore. The client\n\t# will make a new request for the document at its new location.\n\t# Example:\n\t#   Redirect permanent /foo http://www.example.com/bar\n\n\t# Alias: Maps web paths into filesystem paths and is used to\n\t# access content that does not live under the DocumentRoot.\n\t# Example:\n\t#   Alias /webpath /full/filesystem/path\n\t#\n\t# If you include a trailing / on /webpath then the server will\n\t# require it to be present in the URL.  You will also likely\n\t# need to provide a <Directory> section to allow access to\n\t# the filesystem path.\n\n\t# ScriptAlias: This controls which directories contain server scripts.\n\t# ScriptAliases are essentially the same as Aliases, except that\n\t# documents in the target directory are treated as applications and\n\t# run by the server when requested rather than as documents sent to the\n\t# client.  The same rules about trailing \"/\" apply to ScriptAlias\n\t# directives as to Alias.\n\tScriptAlias /cgi-bin/ \"/var/www/localhost/cgi-bin/\"\n</IfModule>\n\n# \"/var/www/localhost/cgi-bin\" should be changed to whatever your ScriptAliased\n# CGI directory exists, if you have that configured.\n<Directory \"/var/www/localhost/cgi-bin\">\n\tAllowOverride None\n\tOptions None\n\tRequire all granted\n</Directory>\n\n# vim: ts=4 filetype=apache\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/apache2/vhosts.d/gentoo.example.com.conf",
    "content": "<VirtualHost *:80>\n\tServerName gentoo.example.com\n\tServerAdmin webmaster@localhost\n\tDocumentRoot /var/www/html\n\tErrorLog ${APACHE_LOG_DIR}/error.log\n\tCustomLog ${APACHE_LOG_DIR}/access.log combined\n</VirtualHost>\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/conf.d/apache2",
    "content": "# /etc/conf.d/apache2: config file for /etc/init.d/apache2\n\n# When you install a module it is easy to activate or deactivate the modules\n# and other features of apache using the APACHE2_OPTS line. Every module should\n# install a configuration in /etc/apache2/modules.d. In that file will have an\n# <IfDefine NNN> directive where NNN is the option to enable that module.\n#\n# Here are the options available in the default configuration:\n#\n#  AUTH_DIGEST  Enables mod_auth_digest\n#  AUTHNZ_LDAP  Enables authentication through mod_ldap (available if USE=ldap)\n#  CACHE        Enables mod_cache\n#  DAV          Enables mod_dav\n#  ERRORDOCS    Enables default error documents for many languages.\n#  INFO         Enables mod_info, a useful module for debugging\n#  LANGUAGE     Enables content-negotiation based on language and charset.\n#  LDAP         Enables mod_ldap (available if USE=ldap)\n#  MANUAL       Enables /manual/ to be the apache manual (available if USE=docs)\n#  MEM_CACHE    Enables default configuration mod_mem_cache\n#  PROXY        Enables mod_proxy\n#  SSL          Enables SSL (available if USE=ssl)\n#  STATUS       Enabled mod_status, a useful module for statistics\n#  SUEXEC       Enables running CGI scripts (in USERDIR) through suexec.\n#  USERDIR      Enables /~username mapping to /home/username/public_html\n#\n#\n# The following two options provide the default virtual host for the HTTP and\n# HTTPS protocol. YOU NEED TO ENABLE AT LEAST ONE OF THEM, otherwise apache\n# will not listen for incoming connections on the appropriate port.\n#\n#  DEFAULT_VHOST      Enables name-based virtual hosts, with the default\n#                     virtual host being in /var/www/localhost/htdocs\n#  SSL_DEFAULT_VHOST  Enables default vhost for SSL (you should enable this\n#                     when you enable SSL)\n#\nAPACHE2_OPTS=\"-D DEFAULT_VHOST -D INFO -D SSL -D SSL_DEFAULT_VHOST -D LANGUAGE\"\n\n# Extended options for advanced uses of Apache ONLY\n# You don't need to edit these unless you are doing crazy Apache stuff\n# As not having them set correctly, or feeding in an incorrect configuration\n# via them will result in Apache failing to start\n# YOU HAVE BEEN WARNED.\n\n# PID file\n#PIDFILE=/var/run/apache2.pid\n\n# timeout for startup/shutdown checks\n#TIMEOUT=10\n\n# ServerRoot setting\n#SERVERROOT=/usr/lib64/apache2\n\n# Configuration file location\n# - If this does NOT start with a '/', then it is treated relative to\n# $SERVERROOT by Apache\n#CONFIGFILE=/etc/apache2/httpd.conf\n\n# Location to log startup errors to\n# They are normally dumped to your terminal.\n#STARTUPERRORLOG=\"/var/log/apache2/startuperror.log\"\n\n# A command that outputs a formatted text version of the HTML at the URL\n# of the command line. Designed for lynx, however other programs may work.\n#LYNX=\"lynx -dump\"\n\n# The URL to your server's mod_status status page.\n# Required for status and fullstatus\n#STATUSURL=\"http://localhost/server-status\"\n\n# Method to use when reloading the server\n# Valid options are 'restart' and 'graceful'\n# See http://httpd.apache.org/docs/2.2/stopping.html for information on\n# what they do and how they differ.\n#RELOAD_TYPE=\"graceful\"\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/testdata/gentoo_apache/apache/sites",
    "content": "vhosts.d/gentoo.example.com.conf, gentoo.example.com\nvhosts.d/00_default_vhost.conf, localhost\nvhosts.d/00_default_ssl_vhost.conf, localhost\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tests/util.py",
    "content": "\"\"\"Common utilities for certbot_apache.\"\"\"\nimport shutil\nfrom typing import Optional\nimport unittest\nfrom unittest import mock\n\nimport augeas\nimport josepy as jose\n\nfrom certbot import util\nfrom certbot.compat import os\nfrom certbot.plugins import common\nfrom certbot.tests import util as test_util\nfrom certbot_apache._internal import configurator\nfrom certbot_apache._internal import entrypoint\nfrom certbot_apache._internal import obj\n\n\nclass ApacheTest(unittest.TestCase):\n\n    def setUp(self, test_dir: str = \"debian_apache_2_4/multiple_vhosts\",\n              config_root: str = \"debian_apache_2_4/multiple_vhosts/apache2\",\n              vhost_root: str = \"debian_apache_2_4/multiple_vhosts/apache2/sites-available\"\n              ) -> None:\n        # pylint: disable=arguments-differ\n        self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(\n            test_dir=test_dir,\n            pkg=__package__)\n\n        self.config_path = os.path.join(self.temp_dir, config_root)\n        self.vhost_path = os.path.join(self.temp_dir, vhost_root)\n\n        self.rsa512jwk = jose.jwk.JWKRSA.load(test_util.load_vector(\n            \"rsa512_key.pem\"))\n\n        self.config = get_apache_configurator(self.config_path, vhost_root,\n                                              self.config_dir, self.work_dir)\n\n        # Make sure all vhosts in sites-enabled are symlinks (Python packaging\n        # does not preserve symlinks)\n        sites_enabled = os.path.join(self.config_path, \"sites-enabled\")\n        if not os.path.exists(sites_enabled):\n            return\n\n        for vhost_basename in os.listdir(sites_enabled):\n            # Keep the one non-symlink test vhost in place\n            if vhost_basename == \"non-symlink.conf\":\n                continue\n            vhost = os.path.join(sites_enabled, vhost_basename)\n            if not os.path.islink(vhost):  # pragma: no cover\n                os.remove(vhost)\n                target = os.path.join(\n                    os.path.pardir, \"sites-available\", vhost_basename)\n                os.symlink(target, vhost)\n\n    def tearDown(self) -> None:\n        util._release_locks()\n        shutil.rmtree(self.temp_dir)\n        shutil.rmtree(self.config_dir)\n        shutil.rmtree(self.work_dir)\n\n\nclass ParserTest(ApacheTest):\n\n    def setUp(self, test_dir: str = \"debian_apache_2_4/multiple_vhosts\",\n              config_root: str = \"debian_apache_2_4/multiple_vhosts/apache2\",\n              vhost_root: str = \"debian_apache_2_4/multiple_vhosts/apache2/sites-available\"\n              ) -> None:\n        super().setUp(test_dir, config_root, vhost_root)\n\n        from certbot_apache._internal.parser import ApacheParser\n        self.aug = augeas.Augeas(\n            flags=augeas.Augeas.NONE | augeas.Augeas.NO_MODL_AUTOLOAD)\n        with mock.patch(\"certbot_apache._internal.parser.ApacheParser.\"\n                        \"update_runtime_variables\"):\n            self.parser = ApacheParser(\n                self.config_path, self.config, self.vhost_path)\n\n\ndef get_apache_configurator(\n        config_path: str, vhost_path: str,\n        config_dir: str, work_dir: str, version: tuple[int, int, int] = (2, 4, 7),\n        os_info: str = \"generic\",\n        conf_vhost_path: Optional[str] = None,\n        use_parsernode: bool = False,\n        openssl_version: str = \"1.1.1a\") -> configurator.ApacheConfigurator:\n    \"\"\"Create an Apache Configurator with the specified options.\n\n    :param conf: Function that returns binary paths. self.conf in Configurator\n\n    \"\"\"\n    backups = os.path.join(work_dir, \"backups\")\n    mock_le_config = mock.MagicMock(\n        apache_server_root=config_path,\n        apache_vhost_root=None,\n        apache_le_vhost_ext=\"-le-ssl.conf\",\n        apache_challenge_location=config_path,\n        apache_enmod=None,\n        backup_dir=backups,\n        config_dir=config_dir,\n        http01_port=80,\n        temp_checkpoint_dir=os.path.join(work_dir, \"temp_checkpoints\"),\n        in_progress_dir=os.path.join(backups, \"IN_PROGRESS\"),\n        work_dir=work_dir)\n\n    with mock.patch(\"certbot_apache._internal.configurator.util.run_script\"):\n        with mock.patch(\"certbot_apache._internal.configurator.util.\"\n                        \"exe_exists\") as mock_exe_exists:\n            mock_exe_exists.return_value = True\n            with mock.patch(\"certbot_apache._internal.parser.ApacheParser.\"\n                            \"update_runtime_variables\"):\n                with mock.patch(\"certbot_apache._internal.apache_util.parse_from_subprocess\") as mock_sp:\n                    mock_sp.return_value = []\n                    try:\n                        config_class = entrypoint.OVERRIDE_CLASSES[os_info]\n                    except KeyError:\n                        config_class = configurator.ApacheConfigurator\n                    config = config_class(config=mock_le_config, name=\"apache\",\n                                          version=version, use_parsernode=use_parsernode,\n                                          openssl_version=openssl_version)\n                    if not conf_vhost_path:\n                        config_class.OS_DEFAULTS.vhost_root = vhost_path\n                    else:\n                        # Custom virtualhost path was requested\n                        config.config.apache_vhost_root = conf_vhost_path\n                    config.config.apache_ctl = config_class.OS_DEFAULTS.ctl\n                    config.config.apache_bin = config_class.OS_DEFAULTS.bin\n                    config.prepare()\n    return config\n\n\ndef get_vh_truth(temp_dir: str, config_name: str) -> Optional[list[obj.VirtualHost]]:\n    \"\"\"Return the ground truth for the specified directory.\"\"\"\n    if config_name == \"debian_apache_2_4/multiple_vhosts\":\n        prefix = os.path.join(\n            temp_dir, config_name, \"apache2/sites-enabled\")\n\n        aug_pre = \"/files\" + prefix\n        vh_truth = [\n            obj.VirtualHost(\n                os.path.join(prefix, \"encryption-example.conf\"),\n                os.path.join(aug_pre, \"encryption-example.conf/Virtualhost\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"encryption-example.demo\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"default-ssl.conf\"),\n                os.path.join(aug_pre,\n                             \"default-ssl.conf/IfModule/VirtualHost\"),\n                {obj.Addr.fromstring(\"_default_:443\")}, True, True),\n            obj.VirtualHost(\n                os.path.join(prefix, \"000-default.conf\"),\n                os.path.join(aug_pre, \"000-default.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\"),\n                    obj.Addr.fromstring(\"[::]:80\")},\n                False, True, \"ip-172-30-0-17\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"certbot.conf\"),\n                os.path.join(aug_pre, \"certbot.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, False, True,\n                \"certbot.demo\", aliases={\"www.certbot.demo\"}),\n            obj.VirtualHost(\n                os.path.join(prefix, \"mod_macro-example.conf\"),\n                os.path.join(aug_pre,\n                             \"mod_macro-example.conf/Macro/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, False, True,\n                modmacro=True),\n            obj.VirtualHost(\n                os.path.join(prefix, \"default-ssl-port-only.conf\"),\n                os.path.join(aug_pre, (\"default-ssl-port-only.conf/\"\n                                       \"IfModule/VirtualHost\")),\n                {obj.Addr.fromstring(\"_default_:443\")}, True, True),\n            obj.VirtualHost(\n                os.path.join(prefix, \"wildcard.conf\"),\n                os.path.join(aug_pre, \"wildcard.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, False, True,\n                \"ip-172-30-0-17\", aliases={\"*.blue.purple.com\"}),\n            obj.VirtualHost(\n                os.path.join(prefix, \"ocsp-ssl.conf\"),\n                os.path.join(aug_pre, \"ocsp-ssl.conf/IfModule/VirtualHost\"),\n                {obj.Addr.fromstring(\"10.2.3.4:443\")}, True, True,\n                \"ocspvhost.com\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"non-symlink.conf\"),\n                os.path.join(aug_pre, \"non-symlink.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, False, True,\n                \"nonsym.link\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"default-ssl-port-only.conf\"),\n                os.path.join(aug_pre,\n                             \"default-ssl-port-only.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, True, True, \"\"),\n            obj.VirtualHost(\n                os.path.join(temp_dir, config_name,\n                             \"apache2/apache2.conf\"),\n                \"/files\" + os.path.join(temp_dir, config_name,\n                                        \"apache2/apache2.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")}, False, True,\n                \"vhost.in.rootconf\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"duplicatehttp.conf\"),\n                os.path.join(aug_pre, \"duplicatehttp.conf/VirtualHost\"),\n                {obj.Addr.fromstring(\"10.2.3.4:80\")}, False, True,\n                \"duplicate.example.com\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"duplicatehttps.conf\"),\n                os.path.join(aug_pre, \"duplicatehttps.conf/IfModule/VirtualHost\"),\n                {obj.Addr.fromstring(\"10.2.3.4:443\")}, True, True,\n                \"duplicate.example.com\")]\n        return vh_truth\n    if config_name == \"debian_apache_2_4/multi_vhosts\":\n        prefix = os.path.join(\n            temp_dir, config_name, \"apache2/sites-available\")\n        aug_pre = \"/files\" + prefix\n        vh_truth = [\n            obj.VirtualHost(\n                os.path.join(prefix, \"default.conf\"),\n                os.path.join(aug_pre, \"default.conf/VirtualHost[1]\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"ip-172-30-0-17\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"default.conf\"),\n                os.path.join(aug_pre, \"default.conf/VirtualHost[2]\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"banana.vomit.com\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"multi-vhost.conf\"),\n                os.path.join(aug_pre, \"multi-vhost.conf/VirtualHost[1]\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"1.multi.vhost.tld\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"multi-vhost.conf\"),\n                os.path.join(aug_pre, \"multi-vhost.conf/IfModule/VirtualHost\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"2.multi.vhost.tld\"),\n            obj.VirtualHost(\n                os.path.join(prefix, \"multi-vhost.conf\"),\n                os.path.join(aug_pre, \"multi-vhost.conf/VirtualHost[2]\"),\n                {obj.Addr.fromstring(\"*:80\")},\n                False, True, \"3.multi.vhost.tld\")]\n        return vh_truth\n    return None  # pragma: no cover\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tls_configs/current-options-ssl-apache.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n\nSSLEngine on\n\n# Intermediate configuration, tweak to your needs\nSSLProtocol             all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1\nSSLOpenSSLConfCmd       Curves X25519:prime256v1:secp384r1\nSSLCipherSuite          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:DHE-RSA-CHACHA20-POLY1305\nSSLHonorCipherOrder     off\nSSLSessionTickets       off\n\nSSLOptions +StrictRequire\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/_internal/tls_configs/old-options-ssl-apache.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n#\n# This file is installed when apache < 2.4.11 or compiled against openssl < 1.0.2l. Since these\n# are deprecated, it is no longer receiving updates. To get the latest configuration\n# version, update apache.\n\nSSLEngine on\n\n# Intermediate configuration, tweak to your needs\nSSLProtocol             all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1\nSSLCipherSuite          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\nSSLHonorCipherOrder     off\n\nSSLOptions +StrictRequire\n"
  },
  {
    "path": "certbot-apache/src/certbot_apache/py.typed",
    "content": ""
  },
  {
    "path": "certbot-ci/MANIFEST.in",
    "content": "recursive-include src/certbot_integration_tests/assets *\ninclude src/certbot_integration_tests/py.typed\ninclude src/snap_integration_tests/py.typed\n"
  },
  {
    "path": "certbot-ci/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-ci\"\ndynamic = [\"version\"]\ndescription = \"Certbot continuous integration framework\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n]\ndependencies = [\n    \"coverage\",\n    \"cryptography\",\n    \"pytest\",\n    \"pytest-cov\",\n    # This version is needed for \"worker\" attributes we currently use like\n    # \"workerinput\". See https://github.com/pytest-dev/pytest-xdist/pull/268.\n    \"pytest-xdist>=1.22.1\",\n    \"python-dateutil>=2.9.0\", # https://github.com/dateutil/dateutil/issues/1314\n    # This dependency needs to be added using environment markers to avoid its\n    # installation on Linux.\n    \"pywin32>=300 ; sys_platform == \\\"win32\\\"\",\n    \"pyyaml\",\n    # requests unvendored its dependencies in version 2.16.0 and this code relies on that for\n    # calling `urllib3.disable_warnings`.\n    \"requests>=2.25.1\",\n    \"types-python-dateutil\",\n]\n\n[project.scripts]\ncertbot_test = \"certbot_integration_tests.utils.certbot_call:main\"\nrun_acme_server = \"certbot_integration_tests.utils.acme_server:main\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-ci/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\nsetup(\n    version=version,\n)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/.coveragerc",
    "content": "[run]\n# Avoid false warnings because certbot packages are not installed in the thread that executes\n# the coverage: indeed, certbot is launched as a CLI from a subprocess.\ndisable_warnings = module-not-imported,no-data-collected\nomit = **/*_test.py,**/tests/*,**/dns_common*,**/certbot_nginx/_internal/parser_obj.py\npatch = subprocess\nparallel = True\n\n[report]\n# Exclude unit tests in coverage during integration tests.\nomit = **/*_test.py,**/tests/*,**/dns_common*,**/certbot_nginx/_internal/parser_obj.py\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/__init__.py",
    "content": "\"\"\"Package certbot_integration_test is for tests that require a live acme ca server instance\"\"\"\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/bind-config/conf/named.conf",
    "content": "options {\n  directory \"/var/cache/bind\";\n\n  // Running inside Docker. Bind address on Docker host is 127.0.0.1.\n  listen-on { any; };\n  listen-on-v6 { any; };\n\n  // We are allowing BIND to service recursive queries, but only in an extremely limimited sense\n  // where it is entirely disconnected from public DNS:\n  // - Iterative queries are disabled. Only forwarding to a non-existent forwarder.\n  // - The only recursive answers we can get (that will not be a SERVFAIL) will come from the\n  //   RPZ \"mock-recursion\" zone. Effectively this means we are mocking out the entirety of\n  //   public DNS.\n  allow-recursion { any; };       // BIND will only answer using RPZ if recursion is enabled\n  forwarders { 192.0.2.254; };    // Nobody is listening, this is TEST-NET-1\n  forward only;                   // Do NOT perform iterative queries from the root zone\n  dnssec-validation no;           // Do not bother fetching the root DNSKEY set (performance)\n  response-policy {               // All recursive queries will be served from here.\n    zone \"mock-recursion\"\n    log yes;\n  } recursive-only no             // Allow RPZs to affect authoritative zones too.\n    qname-wait-recurse no         // No real recursion.\n    nsip-wait-recurse no;         // No real recursion.\n\n  allow-transfer { none; };\n  allow-update { none; };\n};\n\nkey \"default-key.\" {\n  algorithm hmac-sha512;\n  secret \"91CgOwzihr0nAVEHKFXJPQCbuBBbBI19Ks5VAweUXgbF40NWTD83naeg3c5y2MPdEiFRXnRLJxL6M+AfHCGLNw==\";\n};\n\nzone \"mock-recursion\" {\n  type primary;\n  file \"/var/lib/bind/rpz.mock-recursion\";\n  allow-query {\n    none;\n  };\n};\n\nzone \"example.com.\" {\n  type primary;\n  file \"/var/lib/bind/db.example.com\";\n  journal \"/var/cache/bind/db.example.com.jnl\";\n\n  update-policy {\n    grant default-key zonesub TXT;\n  };\n};\n\nzone \"sub.example.com.\" {\n  type primary;\n  file \"/var/lib/bind/db.sub.example.com\";\n  journal \"/var/cache/bind/db.sub.example.com.jnl\";\n\n  update-policy {\n    grant default-key zonesub TXT;\n  };\n};\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/bind-config/rfc2136-credentials-default.ini.tpl",
    "content": "# Target DNS server\ndns_rfc2136_server = {server_address}\n# Target DNS port\ndns_rfc2136_port = {server_port}\n# TSIG key name\ndns_rfc2136_name = default-key.\n# TSIG key secret\ndns_rfc2136_secret = 91CgOwzihr0nAVEHKFXJPQCbuBBbBI19Ks5VAweUXgbF40NWTD83naeg3c5y2MPdEiFRXnRLJxL6M+AfHCGLNw==\n# TSIG key algorithm\ndns_rfc2136_algorithm = HMAC-SHA512\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/bind-config/zones/db.example.com",
    "content": "$ORIGIN example.com.\n$TTL 3600\nexample.com.  IN  SOA   ns1.example.com. admin.example.com. ( 2020091025 7200 3600 1209600 3600 )\n\nexample.com.  IN  NS    ns1\nexample.com.  IN  NS    ns2\n\nns1           IN  A     192.0.2.2\nns2           IN  A     192.0.2.3\n\n@         IN  A     192.0.2.1\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/bind-config/zones/db.sub.example.com",
    "content": "$ORIGIN sub.example.com.\n$TTL 3600\nsub.example.com.  IN  SOA   ns1.example.com. admin.example.com. ( 2020091025 7200 3600 1209600 3600 )\n\nsub.example.com.  IN  NS    ns1\nsub.example.com.  IN  NS    ns2\n\nns1           IN  A     192.0.2.2\nns2           IN  A     192.0.2.3\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/bind-config/zones/rpz.mock-recursion",
    "content": "$TTL 3600\n\n@        SOA ns1.example.test. dummy.example.test. 1 12h 15m 3w 2h\n         NS  ns1.example.test.\n\n_acme-challenge.aliased.example   IN     CNAME   _acme-challenge.example.com.\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFlTCCA32gAwIBAgIUR3wbM8qFE68f8NxfciHhUjR1GeUwDQYJKoZIhvcNAQEL\nBQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbmdpbngud3RmMCAX\nDTE5MDQxODIwMDUwM1oYDzIyOTMwMTMwMjAwNTAzWjBZMQswCQYDVQQGEwJBVTET\nMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ\ndHkgTHRkMRIwEAYDVQQDDAluZ2lueC53dGYwggIiMA0GCSqGSIb3DQEBAQUAA4IC\nDwAwggIKAoICAQC/W+yxYE0PWJOS4df71Yx596fDjW03I9JZuu9kfP7mneMgy+OC\nHyRm0TEhl6FPUp9tD9YeEHloUZNjHEOg/qrnbEOspv3Ha3RFinzrzkMwbzEPR3Xf\n0go+aVsWelDhapFl8fccw4tWwijVZQquhBsWOUnPenS3Txe96kEv2NNJlJ0qFUa+\nrOTruzRzOzlbgKv5WRb4+BxxWonHLkAQ5IT87GBlsCerVIyPD+BnZveZGl6e9oMH\nZlZvUT6aWRnzFWjAnQGiJpVIw7l9r4EW0jq1z7wqb37FrqrFbtWrOfUZVE7AlqXH\naKIR82/xwkcZfFk3sCAM0IcZc8B2SDLi4zNZtDivW6qQgTC/3z5yf1hnJ+j00dtE\nX5qYlgXRaM2raOn31lxcerk5pjgagQ7Zj+v3YZS0QnenrgyXJcdnXLDj+cIARzx4\nQHtoO0nyP0RJqxvwX/H98513JTkeqFBc/Bx11UWYsUv20Qoo9IAuz0VDARu6rquu\nk9anv56yvxo77qZ8r80l3z8eMyDA+UjuSD2p1Za09RAHfva7o8rMUqULHNQ4pfFH\nJIUozHoinAg/9lBC/W80fcbILks+Sdi6E9WQ0n8PLl7oFLx9prEDCycKuC0z76J/\nShb6R6sWr1YtzUFUc5EH2g9pMriaqT8uGO4CMOeRemXahrdT/H+Xg5m4TQIDAQAB\no1MwUTAdBgNVHQ4EFgQU46gJeu9ZOfTQ6c4vfbWbSLUpEMowHwYDVR0jBBgwFoAU\n46gJeu9ZOfTQ6c4vfbWbSLUpEMowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B\nAQsFAAOCAgEAcnfkXDUTsEGs0MleegkGbRCVy72a3U7tv1KVTLB8qLPc3tpPJJoT\nD4PbOuw9+yIE+HetZTZooOpaZoorLQdiwAEjlQ44RVuXSHSARQ8KW9ZZeiWN/Qvl\nIp4xJ/cHxcKTFKSc/99o8M+kmPKEXF9SUMfKPc5jXarNxCsnA3VriYqJ+CnYEox2\nduNUEe3A9Y2d8ZxjmscBqlcXpk1kFwsCRT5UYVoUYwyjYznLkO5A+GJ0ZnMyRMQp\nobUiB34hUrNgyOaBvizk+pNh9EV4rEBPRQwhy4vDMco4AjQcwLWQAQ9G4GSt/E+Q\n62XdVDa6CAuOvBCudDPki7kEqNLbj1tMY1K/gsbgb6TYA/xTOVulAnqm4OEZ2svJ\n0Jqw3BzMfRTaxsNU6jxm8WehVL15GjoJUzfs7Te+l7Vm/QNc1Dv2pmEhVfBibwMa\nYxUZ8ClQtQ1lsOpne97Og0p/Cm93kKELNBLTjzXtpXGGPPYisAyNwe0Hadq8SiOd\npXeNwXa5vHOXHv8xBENzBvFJ3TRN2GmMlHBp/eOfVUx/huNSpcnh2gO3fn5EbMj7\n43IaR133JW5yWbneYAMJOEAMdEB5EthRmEDtLVA7kLqLc/ywFTQ4VbS2b+PsOr5O\n501nzt0OTMMEz+UafvGXj5OPJBhe26RtnYXzVwwLfto/F5udM5zglWo=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/hook.py",
    "content": "#!/usr/bin/env python\n\"\"\"A Certbot hook for probing.\"\"\"\nimport os\nimport sys\n\nhook_script_type = os.path.basename(os.path.dirname(sys.argv[1]))\nif hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ\n                                     or 'RENEWED_LINEAGE' not in os.environ):\n    sys.stderr.write('Environment variables not properly set!\\n')\n    sys.exit(1)\n\nprint(hook_script_type)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQC/W+yxYE0PWJOS\n4df71Yx596fDjW03I9JZuu9kfP7mneMgy+OCHyRm0TEhl6FPUp9tD9YeEHloUZNj\nHEOg/qrnbEOspv3Ha3RFinzrzkMwbzEPR3Xf0go+aVsWelDhapFl8fccw4tWwijV\nZQquhBsWOUnPenS3Txe96kEv2NNJlJ0qFUa+rOTruzRzOzlbgKv5WRb4+BxxWonH\nLkAQ5IT87GBlsCerVIyPD+BnZveZGl6e9oMHZlZvUT6aWRnzFWjAnQGiJpVIw7l9\nr4EW0jq1z7wqb37FrqrFbtWrOfUZVE7AlqXHaKIR82/xwkcZfFk3sCAM0IcZc8B2\nSDLi4zNZtDivW6qQgTC/3z5yf1hnJ+j00dtEX5qYlgXRaM2raOn31lxcerk5pjga\ngQ7Zj+v3YZS0QnenrgyXJcdnXLDj+cIARzx4QHtoO0nyP0RJqxvwX/H98513JTke\nqFBc/Bx11UWYsUv20Qoo9IAuz0VDARu6rquuk9anv56yvxo77qZ8r80l3z8eMyDA\n+UjuSD2p1Za09RAHfva7o8rMUqULHNQ4pfFHJIUozHoinAg/9lBC/W80fcbILks+\nSdi6E9WQ0n8PLl7oFLx9prEDCycKuC0z76J/Shb6R6sWr1YtzUFUc5EH2g9pMria\nqT8uGO4CMOeRemXahrdT/H+Xg5m4TQIDAQABAoICAAGGL+pxw+tdXz+KQPgmiUnn\naRSrqbUIugIw9Pst67HWjBqUxSkiKl4PSH7mAEjrdY2e1KvEodLs42mkrf04ShAx\n0pArfFX8Sx7KrZgLOonGOPPQM+YmfCJnIGybaM2C1cmkFb3K6O81+LFKbr1ZHAYf\nSrE2XnufS6cdmItTBMvPPTk6lieqpOAjy5UnYZuS+Muxo/czsrZMbFCD08rOpyiE\nkXf94TMCJ2R0UetA7LPxe9N0TzLd485bLU55azV+dCkklwC9oe7EcFPJ9BNEdWdB\nUlRcMvxMGdwct+L3QTaEb2QlTwi5kqDl+XxJeduAHA3Pf1Haz1iqjVvj01PvT1di\nCs0+ZeFBsa+BfiGDe9ONwuSQljda1CuI+vDv5bGUExulOSG1dHJ7RK9PBaXFaR/b\n/9tRBwAw1Erm7s1JIkjda5Oc46jFb3HzDaZYB1n5hUmEIrYM8HhUOGITyVT3hxDO\nAWlaV3aveQ0MmMXLptVXDgbjPGbWDGMLD9d5vUE9R7IyOLeXOmjthYlCH2rj378M\nr2PkgX2tD0A/yoEZ8XCFdtBWSVajLdL0/gkm7sKosMABBy3yrSCxbHeq5TFuTAXA\nhOdypX4NOZkA6WJU+hn3GkQyIScLqSrvGRA9kzHGoEWVZDKkB9DXg+dmTARZDWXD\nmCnHkJo6+FcbhUpXniuZAoIBAQDmE94vvdstB+HEtXxN1uNDY7H8gPc/BUonU6a9\nG5YOIbjByCfEDcXF8AUWekc6lc8DNG3ydx0dnb2ZAkxmdlsaD8GLqHGILzlSsOwR\nsez8nR4+4n9vYMfx9Qal8Ren5xEP9Z9sJcNqbKVGta1WFtQzrgYbpVXXf/Luv0xS\nYoVK8KaEACciD6XX4wmajrAXPPQgThvqQtXuTn/AxWsUDg1DK0tw1VRUuOJuJwpw\nf6qocM9AyqUNvdeVyjFx8Slag34ZI7fmxPtHX/e6opTg3zVXab1Ow8AMICOHMRL6\nm5/+wnWa9xMoKI4kYfk/QFqeTccnLDlwi6kQM8WRfbwr9AyPAoIBAQDU60wrX6Lm\n0vIfngv1/4j/w+AGAwjvxiuJ7Q7LwQ2fGsZGOIfMK/lpBxCn543kGbQT+KQKNOjO\n+EywObftnJ6Y2+om2NoLkCnCiptsfr5WlN8pxtIPQu2iu5xXA67WpQv4Nc4769PM\nwJGVW3pmPKi6H0QjjqYAZd1NAXdN9Au14zZVh3KBWoz82kTHWKSL6Ld1UClG728W\nk/moyCFFMMGTXX/LVliQzDVLM6L5jbAOaG317qAuxZIqFJ9NLwHFW9uH/i1S6Qfp\n+lOmOfVYKu1O/qh1DUBQfuJkR1XIn2ifZEjxOsxeTmWu1LXpyoZy526JRu49pk8Z\nDdEu+w7hsdNjAoIBAD1YWsub8Y6GJXpPcX9HpnzXXiOXN1VEUcs+kJyneFD4SMzS\nU1gA3BS0tIaTv94tB28xUYdunwLAhkb/x+Mh95RxUwert+m5va0Ao1DsgeWw9tmJ\nhrTptyYaUNV5/Pa1s2Tv9rvdLcd4hHDgDAGCQL4uzk4cvVCiOuHRe8YTorqig6N6\nbvSz+2IelPbyyJzJkcXzTZoei+/oWkPJ340PWhXou0qwdrXIPgdkvXHVeGlE+t2p\nqmyJi6vSp3Bb/sy1dq+5SFVtfBpBykmnA88ZdJ2EAge4RcJ150MqoIbVa8l/i9/v\ntNnmRlAJF233+LFwx4L4VbBebIt3YlwyjDOj9J0CggEAIknKOGnsV/O8ni7bikAe\nleG7X/x5IfPt6wZMDbAHO4oaSBCufcjPH4TNv9xgU014XIb8E9C1dS8zWmXRIujH\n+aHgsWTWqGoM75FWukAm8taCob2s8lw63KwN301uiI6HwO8ZSTkPILgaOc1DhtdZ\n7K9AT+GXBhVhcBc+WUVl5WKzy05GuGIWtlmIHfo+dXGCqdfA7fV9FEu8NtwTz4qs\ngcja3aoIFTltk7C7HCkfIxLaMnK9RQr4IOK1TL63MEs8rUfXkLSKW7m+YtSOmCZB\nlSkZg9AgfVYRq0h5nhddx91kicSISN+jLGaA7Sd6Q2LVwDG2CCOSNVyuRTyVBu+W\nNQKCAQAWN6vB6oToNIoBLdOThm0HD07cNHcrnBjtaKsYsQDgqbr2m8LRCRzNRML4\njG0IAOWpuCiEGsgUPxywiI1Ufvyq7ZSNT1QQNzCR47NM3Ve6S2abrQkMIk9VJ+za\nCB9c1BH92GokoRxqswb/BiMttG2EIP8L8/pSRYEcVnaaxAkf9QOhEwj4LJPGX0mS\nt7kWIUVHPdFJ67F25dYr3mUHgyV+QJupQICkkkgY3nBOU1fS42vAugaxqH0wAP3T\n53FlpY3NuE7+kYC3FjfcBer99F1pOac3X9jxhk26w9dr2/QNA33xhDXHKYvoLUCG\nRPQylahJByU7IrtQzSCf/RE7q4v0\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/meta.json",
    "content": "{\"creation_host\": \"ec2-52-91-193-99.compute-1.amazonaws.com\", \"creation_dt\": \"2016-12-23T02:08:32Z\"}"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/private_key.json",
    "content": "{\"e\": \"AQAB\", \"d\": \"W410Wny96RO4qJ207KGQ3RSn0KAwqb93JBMHWU1yS9H3fN_2eCpFYdMLNFI9t1__nW1okeUioEfvMN_YW-G9krw97kVdZ63MfbeJCf35Onc8VZhAnk_3V8MtS26Of8ml0tTYhlQ65nuzhvHbY7aP-Uk260oDN-AbCCVhu5G4CQiMY6sdtCc8YkB6gK7SK874oWU7ogvAIPtNtEI-AXDUBYNAfoh34s1r2fE6mJSX4UYtzWB2hTUisvZdVL5JUInvxpCQFttk1cwWLFwwb6d2ERCbseeudvGJ6fkYiJ-EYxfHKOQK2kxPeOlLFMwGYQ0khDxTNajxQ1Asl43r7wgAeQ\", \"n\": \"xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw\", \"q\": \"0xdfHMMKYWHPE1UoQ10niDI7rnCM9vmPo4JpCOCYZf51KPNJgNaPCw62Q0Y-ZQfCBifypQyf291d0_2C_Rif0WMg07Y-Ypv8SpPK77vLV12GoAoAX2Xy3AJAz1gDBcyUzDtRlrzgCZja9YqIDVzMatkdPJXaBrBu5B-sXv4wGa0\", \"p\": \"7pl5xe_400Sn6PdN_F6KLWHFROVd7379WPWGHYmnvOvXx7DmrMjDsTOmhNRlrv7jPemVqMzp1FGsubGBizEMFGyCET30bUgH6ZU7Cmgv-2JKKN1FZnm1QTepZ7kjAT_qRCI6nvN6J0SIX197QOSz3hMmP7UYQXQ32QcVKdCksps\", \"kty\": \"RSA\", \"qi\": \"zG60VpLZjgR0o7dTeEP-HjbtxHUedyZLGe4FIPyWrPRl28anebkMUGzibpB8z5ohRsqHU2i4tmDq2NMvshISqkpk8t5PLiIcQgU46HQ24SCv7lunkVPKYU1n2uXVVfttrBP4c3UkjYzda1bcIVp6cJHanm_JuWI5nxy9ebVQJiw\", \"dp\": \"kRIBx0aj7Jh22x_aa9JzgypKDhzDY4W7tmX5-GWk9ioTVZgKeQ3MZiZ4XZTiimbxdchbNXn5xh0uvuzdTesxZA2he6hGwFcmcHBKqIY2fksBuhznQGpJuXCFcMpRLUZWQrzpFZIGOG_j1tEwGIG1lxXfkKakK8_k0PEMfhMcwHc\", \"dq\": \"AsoSRa0GHBdQxy6e45T9ir0vMLToB_NwRHbasHVXTjG4lpvwYrVzGnBNVEI_XNJna_FnMWsjSaJ5NO3qpzGGGxw2ONX1qRPql4mwas6Od08TElZPfvM37FRTSuoc0BzN8ozuHRHN3BKbAheciKCrStYnnr9ULDZ0oKsSegbd19k\"}"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/accounts/acme-staging.api.letsencrypt.org/directory/48d6b9e8d767eccf7e4d877d6ffa81e3/regr.json",
    "content": "{\"body\": {\"agreement\": \"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf\", \"key\": {\"e\": \"AQAB\", \"kty\": \"RSA\", \"n\": \"xL5HzdhU_7P-_tphpRxpDSIL2L-aAlWt6r9EVyw53Sp-jx4fHDgnYv9HQOzNeL_IpLRCLLBItMzqnBvHUdHcS3aB6fv8HSNiHdVdC-c2rPFO8DLSGLNqi9G9WshjLDsKwc__BPNX5wHFcm8TZUJ4uZ_Ax1JCe05ePHWAf8GTr8vPaKtMpUVF55HPwpJtYvFZlH1LiVo8I_trJtHl8-pGeel3zdcaDJgNZrohZG2acTg95Ry46FE4HOslAg8Z6yECPyYLInJSDcb5yCgSqtOOp7rMVSPQFhoZRt4KDfew9lqIwNQSJoDE3bJWpwkzL1tp4clG8ExI1WnA86OjW83Vvw\"}}, \"uri\": \"https://acme-staging.api.letsencrypt.org/acme/reg/566631\", \"new_authzr_uri\": \"https://acme-staging.api.letsencrypt.org/acme/new-authz\", \"terms_of_service\": \"https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf\"}"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/cert1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF\nADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw\nMTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l\neGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco\nhsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR\nWzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM\nFvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca\nnsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny\nCj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q\nlmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE\nFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU\nC7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L\nIKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\ndGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j\nZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu\nY3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG\nCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy\neXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv\nbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp\nbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh\ndCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B\nAQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR\nomYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J\n/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH\nFgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY\nPtu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC\n4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/chain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw\nGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2\nMDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0\n8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym\noLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0\nZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN\nxDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56\ndhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9\nAgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw\nHQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0\nBggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu\nb3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu\nY3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq\nhkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF\nUGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9\nAFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp\nDQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7\nIkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf\nzWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI\nPTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w\nSVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em\n2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0\nWzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt\nn5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/fullchain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE9TCCA92gAwIBAgITAPrA8hxQOlpVRMgAm/Ib0HYdqzANBgkqhkiG9w0BAQsF\nADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw\nMTAyMDBaFw0xNzAzMjMwMTAyMDBaMCMxITAfBgNVBAMTGGEuZW5jcnlwdGlvbi1l\neGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKqz0cco\nhsCqyWPwGr79a8j+JO3HqbphLTzhoNHYF+fW8glyMyBmOMyZjc8v8E3U3KYEXuuR\nWzR+bvUXBcLOhSogIifZDNiMKEFyDNcDlG08ze9GTj2hTQyjet2ZuPWNuuJ4u5UM\nFvobaceDqITuqEqUrjCBi5CmEXswrV3l2BVSiOcPf+l+ZR81xG7qcjGfLG6YQWca\nnsYYorz/kSRtwYjAT4NaeUYNXVeH1luWTWhbed8pmKfBVfv+OEmwUyAhSE1ePfny\nCj37wo1+nqQz37IJNEpI0RNbxrE7ZCgA40QrFVqc9XevcypFi9DftVWzDNBtd97Q\nlmHuIqA9Kb3C/e8CAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE\nFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU\nC7/XcCnNRht91hnQVEB2E9AtNUowHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L\nIKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\ndGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j\nZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhhLmVu\nY3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG\nCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy\neXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv\nbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp\nbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh\ndCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B\nAQsFAAOCAQEAP04z87VVNYYHpBkCLkw3B+gTd/F0xDo7ab2HvJJAeOpZgSfoSYMR\nomYWiug9wGQqKjs4kaOGjAkW1EV3qosumOtvK7uTvoa2caXDjPYAxRiVIp08Qm0J\n/FU/FfGpUXBZW9Ne3m3nDYxOCAWAw9WmV+dUuvb7qZWQSKs7cQv3FY/NuQe0o9LH\nFgL7T0W7vc6uVGeBgcoEkX7xX4T7A9V3BqL6mgkK+L++n0EFrDXXzWWENNdWYCvY\nPtu0Ez95IyYNRgI3U1waO9QZ944Pc9OuMCZD4ifbYoMKGqSQb3sGR+B2TQ+qqCUC\n4sikdX4WRbEYKlBTcvSpCVJ7ndFTyD6lyg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw\nGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2\nMDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0\n8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym\noLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0\nZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN\nxDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56\ndhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9\nAgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw\nHQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0\nBggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu\nb3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu\nY3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq\nhkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF\nUGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9\nAFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp\nDQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7\nIkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf\nzWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI\nPTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w\nSVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em\n2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0\nWzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt\nn5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/a.encryption-example.com/privkey1.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj\n8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71\nFwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH\ng6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8\n/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN\nfp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg\nPSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY\nJsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo\nfG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc\nxEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj\nbHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D\nWB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2\nmQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu\nwAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8\nenLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO\niH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x\nnKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL\nW+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4\nq5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R\nzNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz\nh7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3\nArog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS\nxpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy\nYsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY\no+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl\nwkqKc3PndJHrR40mswUOHbs=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/cert1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF\nADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw\nMTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l\neGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR\nFAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f\ns9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim\n+144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi\nTw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl\neSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0\nXCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE\nFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU\nRJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L\nIKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\ndGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j\nZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu\nY3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG\nCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy\neXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv\nbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp\nbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh\ndCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B\nAQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u\nSDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw\nU4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT\n0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ\nomXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV\nZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/chain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw\nGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2\nMDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0\n8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym\noLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0\nZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN\nxDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56\ndhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9\nAgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw\nHQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0\nBggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu\nb3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu\nY3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq\nhkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF\nUGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9\nAFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp\nDQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7\nIkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf\nzWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI\nPTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w\nSVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em\n2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0\nWzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt\nn5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/fullchain1.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIE9TCCA92gAwIBAgITAPqBl0IgXf6F9LO/8sV1SsoA9DANBgkqhkiG9w0BAQsF\nADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0xNjEyMjMw\nMTA0MDBaFw0xNzAzMjMwMTA0MDBaMCMxITAfBgNVBAMTGGIuZW5jcnlwdGlvbi1l\neGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWA6tWR\nFAfYyOEM9HtJXK4tCd1tGF2QZrlJHEL3PJzFHonv7ZaPo6Vkrar1uLinM4AVux/f\ns9vcsbdebu54DXpj1IllzjKs3tjStHK46luMqj8gf+3yLZIIVnN4YxkItd1WBtim\n+144ku1gULsGnnHmuCefXz6qqkLzFZsElqO7NY+TL4F4m/L0lDjYsU++XgbHT9gi\nTw0jAi8SyH8Ia4IYi4ynnMuHuS11e+yOtq16kLW1RdnxrYpleu9z0DU+6Xlr1tbl\neSkyzbWelDgdsicfOxZz5pbmALXErb472TidcHHK6bsMVhR/P1zQK9Ydc+tC33d0\nXCRRgPoduN8XRfcCAwEAAaOCAiEwggIdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUE\nFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU\nRJ6J6HcpXRdRjqfyGshMEzkJy4cwHwYDVR0jBBgwFoAUwMwDRrlYIMxccnDz4S7L\nIKb1aDoweAYIKwYBBQUHAQEEbDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5z\ndGctaW50LXgxLmxldHNlbmNyeXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9j\nZXJ0LnN0Zy1pbnQteDEubGV0c2VuY3J5cHQub3JnLzAjBgNVHREEHDAaghhiLmVu\nY3J5cHRpb24tZXhhbXBsZS5jb20wgf4GA1UdIASB9jCB8zAIBgZngQwBAgEwgeYG\nCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNy\neXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENlcnRpZmljYXRlIG1heSBv\nbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFydGllcyBhbmQgb25seSBp\nbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRlIFBvbGljeSBmb3VuZCBh\ndCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0b3J5LzANBgkqhkiG9w0B\nAQsFAAOCAQEA2K8R+nSf9TmfSeUqB+ckObkf8bgyR0qKx/8fGoYGNAzKVE0KUs8u\nSDIITjbcTivEuSChycZAGQMEMZal8uT8GsFqqJUcEJUzuxbv7nvZkCSdal1PrRsw\nU4cBBuuZ/NvisEZCyjZe8mMdlhcSgThzqljF5Tcz3EWvaH9kxhqr8eL/6pYdAasT\n0HqirveIQUrf9LqEEAYGB3P6VI2kjroxUZif7dt2jvOGwJEJfHOjiC8rp0Db0hVZ\nomXSsZN6mVkbv1q0I7lgKWu1RHfNAefado3TJZHe8JJ5Oxrl3f2hxi3SzuPGgfXV\nZdKb0zjDXhgumrp0F2eT9zltTIUr8alYcg==\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw\nGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2\nMDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw\nggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0\n8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym\noLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0\nZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN\nxDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56\ndhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9\nAgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw\nHQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0\nBggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu\nb3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu\nY3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq\nhkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF\nUGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9\nAFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp\nDQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7\nIkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf\nzWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI\nPTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w\nSVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em\n2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0\nWzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt\nn5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/b.encryption-example.com/privkey1.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh\nDPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3\nXm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt\nYFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv\nEsh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21\nnpQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6\nHbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX\nKaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l\n/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r\nm4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK\ntiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj\nxDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n\n9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf\nY4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN\ncDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT\n+kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P\nJhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F\nNghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/\nIGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv\nJG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc\nXqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY\nEXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT\nxmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe\nvJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj\nt6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar\nBwhs4S84KEJFT0LAC3YeRQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/README",
    "content": "This directory contains your keys and certificates.\n\n`privkey.pem`  : the private key for your certificate.\n`fullchain.pem`: the certificate file used in most server software.\n`chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.\n`cert.pem`     : will break many server configurations, and should not be used\n                 without reading further documentation (see link below).\n\nWARNING: DO NOT MOVE OR RENAME THESE FILES!\n         Certbot expects these files to remain in this location in order\n         to function properly!\n\nWe recommend not moving these files. For more information, see the Certbot\nUser Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIIBvrEnbPRYu8wDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjEwNzQw\nWhcNMjUxMDEyMjEwNzQwWjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs\nZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARjMhuW0ENPPC33PjB5XsYU\nCRw640kPQENIDatcTJaENZIZdqKd6rI6jc+lpbmXot7Zi52clJlSJS+V6oDAt2Lh\no4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUj7Kd3ENqxlPf8B2bIGhsjydX\nmPswHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE\nJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww\nGoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQCl\nk0JXsa8y7fg41WWMDhw60bPW77O0FtOmTcnhdI5daYNemQVk+Q5EMaBLQ/oGjgXd\n9QXFzXH1PL904YEnSLt+iTpXn++7rQSNzQsdYqw0neWk4f5pEBiN+WORpb6mwobV\nifMtBOkNEHvrJ2Pkci9U1lLwtKD/DSew6QtJU5DSkmH1XdGuMJiubygEIvELtvgq\ncP9S368ZvPmPGmKaJQXBiuaR8MTjY/Bkr79aXQMjKbf+mpn7h0POCcePk1DY/rm6\nDa+X16lf0hHyQhSUa7Vgyim6rK1/hlw+Z00i+sQCKD9Ih7kXuuGqfSDC33cfO8Tj\no/MXO8lcxkrem5zU5QWP\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/chain.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx\nMDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy\nNmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq\nmJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB\nqKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5\nCIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH\nnm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY\nMRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx\nPzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE\nbIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT\nMA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq\nuCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P\nfJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV\nEdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW\nfcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG\n9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/fullchain.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC2zCCAcOgAwIBAgIILlmGtZhUFEwwDQYJKoZIhvcNAQELBQAwKDEmMCQGA1UE\nAxMdUGViYmxlIEludGVybWVkaWF0ZSBDQSAxMjZjNGIwHhcNMjAxMDEyMjA1MDM0\nWhcNMjUxMDEyMjA1MDM0WjAjMSEwHwYDVQQDExhjLmVuY3J5cHRpb24tZXhhbXBs\nZS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARHEzR8JPWrEmpmgM+F2bk5\n9mT0u6CjzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/\no4HYMIHVMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB\nBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU1CsVL+bPnzaxxQ5jUENmQJIO\nlKwwHwYDVR0jBBgwFoAUEiGxlkRsi+VvcogH5dVD3h1laAcwMQYIKwYBBQUHAQEE\nJTAjMCEGCCsGAQUFBzABhhVodHRwOi8vMTI3LjAuMC4xOjQwMDIwIwYDVR0RBBww\nGoIYYy5lbmNyeXB0aW9uLWV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBn\n2D8loC7pfk28JYpFLr5lmFKJWWmtLGlpsWDj61fVjtTfGKLziJz+MM6il4Y3hIz5\n58qiFK0ue0M63dIBJ33N+XxSEXon4Q0gy/zRWfH9jtPJ3FwfjkU/RT9PAUClYi0G\nptNWnTmgQkNzousbcAtRNXuuShH3856vhUnwkX+xM+cbIDi1JVmFjcGrEEQJ0rUF\nmv2ZTyfbWbUs3v4rReETi2NVzr1Ql6J+ByNcMvHODzFy3t0L6yelAw2ca1I+c9HU\n+Z0tnp/ykR7eXNuVLivok8UBf5OC413lh8ZO5g+Bgzh/LdtkUuavg1MYtEX0H6mX\n9U7y3nVI8WEbPGf+HDeu\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIDUDCCAjigAwIBAgIIbi787yVrcMAwDQYJKoZIhvcNAQELBQAwIDEeMBwGA1UE\nAxMVUGViYmxlIFJvb3QgQ0EgMGM1MjI1MCAXDTIwMTAxMjIwMjI0NloYDzIwNTAx\nMDEyMjEyMjQ2WjAoMSYwJAYDVQQDEx1QZWJibGUgSW50ZXJtZWRpYXRlIENBIDEy\nNmM0YjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGeVk1BMJraeqRq\nmJ2+hgso8VOAv2s2CVxUJjIVcn7f2adE8NyTsSQ1brlsnKCUYUw7yLTQH0izLQRB\nqKVIDFkUqo5/FuTJ2QlfA2EwBL8J7s/7L7vj3L0DiVpwgxPSyFEwdl/Y5y7ofsX5\nCIhCFcaMAmTIuKLiSfCJjGwkbEMuolm+lO8Mikxxc/JtDVUC479ugU7PU9O09bMH\nnm+sD6Bgd+KMoPkCCCoeShJS9X3Ziq9HGc7Z6nhM/zirFARt2XkonEdAZ8br01zY\nMRiY9txhlWQ7mUkOtzOSoEuYJNoUbvMUf0+tNzto26WRyF7dJmh7lTBsYrvAwUTx\nPzNyst0CAwEAAaOBgzCBgDAOBgNVHQ8BAf8EBAMCAoQwHQYDVR0lBBYwFAYIKwYB\nBQUHAwEGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBIhsZZE\nbIvlb3KIB+XVQ94dZWgHMB8GA1UdIwQYMBaAFOaKTaXg37vKgRt7d79YOjAoAtJT\nMA0GCSqGSIb3DQEBCwUAA4IBAQAU2mZii7PH2pkw2lNM0QqPbcW/UYyvFoUeM8Aq\nuCtsI2s+oxCJTqzfLsA0N8NY4nHLQ5wAlNJfJekngni8hbmJTKU4JFTMe7kLQO8P\nfJbk0pTzhhHVQw7CVwB6Pwq3u2m/JV+d6xDIDc+AVkuEl19ZJU0rTWyooClfFLZV\nEdZmEiUtA3PGlxoYwYhoGHYlhFxsoFONhCsBEdN7k7FKtFGVxN7oc5SKmKp0YZTW\nfcrEtrdNThATO4ymhCC2zh33NI/MT1O74fpaAc2k6LcTl57MKiLfTYX4LTL6v9JG\n9tlNqjFVRRmzEbtXTPcCb+w9g1VqoOGok7mGXYLTYtShCuvE\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/archive/c.encryption-example.com/privkey.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgNgefv2dad4U1VYEi\n0WkdHuqywi5QXAe30OwNTTGjhbihRANCAARHEzR8JPWrEmpmgM+F2bk59mT0u6Cj\nzmJG0QpbaqprLiG5NGpW84VQ5TFCrmC4KxYfigCfMhfHRNfFYvNUK3V/\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/csr/0000_csr-certbot.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIIChjCCAW4CAQIwFzEVMBMGA1UEAwwMaXMuaXNub3Qub3JnMIIBIjANBgkqhkiG\n9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7nsHOCTvvQlRYXpI5xE7AggqTVmM8lGi18Y2\ngVlr3WYAS7higHRJjWroAmZ2Bx9IRfHOxwhVWm/hlc/u4w0IYlRnArg6suXrgtn+\n6Ea0WDUCiKEiKvQqD0kaI936hpydU/dY70UZnpKSyi0kiCrLzCkIaXS8HJdLOIXB\nQ4FMVqjppYjUejMgrabthq1QTqU0S4MxwS1oj67VqaAkedGWxFgFQ2kIFV0/WL13\nXs0SCTYyN96KK1Q2CF63HoN79zc+TVslg32DDU5UF7sVVvlkoHcl0OgR9l4jfou5\nHwmatMjXPI+0bWVxmw6iC6tbK7Dx+ytYIodhEOL52Youzy/lLwIDAQABoCowKAYJ\nKoZIhvcNAQkOMRswGTAXBgNVHREEEDAOggxpcy5pc25vdC5vcmcwDQYJKoZIhvcN\nAQELBQADggEBAAJsLiylvGq64wxVt8EBeXRB4ycBzC5J/pyOWMP9oexW1o3XPhCC\n+0tIQVGk7wJMe3+WiPMVsn4pGOUGDaPvfC7ijlvipzaYyLEfnr+J7pukhYbzNHmu\nXL5lbTJ0hTCfqUjmi1yE4M/v2eX5yNaEHsZExZ1NbtwutE/Tx5iSqt7kxbIoFqmF\n7Tne2JHjt945+/l9yvqaIcEFOmblS0OxY9EjxgJdhKCKbhD/ZoYaVVisc52h/2/M\njtzvzZr1rZCvFnuQxGDco5vYe3u7uJ9tQHLCMpoIorT3kX3yTdgnWxst6XBVUY/P\nQ6O18obG4ALoP/ESzvTauQIwFVGfal/jqyI=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/csr/0001_csr-certbot.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICgDCCAWgCAQIwFDESMBAGA1UEAwwJaXNub3Qub3JnMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAsEAy7rdPsYFFt9VsK9NZy+W9nbsYGmvIaMSyJkEg\nXe2P0MmnWG/hn6F1bLPm85uS5oQsOWDpwVz31tKhoWhUDbRzPWP5Ur2NnHY92Whz\n5tP4ir4vEEDuB9etQ8+wZ7+3z9q1VhPcgDdYyouQVB0QejJ1yUBiVPr289bW//ln\nkj9DFxn4oufoJ4ELSZSZgWFM92EGKMMy1zD2bJH87mI0Gs0pIOEo+QMJ8TvVEbau\n+aFaTANslqRAF5LaWcrPgvHor7cK5w/4bVBZCmY2QYKqlYwZiRPpwg3Ii6B9Q8kz\nrDkGSDjwsazca4api57cza13XkRl7KvyZbwTwlFBud+ydwIDAQABoCcwJQYJKoZI\nhvcNAQkOMRgwFjAUBgNVHREEDTALgglpc25vdC5vcmcwDQYJKoZIhvcNAQELBQAD\nggEBAB3vniZw2ML6E9jrMY8DtQjPDDNr1BqOGzyOaJipqpGZSRvhTA44DAAjdFpS\n5BLrnXniPIZGG4/6WorLTEDBnlFcLinUg7GDT2DpauQa+4PLxFi13hE1TuSVOp9A\n08YXhzALvZxMIjQ/tVhAp0+PkGEWU2wI0SmDvUUTJqMwSJYgXkf/vBS34/koKywV\ngPDod5AbLuhYgKiQYwDZ0dd69leT0REmizuaHtA6tW3mBgewSKotwqY3fHmhHV8o\nYLSVhImz4jJjK3LjmcdXuBxqE0z+p6n/+lSGG8RR/E8pix4OAkVAP6nyt/loW1BX\nZzWOuSHozGN5UJSL248vLFWrsV8=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/csr/0002_csr-certbot.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYS5lbmNyeXB0aW9uLWV4YW1wbGUuY29t\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqrPRxyiGwKrJY/Aavv1r\nyP4k7cepumEtPOGg0dgX59byCXIzIGY4zJmNzy/wTdTcpgRe65FbNH5u9RcFws6F\nKiAiJ9kM2IwoQXIM1wOUbTzN70ZOPaFNDKN63Zm49Y264ni7lQwW+htpx4OohO6o\nSpSuMIGLkKYRezCtXeXYFVKI5w9/6X5lHzXEbupyMZ8sbphBZxqexhiivP+RJG3B\niMBPg1p5Rg1dV4fWW5ZNaFt53ymYp8FV+/44SbBTICFITV49+fIKPfvCjX6epDPf\nsgk0SkjRE1vGsTtkKADjRCsVWpz1d69zKkWL0N+1VbMM0G133tCWYe4ioD0pvcL9\n7wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhhLmVuY3J5cHRp\nb24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAJyKJHdUwR9BOKYJarUy\nP8mqu6UBUt8faSu6o3EUeDHbnUgxGAVwB5TJV0+JwIjPFQFRofHE8CFhUvi0W0YJ\nBsGVqblnJzz80NkUX9uwjBAGKaDxXqXDOctkQSAOJxM/rvD2uJLmlokibDDm7mnS\nDX8SUVAPgORDGlVTGATjvmA3YeH05gHRFgRDWFP5DOZs99fx4957HrXhsIxew98s\nFelupgswnouyq3crrgcjY0qo3Pc5gjUcuwaT2cjtvzi93f/ImDt6f1sdSSJB00wk\n34lbs/Z+0G8bH1dqYIZzkwNgq7rolhDYh3WRgTlfkgkV7FlkQGm8qn5uoQvaXaaS\nShM=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/csr/0003_csr-certbot.pem",
    "content": "-----BEGIN CERTIFICATE REQUEST-----\nMIICnjCCAYYCAQIwIzEhMB8GA1UEAwwYYi5lbmNyeXB0aW9uLWV4YW1wbGUuY29t\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtYDq1ZEUB9jI4Qz0e0lc\nri0J3W0YXZBmuUkcQvc8nMUeie/tlo+jpWStqvW4uKczgBW7H9+z29yxt15u7ngN\nemPUiWXOMqze2NK0crjqW4yqPyB/7fItkghWc3hjGQi13VYG2Kb7XjiS7WBQuwae\ncea4J59fPqqqQvMVmwSWo7s1j5MvgXib8vSUONixT75eBsdP2CJPDSMCLxLIfwhr\nghiLjKecy4e5LXV77I62rXqQtbVF2fGtimV673PQNT7peWvW1uV5KTLNtZ6UOB2y\nJx87FnPmluYAtcStvjvZOJ1wccrpuwxWFH8/XNAr1h1z60Lfd3RcJFGA+h243xdF\n9wIDAQABoDYwNAYJKoZIhvcNAQkOMScwJTAjBgNVHREEHDAaghhiLmVuY3J5cHRp\nb24tZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACDw8/zjFaIdp4aqyrzT\nfzaqAnoXZt3+0JDPLANy3DLCJmK2TQMyItg/Oid5NEQ45UluXv811IMCcONyVmrD\n19W3XErhTJOJMgpjg4GLBRRFhLm+uTIcbv/xEeUgOYbslsqwi2gHECe1Vsj/Ahbo\nQXXqcDg1cXe6VTQhX+Nw5q30t/oCmkJWcUVHBON2nbOujRz1+z6AjVl1dM+CYDRq\nbsKn7m3biYS7lx7/ApIuhJQsghcmccCtWrH5GsOUsJUgiANv5u+QZgGaajkCRKYV\nfD/u8qTPfKb/+lTxtDrfFOGH+mbZKbKf2/ibneYcql8fFQWiapbudI2cMk8yDxA9\n2Tw=\n-----END CERTIFICATE REQUEST-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/keys/0000_key-certbot.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDuewc4JO+9CVFh\nekjnETsCCCpNWYzyUaLXxjaBWWvdZgBLuGKAdEmNaugCZnYHH0hF8c7HCFVab+GV\nz+7jDQhiVGcCuDqy5euC2f7oRrRYNQKIoSIq9CoPSRoj3fqGnJ1T91jvRRmekpLK\nLSSIKsvMKQhpdLwcl0s4hcFDgUxWqOmliNR6MyCtpu2GrVBOpTRLgzHBLWiPrtWp\noCR50ZbEWAVDaQgVXT9YvXdezRIJNjI33oorVDYIXrceg3v3Nz5NWyWDfYMNTlQX\nuxVW+WSgdyXQ6BH2XiN+i7kfCZq0yNc8j7RtZXGbDqILq1srsPH7K1gih2EQ4vnZ\nii7PL+UvAgMBAAECggEBAIX9jeLXrfNSRu0z3b4mCjdsCwiGphCIGayOa5VlfptY\nchYZNQ7jR2gzhsPCedIqm1rhL8LYRcyYS/D2cUwUyH8m2PHIPQLC9/3/KZ+sCiv9\nLL1De4USxobsFcnNMLNtT2Ab+1YERw63X85EauAu226MJ3PI6OBPiS3qyNl6zj9p\ndo9SyzsNFEGtDk+ndWf3keoHBKLge4DP1lA3Jt42wSUxVv9U5SLvFpMQm8PqbqrK\n4ofXcgxMFIJHDDGXsoDI7LOOsV6ncBVlui0ELM/QWBb5x1605VxqEDRL+h/wMp5Y\nJIc6HbgcERmtHmyFlHHNtjAXxeulJVDJQDekd/irJ5ECgYEA/WQJ4LwkkA/Yhf2W\nWYJtD8LuwzRnvGs3R+rgx3+hOeO4TFZD5fzObZVRSwWQO2jbOtBJOaRLUsUngcJQ\nDXr/FGf1rnGhLmNeLE+jN9FS73wBhEXViFZ/fzhVibGbc7u45Y5REykZj8HtUHP5\nhBKR2Nx94WDiv1MBgcKrRk6yI50CgYEA8O+vWcMzEdPtonHl8UgTa8/c5g/RBBvS\nplB8mVsmM/E5CNwnetZM32cg7dC7yNaZzn3qF6w+LdE2vw3j5VbqvuVUvsRgvYcJ\n3kMbHsbsxkRw+HVWZGgEtWNzuYQUL0xN+xzIZDWkbtuaihqYAy4voYNAM08BTNcE\nPOQEMIGxcDsCgYEAg+TLo3grS/WDjhM2bHcQT9D2uRMRIClqx/uBbzaG9HwNFWcd\nxpv102KSwwstTU9CNfXu95sGPhozez5qrumj1rpaTqgE7wF4JnZ5jfdeRRv2KiSz\nhlkH2m+3TontUauYDZ0rpF6TWJnn7iW/7jhARHJY77SfslkBgsqSnnEeFp0CgYEA\n7FsFVvZRzCRt01UOsPL28mWYmyxa7D/rFvKQONUdFgmG3PUz2aIPCX2e5Q1GmlBD\n1Djbg1uaJ9I8dZJHxbzNTnWk+/ujt2mYuax1F20n65xKgsKA/MC6FcM5TH2QW5Hs\nUfI7d2rUI1hVMzPBeiU93qDmQy825E1uP9mjbn5cNe8CgYAsBpJgS1LkDruyWmjG\nZTzdHGciA1O3gUArLQmyUfJlPS3Hgwn7wnBBihtGZDHmjJ7734+PQ9ioCnO9Pb+K\n8Cp29vJ85lka7o7I48OeScLmczgEUYOPCrbkkKJdKaG6gn5CKpRBVYDlhbWjVZ51\n4uda/BQ1hqHh8WmxK6x21qC9JQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/keys/0001_key-certbot.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwQDLut0+xgUW3\n1Wwr01nL5b2duxgaa8hoxLImQSBd7Y/QyadYb+GfoXVss+bzm5LmhCw5YOnBXPfW\n0qGhaFQNtHM9Y/lSvY2cdj3ZaHPm0/iKvi8QQO4H161Dz7Bnv7fP2rVWE9yAN1jK\ni5BUHRB6MnXJQGJU+vbz1tb/+WeSP0MXGfii5+gngQtJlJmBYUz3YQYowzLXMPZs\nkfzuYjQazSkg4Sj5AwnxO9URtq75oVpMA2yWpEAXktpZys+C8eivtwrnD/htUFkK\nZjZBgqqVjBmJE+nCDciLoH1DyTOsOQZIOPCxrNxrhqmLntzNrXdeRGXsq/JlvBPC\nUUG537J3AgMBAAECggEBAJoZR27X72GvORmmDFG1FInlcIf8EPLo0exoLaqsvnPh\nRSCzbxEvoQFE1boZARB1MVdCsLfqN/bMJhU5TAAni3YAE9HVGyRwfuQRrbnsTYnA\nQ0prRhLb8kIBHIhxijbrtPaSroF4FA42VfehVqt0TffJLpqrJE5QrqI7cPeVRCzk\nlaLyi2rjZBhN6l1OxFSIOrEDlcowlPUMORbmNDMbq/dLu5riVO/kP2x70K1IiANI\nNZzVhMwkktYj3Ku2altRLcyRrC3Bs46w2QF6wiC88/LMapt79um65P/SgcCgyOYE\noxJywZwMnyw8ut1Y+KS8B7AdzqWmj7Q9wr0xbW6+4eECgYEA6sNrMGZVRUFRPAcr\nm3y5fkM/WJ8tAkT3hI2/noljv3k8iameTy/B/y3p+aM8/6Oa/gdO/SWtfKPednkf\nCIh/3J5tJ1yvK7wHEEU6r6qxVKr2FLCMfSXoGx+E+r9qPF8WdV+55beVgO86UqA5\ny9a6DhNA+Xt4jDJc+rbpga0pj60CgYEAwDHDV0lR7jVT6iiU6VhAu1gM/SBVqXE/\nVSfmGihgaO4pJ9OgfqusKbraNONc+oBub7B4T3sSnF/I0mSUclD6brmG99OWLIg8\nL6/ed+bLPRO0iTvKRLbyBLom1Totfh/X6iQ2Zci40vLIS7kbYDban16ca+iSm+0B\n41RV4q6+vzMCgYBLoxiW6HGStZ+xonHHT+EHsCzppac/su64c18IeiV8HFiH1fFe\ne/mZ+LYIqzJM/u5B6CLn5srFfJqBOzbnbescLqLmarM5eQQhltx4mps1tzs/oT4y\nWBM3IembTC6zMsOun1/qhkKR3wHAe0UDyrP5MvTdLI3DRbq1QFdtY1gfpQKBgEgg\npNGWJ5RBGSvwbOohf7GPOtioEN3VLVJ09crtSjk23+Uda8b+AE9s20Ur6pHsLwXl\ncVFKu9JJtCEZNAiu0T1KjRdmpZ4yxnuTAed3iuByC7fQ43jkO3GAtuAgxD/oDWzG\niE+sg4hPKtIYNujlzSgwJn3su1CfIq1A0jaPI/C3AoGAHGTBtsXdR1goFvcxwA+n\nl2bAs/InoED5nj26a//JuONgtGlm//QKCxIgjjktpeZm8sfsaYeR+rwIUODWRX/e\nLUF85a70SaH+FZRXBRS2d/zaNxO4F37nE5fwO+VAurSb7El7yOyCepK22iSHMYdl\nxak78KZKv3HXW5yrfA+dc2Y=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/keys/0002_key-certbot.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqs9HHKIbAqslj\n8Bq+/WvI/iTtx6m6YS084aDR2Bfn1vIJcjMgZjjMmY3PL/BN1NymBF7rkVs0fm71\nFwXCzoUqICIn2QzYjChBcgzXA5RtPM3vRk49oU0Mo3rdmbj1jbrieLuVDBb6G2nH\ng6iE7qhKlK4wgYuQphF7MK1d5dgVUojnD3/pfmUfNcRu6nIxnyxumEFnGp7GGKK8\n/5EkbcGIwE+DWnlGDV1Xh9Zblk1oW3nfKZinwVX7/jhJsFMgIUhNXj358go9+8KN\nfp6kM9+yCTRKSNETW8axO2QoAONEKxVanPV3r3MqRYvQ37VVswzQbXfe0JZh7iKg\nPSm9wv3vAgMBAAECggEAattP6Wz8FaWTlgTaqU44Z8R314VSQULNr7vKETJFnLKY\nJsOfL5vt2F4TQGxQ8Ffcm+xGgw4l2tF+odv8ljrzbzBYUTt06CWsmXNMiFhMVKlo\nfG01Uy0i71Ny+T9eYhCLuXM8cYv04jHA4M0Q8831+WHjPKgLdswOS2BoVkwoHQfc\nxEo40D0sPynd+KRukhgR+5AjwMdaNOV7S8c5iuQYIaZ1Xe5AyfiQkMV4LdbobMDj\nbHzGxdeC5GRVOHnMBYrRotgSt4+bsQGeoV9yWY0WAVvnoDfRBRdWK8yRVhuJY1+D\nWB6sPJ5cOg7Ijclubo9b+EaUkddvP0aCA3FepqNwcQKBgQDR0hz9OSom2fBjLaR2\nmQe3LqnotwPCuMmXuKndGIwJz9KgelBaRNUcvDtnzSzQVZ3h9/YFJKUkoVPVCoAu\nwAF9aBeDGs+LdHerBK8fI87PXwCV0OlZLQfUw1/82dpO/dyYXVeGorrO6FE/Oxb8\nenLerMW0Ocp/MhEgM5lFRUJM1wKBgQDQRauI9QuMoBnl516pOs+7EPRvTwe4oBpO\niH2U7ryJ/YQTgsx25sDWqQBouEnv3j83wnVh9kApkS8UXFd4ZwuizIFCMlgrxw4x\nnKDsd1TZOLUO2FNi09YWPUnzxzQBOjBeekEIDKUQCLOKttTrjRHgGld3tmVtHWtL\nW+OvNIdcqQKBgCMpqjAJr3W5Wl7UnFY/yRo62MCmQxwT6bzidp0V6woN6Qd52BN4\nq5pYNUBtExCK+J2Q94rfHEnqO2ldjCPJi7ZfhmkzSgrd5twjOdHnJ1Z7Xla9Hw4R\nzNksMN7oB3zrcFecdPmcNeBM8Ki/F1gSkUOeArf0Y2ozkskpvIruU3EbAoGBAMVz\nh7CMQKrNjj/8Hi5qZ05+QH7Wegd7IfWaSRTNUUmxY2nr81Q2aFQaXRzquo4CMgT3\nArog76t4zR2MfhDUAKATKehMOnMmgDpgt9/3MiXOMTkltchX9PuYl2faT19qfzjS\nxpyPAF43IaA8vZejYnMIBiyka3wLDBGhyDXuovYhAoGAB/AZnOM/4SQuIdtzmBSy\nYsHpXcNgRPqvfauCus3e5I6H4wmi+nqF/jyt0oyDBDKZki67CpStwu5Eo7tcLLnY\no+VfJ9co8jUfVxRh0NlZwomF1t/8yAm/deWoV9sX9Yj71ft/eomCifNseeeg31Kl\nwkqKc3PndJHrR40mswUOHbs=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/keys/0003_key-certbot.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC1gOrVkRQH2Mjh\nDPR7SVyuLQndbRhdkGa5SRxC9zycxR6J7+2Wj6OlZK2q9bi4pzOAFbsf37Pb3LG3\nXm7ueA16Y9SJZc4yrN7Y0rRyuOpbjKo/IH/t8i2SCFZzeGMZCLXdVgbYpvteOJLt\nYFC7Bp5x5rgnn18+qqpC8xWbBJajuzWPky+BeJvy9JQ42LFPvl4Gx0/YIk8NIwIv\nEsh/CGuCGIuMp5zLh7ktdXvsjratepC1tUXZ8a2KZXrvc9A1Pul5a9bW5XkpMs21\nnpQ4HbInHzsWc+aW5gC1xK2+O9k4nXBxyum7DFYUfz9c0CvWHXPrQt93dFwkUYD6\nHbjfF0X3AgMBAAECggEAYjEnWnjNTF10d4Qps5UBxdzpzFfb6apYWH78AiJ9MRbX\nKaqab2ywDKdF6Qpcb9FM5EtdW6YLSLPBlUFKZEqgiAkAD4D7J6EsQkLjinkNmI+l\n/tbXPuRY0PsfwgJsIjv7H44N0CGuNdAHdNI5eqTfDSHTmOP4hA+SYvvdQWsfD94r\nm4ocr2YfL4BmEh3hujb8NjVD8csSnFlpeVibtJ1rWiv1otLaEuVmcN49n0rIj0IK\ntiCIdqqIscVZ+P3fFfr/E3oL2nhBqxRnzqoK/HNTpI4JJAbRGP51nVr0QhZYpIuj\nxDM+zeuIt0lMYOzoE+JD0612Q66mokBPHZAd5MuEwQKBgQDbdJUQfcw/9zHuWm4n\n9+wYgMN1QhfJNEr21LUjbe551YapkU389mBJJIlmjH5p67PaMRuJ1o6uRJWv40hf\nY4xy6iViLc1FExIvRVznxMCIyCELtuvYMiCJtaekFKunziniw8yg5SwSZJY3GlXN\ncDAwIcgb9PPU5rBEip8g0DIp1wKBgQDTunF3OtEoVqdsPSmw5y1767YTCsm3dnVT\n+kwp7ZrX3TJ3Xd6EVPWUBP1HbGD3qfsIR+Ha3Vl8OiLNC4zDoZY886U4qY5Mtn4P\nJhUN0H9zYZg2l9gFf9u8RkUoPZPXXuk+eQnlGT133PrkCloDlqP47u/fQ5dV1t6F\nNghgwfOA4QKBgHI/IRMyylBKmj3h6hL4qHqhHiA/Ri7DAHu7hIlrQ4k9ths0wAr/\nIGUzlixC29S8libzBckeX60tm1ez1QuDwaxZZRjVi1V4djERxSoLbchHl5yHoAQv\nJG1Mmnd7I1n6pCefkzn31JfGscUB+sU2sH9+NrUHMqEVb5JfMDRe7p6FAoGAcYGc\nXqz7gEKkUtSfSyVELxD4dVDtPxuUXsbqmfe1cVA2Q+Pg7NSXKxlZpzak7WEFITVY\nEXtlA8Iu8fnlJuOzpU2BH9VWYi3beseRtew2x2Zksa/JsXkQFekeHiqU3XsWU9WT\nxmw3ldCz+BjMlOvnUAbYNbsIoI4mkQecijKwFkECgYA2zafSyWCW5zAronUBQDEe\nvJumAJ77TwpYzzvH2ic6siWimdePxQ6TgdM3s1FgpdkbaXgKzS5MbZbD0Uyg3MEj\nt6ZT7GSWq39wLDJVDYJ5ClAi8mv9WNs8X8rJ0CkdiPZgHC77OwBELthGn2p9ncar\nBwhs4S84KEJFT0LAC3YeRQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/live/a.encryption-example.com/README",
    "content": "This directory contains your keys and certificates.\n\n`privkey.pem`  : the private key for your certificate.\n`fullchain.pem`: the certificate file used in most server software.\n`chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.\n`cert.pem`     : will break many server configurations, and should not be used\n                 without reading further documentation (see link below).\n\nWe recommend not moving these files. For more information, see the Certbot\nUser Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/live/b.encryption-example.com/README",
    "content": "This directory contains your keys and certificates.\n\n`privkey.pem`  : the private key for your certificate.\n`fullchain.pem`: the certificate file used in most server software.\n`chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.\n`cert.pem`     : will break many server configurations, and should not be used\n                 without reading further documentation (see link below).\n\nWe recommend not moving these files. For more information, see the Certbot\nUser Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/live/c.encryption-example.com/README",
    "content": "This directory contains your keys and certificates.\n\n`privkey.pem`  : the private key for your certificate.\n`fullchain.pem`: the certificate file used in most server software.\n`chain.pem`    : used for OCSP stapling in Nginx >=1.3.7.\n`cert.pem`     : will break many server configurations, and should not be used\n                 without reading further documentation (see link below).\n\nWARNING: DO NOT MOVE OR RENAME THESE FILES!\n         Certbot expects these files to remain in this location in order\n         to function properly!\n\nWe recommend not moving these files. For more information, see the Certbot\nUser Guide at https://certbot.eff.org/docs/using.html#where-are-my-certificates.\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/options-ssl-apache.conf",
    "content": "# Baseline setting to Include for SSL sites\n\nSSLEngine on\n\n# Intermediate configuration, tweak to your needs\nSSLProtocol             all -SSLv2 -SSLv3\nSSLCipherSuite          ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA\nSSLHonorCipherOrder     on\nSSLCompression          off\n\nSSLOptions +StrictRequire\n\n# Add vhost name to log entries:\nLogFormat \"%h %l %u %t \\\"%r\\\" %>s %b \\\"%{Referer}i\\\" \\\"%{User-agent}i\\\"\" vhost_combined\nLogFormat \"%v %h %l %u %t \\\"%r\\\" %>s %b\" vhost_common\n\n#CustomLog /var/log/apache2/access.log vhost_combined\n#LogLevel warn\n#ErrorLog /var/log/apache2/error.log\n\n# Always ensure Cookies have \"Secure\" set (JAH 2012/1)\n#Header edit Set-Cookie (?i)^(.*)(;\\s*secure)??((\\s*;)?(.*)) \"$1; Secure$3$4\"\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/renewal/a.encryption-example.com.conf",
    "content": "# renew_before_expiry = 30 days\nversion = 0.10.0.dev0\narchive_dir = sample-config/archive/a.encryption-example.com\ncert = sample-config/live/a.encryption-example.com/cert.pem\nprivkey = sample-config/live/a.encryption-example.com/privkey.pem\nchain = sample-config/live/a.encryption-example.com/chain.pem\nfullchain = sample-config/live/a.encryption-example.com/fullchain.pem\n\n# Options used in the renewal process\n[renewalparams]\nauthenticator = apache\ninstaller = apache\naccount = 48d6b9e8d767eccf7e4d877d6ffa81e3\nconfig_dir = sample-config\nserver = https://acme-staging.api.letsencrypt.org/directory\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/renewal/b.encryption-example.com.conf",
    "content": "# renew_before_expiry = 30 days\nversion = 0.10.0.dev0\narchive_dir = sample-config/archive/b.encryption-example.com\ncert = sample-config/live/b.encryption-example.com/cert.pem\nprivkey = sample-config/live/b.encryption-example.com/privkey.pem\nchain = sample-config/live/b.encryption-example.com/chain.pem\nfullchain = sample-config/live/b.encryption-example.com/fullchain.pem\n\n# Options used in the renewal process\n[renewalparams]\nauthenticator = apache\ninstaller = apache\naccount = 48d6b9e8d767eccf7e4d877d6ffa81e3\nconfig_dir = sample-config\nserver = https://acme-staging.api.letsencrypt.org/directory\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/assets/sample-config/renewal/c.encryption-example.com.conf",
    "content": "# renew_before_expiry = 30 days\nversion = 1.10.0.dev0\narchive_dir = sample-config/archive/c.encryption-example.com\ncert = sample-config/live/c.encryption-example.com/cert.pem\nprivkey = sample-config/live/c.encryption-example.com/privkey.pem\nchain = sample-config/live/c.encryption-example.com/chain.pem\nfullchain = sample-config/live/c.encryption-example.com/fullchain.pem\n\n# Options used in the renewal process\n[renewalparams]\nauthenticator = apache\ninstaller = apache\naccount = 48d6b9e8d767eccf7e4d877d6ffa81e3\nkey_type = ecdsa\nconfig_dir = sample-config-ec\nelliptic_curve = secp256r1\nmanual_public_ip_logging_ok = True\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/certbot_tests/__init__.py",
    "content": "# pylint: disable=missing-module-docstring\nimport pytest\n\n# Custom assertions defined in the following package need to be registered to be properly\n# displayed in a pytest report when they are failing.\npytest.register_assert_rewrite('certbot_integration_tests.certbot_tests.assertions')\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/certbot_tests/assertions.py",
    "content": "\"\"\"This module contains advanced assertions for the certbot integration tests.\"\"\"\nimport os\nfrom typing import Optional\n\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey\nfrom cryptography.hazmat.primitives.serialization import load_pem_private_key\n\ntry:\n    import grp\n    POSIX_MODE = True\nexcept ImportError:\n    import ntsecuritycon\n    import win32security\n    POSIX_MODE = False\n\nEVERYBODY_SID = 'S-1-1-0'\nSYSTEM_SID = 'S-1-5-18'\nADMINS_SID = 'S-1-5-32-544'\n\n\ndef assert_elliptic_key(key_path: str, curve: type[EllipticCurve]) -> None:\n    \"\"\"\n    Asserts that the key at the given path is an EC key using the given curve.\n    :param key_path: path to key\n    :param EllipticCurve curve: name of the expected elliptic curve\n    \"\"\"\n    with open(key_path, 'rb') as file:\n        privkey1 = file.read()\n\n    key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())\n\n    assert isinstance(key, EllipticCurvePrivateKey), f\"should be an EC key but was {type(key)}\"\n    assert isinstance(key.curve, curve), f\"should have curve {curve} but was {key.curve}\"\n\n\ndef assert_rsa_key(key_path: str, key_size: Optional[int] = None) -> None:\n    \"\"\"\n    Asserts that the key at the given path is an RSA key.\n    :param str key_path: path to key\n    :param int key_size: if provided, assert that the RSA key is of this size\n    \"\"\"\n    with open(key_path, 'rb') as file:\n        privkey1 = file.read()\n\n    key = load_pem_private_key(data=privkey1, password=None, backend=default_backend())\n    assert isinstance(key, RSAPrivateKey)\n    if key_size:\n        assert key_size == key.key_size\n\n\ndef assert_hook_execution(probe_path: str, probe_content: str) -> None:\n    \"\"\"\n    Assert that a certbot hook has been executed\n    :param str probe_path: path to the file that received the hook output\n    :param str probe_content: content expected when the hook is executed\n    \"\"\"\n    encoding = 'utf-8' if POSIX_MODE else 'utf-16'\n    with open(probe_path, 'rt', encoding=encoding) as file:\n        data = file.read()\n\n    lines = [line.strip() for line in data.splitlines()]\n    assert probe_content in lines\n\n\ndef assert_saved_lineage_option(config_dir: str, lineage: str,\n                                option: str, value: Optional[str] = None) -> None:\n    \"\"\"\n    Assert that the option of a lineage has been saved.\n    :param str config_dir: location of the certbot configuration\n    :param str lineage: lineage domain name\n    :param str option: the option key\n    :param value: if desired, the expected option value\n    \"\"\"\n    with open(os.path.join(config_dir, 'renewal', f'{lineage}.conf')) as file_h:\n        assert f\"{option} = {value if value else ''}\" in file_h.read()\n\n\ndef assert_saved_deploy_hook(config_dir: str, lineage: str) -> None:\n    \"\"\"\n    Assert that the deploy hook configuration of a lineage has been saved.\n    :param str config_dir: location of the certbot configuration\n    :param str lineage: lineage domain name\n    \"\"\"\n    assert_saved_lineage_option(config_dir, lineage, 'renew_hook')\n\n\ndef assert_cert_count_for_lineage(config_dir: str, lineage: str, count: int) -> None:\n    \"\"\"\n    Assert the number of certificates generated for a lineage.\n    :param str config_dir: location of the certbot configuration\n    :param str lineage: lineage domain name\n    :param int count: number of expected certificates\n    \"\"\"\n    archive_dir = os.path.join(config_dir, 'archive')\n    lineage_dir = os.path.join(archive_dir, lineage)\n    certs = [file for file in os.listdir(lineage_dir) if file.startswith('cert')]\n    assert len(certs) == count\n\n\ndef assert_equals_group_permissions(file1: str, file2: str) -> None:\n    \"\"\"\n    Assert that two files have the same permissions for group owner.\n    :param str file1: first file path to compare\n    :param str file2: second file path to compare\n    \"\"\"\n    # On Windows there is no group, so this assertion does nothing on this platform\n    if POSIX_MODE:\n        mode_file1 = os.stat(file1).st_mode & 0o070\n        mode_file2 = os.stat(file2).st_mode & 0o070\n\n        assert mode_file1 == mode_file2\n\n\ndef assert_equals_world_read_permissions(file1: str, file2: str) -> None:\n    \"\"\"\n    Assert that two files have the same read permissions for everyone.\n    :param str file1: first file path to compare\n    :param str file2: second file path to compare\n    \"\"\"\n    if POSIX_MODE:\n        mode_file1 = os.stat(file1).st_mode & 0o004\n        mode_file2 = os.stat(file2).st_mode & 0o004\n    else:\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID) # pylint: disable=used-before-assignment\n\n        security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION)\n        dacl1 = security1.GetSecurityDescriptorDacl()\n\n        mode_file1 = dacl1.GetEffectiveRightsFromAcl({\n            'TrusteeForm': win32security.TRUSTEE_IS_SID,\n            'TrusteeType': win32security.TRUSTEE_IS_USER,\n            'Identifier': everybody,\n        })\n        mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ # pylint: disable=used-before-assignment\n\n        security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION)\n        dacl2 = security2.GetSecurityDescriptorDacl()\n\n        mode_file2 = dacl2.GetEffectiveRightsFromAcl({\n            'TrusteeForm': win32security.TRUSTEE_IS_SID,\n            'TrusteeType': win32security.TRUSTEE_IS_USER,\n            'Identifier': everybody,\n        })\n        mode_file2 = mode_file2 & ntsecuritycon.FILE_GENERIC_READ\n\n    assert mode_file1 == mode_file2\n\n\ndef assert_equals_group_owner(file1: str, file2: str) -> None:\n    \"\"\"\n    Assert that two files have the same group owner.\n    :param str file1: first file path to compare\n    :param str file2: second file path to compare\n    \"\"\"\n    # On Windows there is no group, so this assertion does nothing on this platform\n    if POSIX_MODE:\n        group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0]\n        group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0]\n\n        assert group_owner_file1 == group_owner_file2\n\n\ndef assert_world_no_permissions(file: str) -> None:\n    \"\"\"\n    Assert that the given file is not world-readable.\n    :param str file: path of the file to check\n    \"\"\"\n    if POSIX_MODE:\n        mode_file_all = os.stat(file).st_mode & 0o007\n        assert mode_file_all == 0\n    else:\n        security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)\n        dacl = security.GetSecurityDescriptorDacl()\n        mode = dacl.GetEffectiveRightsFromAcl({\n            'TrusteeForm': win32security.TRUSTEE_IS_SID,\n            'TrusteeType': win32security.TRUSTEE_IS_USER,\n            'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),\n        })\n\n        assert not mode\n\n\ndef assert_world_read_permissions(file: str) -> None:\n    \"\"\"\n    Assert that the given file is world-readable, but not world-writable or world-executable.\n    :param str file: path of the file to check\n    \"\"\"\n    if POSIX_MODE:\n        mode_file_all = os.stat(file).st_mode & 0o007\n        assert mode_file_all == 4\n    else:\n        security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)\n        dacl = security.GetSecurityDescriptorDacl()\n        mode = dacl.GetEffectiveRightsFromAcl({\n            'TrusteeForm': win32security.TRUSTEE_IS_SID,\n            'TrusteeType': win32security.TRUSTEE_IS_USER,\n            'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),\n        })\n\n        assert not mode & ntsecuritycon.FILE_GENERIC_WRITE\n        assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE\n        assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/certbot_tests/context.py",
    "content": "\"\"\"Module to handle the context of integration tests.\"\"\"\nimport os\nimport shutil\nimport sys\nimport tempfile\nfrom typing import Iterable\n\nimport pytest\n\nfrom certbot_integration_tests.utils import certbot_call\n\n\nclass IntegrationTestsContext:\n    \"\"\"General fixture describing a certbot integration tests context\"\"\"\n    def __init__(self, request: pytest.FixtureRequest) -> None:\n        self.request = request\n\n        if hasattr(request.config, 'workerinput'):  # Worker node\n            self.worker_id = request.config.workerinput['workerid']\n            acme_xdist = request.config.workerinput['acme_xdist']\n        else:  # Primary node\n            self.worker_id = 'primary'\n            acme_xdist = request.config.acme_xdist  # type: ignore[attr-defined]\n\n        self.directory_url = acme_xdist['directory_url']\n        self.https_port = acme_xdist['https_port'][self.worker_id]\n        self.local_ip = str(acme_xdist['local_ip'][self.worker_id])\n        self.http_01_port = acme_xdist['http_port'][self.worker_id]\n        self.other_port = acme_xdist['other_port'][self.worker_id]\n        # Challtestsrv REST API, that exposes entrypoints to register new DNS entries,\n        # is listening on challtestsrv_url.\n        self.challtestsrv_url = acme_xdist['challtestsrv_url']\n\n        self.workspace = tempfile.mkdtemp()\n        self.config_dir = os.path.join(self.workspace, 'conf')\n\n        probe = tempfile.mkstemp(dir=self.workspace)\n        os.close(probe[0])\n        self.hook_probe = probe[1]\n\n        self.manual_dns_auth_hook = (\n            '{0} -c \"import os; import requests; import json; '\n            \"assert not os.environ.get('CERTBOT_DOMAIN').startswith('fail'); \"\n            \"data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN')),\"\n            \"'value':os.environ.get('CERTBOT_VALIDATION')}}; \"\n            \"request = requests.post('{1}/set-txt', data=json.dumps(data)); \"\n            \"request.raise_for_status(); \"\n            '\"'\n        ).format(sys.executable, self.challtestsrv_url)\n        self.manual_dns_auth_hook_allow_fail = (\n            '{0} -c \"import os; import requests; import json; '\n            \"data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN')),\"\n            \"'value':os.environ.get('CERTBOT_VALIDATION')}}; \"\n            \"request = requests.post('{1}/set-txt', data=json.dumps(data)); \"\n            \"request.raise_for_status(); \"\n            '\"'\n        ).format(sys.executable, self.challtestsrv_url)\n        self.manual_dns_cleanup_hook = (\n            '{0} -c \"import os; import requests; import json; '\n            \"data = {{'host':'_acme-challenge.{{0}}.'.format(os.environ.get('CERTBOT_DOMAIN'))}}; \"\n            \"request = requests.post('{1}/clear-txt', data=json.dumps(data)); \"\n            \"request.raise_for_status(); \"\n            '\"'\n        ).format(sys.executable, self.challtestsrv_url)\n\n    def cleanup(self) -> None:\n        \"\"\"Cleanup the integration test context.\"\"\"\n        shutil.rmtree(self.workspace)\n\n    def certbot(self, args: Iterable[str], force_renew: bool = True) -> tuple[str, str]:\n        \"\"\"\n        Execute certbot with given args, not renewing certificates by default.\n        :param args: args to pass to certbot\n        :param bool force_renew: set to False to not renew by default\n        :return: stdout and stderr from certbot execution\n        :rtype: Tuple of `str`\n        \"\"\"\n        command = ['--authenticator', 'standalone', '--installer', 'null']\n        command.extend(args)\n        return certbot_call.certbot_test(\n            command, self.directory_url, self.http_01_port, self.https_port,\n            self.config_dir, self.workspace, force_renew=force_renew)\n\n    def get_domain(self, subdomain: str = 'le') -> str:\n        \"\"\"\n        Generate a certificate domain name suitable for distributed certbot integration tests.\n        This is a requirement to let the distribution know how to redirect the challenge check\n        from the ACME server to the relevant pytest-xdist worker. This resolution is done by\n        appending the pytest worker id to the subdomain, using this pattern:\n        {subdomain}.{worker_id}.wtf\n        :param str subdomain: the subdomain to use in the generated domain (default 'le')\n        :return: the well-formed domain suitable for redirection on\n        :rtype: str\n        \"\"\"\n        return '{0}.{1}.wtf'.format(subdomain, self.worker_id)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/certbot_tests/test_main.py",
    "content": "\"\"\"Module executing integration tests against certbot core.\"\"\"\nimport json\nimport os\nfrom os.path import exists\nfrom os.path import join\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom typing import Generator\n\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurve\nfrom cryptography.hazmat.primitives.asymmetric.ec import SECP256R1\nfrom cryptography.hazmat.primitives.asymmetric.ec import SECP384R1\nfrom cryptography.hazmat.primitives.asymmetric.ec import SECP521R1\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography import x509\nfrom cryptography.x509 import ExtensionNotFound\nfrom cryptography.x509 import NameOID\nfrom cryptography.x509 import TLSFeatureType\nimport pytest\n\nfrom certbot_integration_tests.certbot_tests.assertions import assert_cert_count_for_lineage\nfrom certbot_integration_tests.certbot_tests.assertions import assert_elliptic_key\nfrom certbot_integration_tests.certbot_tests.assertions import assert_equals_group_owner\nfrom certbot_integration_tests.certbot_tests.assertions import assert_equals_group_permissions\nfrom certbot_integration_tests.certbot_tests.assertions import assert_equals_world_read_permissions\nfrom certbot_integration_tests.certbot_tests.assertions import assert_hook_execution\nfrom certbot_integration_tests.certbot_tests.assertions import assert_rsa_key\nfrom certbot_integration_tests.certbot_tests.assertions import assert_saved_lineage_option\nfrom certbot_integration_tests.certbot_tests.assertions import assert_saved_deploy_hook\nfrom certbot_integration_tests.certbot_tests.assertions import assert_world_no_permissions\nfrom certbot_integration_tests.certbot_tests.assertions import assert_world_read_permissions\nfrom certbot_integration_tests.certbot_tests.assertions import EVERYBODY_SID\nfrom certbot_integration_tests.certbot_tests.context import IntegrationTestsContext\nfrom certbot_integration_tests.utils import misc, constants\n\n\n@pytest.fixture(name='context')\ndef test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]:\n    \"\"\"Fixture providing the integration test context.\"\"\"\n    # Fixture request is a built-in pytest fixture describing current test request.\n    integration_test_context = IntegrationTestsContext(request)\n    try:\n        yield integration_test_context\n    finally:\n        integration_test_context.cleanup()\n\n\ndef test_basic_commands(context: IntegrationTestsContext) -> None:\n    \"\"\"Test simple commands on Certbot CLI.\"\"\"\n    # TMPDIR env variable is set to workspace for the certbot subprocess.\n    # So tempdir module will create any temporary files/dirs in workspace,\n    # and its content can be tested to check correct certbot cleanup.\n    initial_count_tmpfiles = len(os.listdir(context.workspace))\n\n    context.certbot(['--help'])\n    context.certbot(['--help', 'all'])\n    context.certbot(['--version'])\n\n    with pytest.raises(subprocess.CalledProcessError):\n        context.certbot(['--csr'])\n\n    new_count_tmpfiles = len(os.listdir(context.workspace))\n    assert initial_count_tmpfiles == new_count_tmpfiles\n\n\ndef test_hook_dirs_creation(context: IntegrationTestsContext) -> None:\n    \"\"\"Test all hooks directory are created during Certbot startup.\"\"\"\n    context.certbot(['register'])\n\n    for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):\n        assert os.path.isdir(hook_dir)\n\n\ndef test_registration_override(context: IntegrationTestsContext) -> None:\n    \"\"\"Test correct register/unregister, and registration override.\"\"\"\n    context.certbot(['register'])\n    context.certbot(['unregister'])\n    context.certbot(['register', '--email', 'ex1@domain.org,ex2@domain.org'])\n\n    context.certbot(['update_account', '--email', 'example@domain.org'])\n    stdout1, _ = context.certbot(['show_account'])\n    context.certbot(['update_account', '--email', 'ex1@domain.org,ex2@domain.org'])\n    stdout2, _ = context.certbot(['show_account'])\n\n    assert 'example@domain.org' in stdout1, \"New email should be present\"\n    assert 'example@domain.org' not in stdout2, \"Old email should not be present\"\n    assert 'ex1@domain.org, ex2@domain.org' in stdout2, \"New emails should be present\"\n\n\ndef test_prepare_plugins(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that plugins are correctly instantiated and displayed.\"\"\"\n    stdout, _ = context.certbot(['plugins', '--init', '--prepare'])\n\n    assert 'webroot' in stdout\n\n\ndef test_http_01(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the HTTP-01 challenge using standalone plugin.\"\"\"\n    certname = context.get_domain('le2')\n    context.certbot([\n        '--domains', certname, '--preferred-challenges', 'http-01', 'run',\n        '--cert-name', certname,\n        '--pre-hook', misc.echo('wtf_pre', context.hook_probe),\n        '--post-hook', misc.echo('wtf_post', context.hook_probe),\n        '--deploy-hook', misc.echo('deploy', context.hook_probe),\n    ])\n\n    assert_hook_execution(context.hook_probe, 'deploy')\n    assert_saved_deploy_hook(context.config_dir, certname)\n    assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'ecdsa')\n\n\n@pytest.mark.skipif(sys.platform == 'darwin',\n                    reason='macOS has one IPv4 loopback address by default')\ndef test_ipv4_address_standalone(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the HTTP-01 challenge with an IPv4 address using standalone authenticator.\n\n    While Pebble will offer both HTTP-01 and TLS-ALPN-01 challenges, we will\n    only select HTTP-01 because TLS-ALPN-01 is not supported by the standalone\n    authenticator.\n\n    This test relies on proxy.py being able to forward requests for, e.g. `127.0.0.2`,\n    (`local_ip`) to this test runner. That works on Linux because 127.0.0.0/8 all routes\n    to localhost. However, on macOS by default only 127.0.0.1 is routed, so this test is\n    skipped. If you want to run it, configure additional loopback addresses:\n        for n in $(seq 2 127) ; do sudo ifconfig lo0 alias \"127.0.0.${n}\" up ; done.\n    \"\"\"\n    context.certbot([\n         'certonly', '--ip-address', context.local_ip, '--standalone',\n    ])\n    assert_cert_count_for_lineage(context.config_dir, context.local_ip, 1)\n\n\ndef test_ipv6_address_standalone(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the HTTP-01 challenge with an IPv6 address using standalone authenticator.\n\n    This test relies on some tricks. Pebble is configured to do validations on port 5002.\n    Since multiple integration tests want to handle validation requests, and may run\n    concurrently, proxy.py handles HTTP traffic for port 5002 and sends it to the appropriate\n    integration test runner. However, it binds that port for IPv4 only. That is,\n    GracefulTCPServer doesn't specify `address_family = AF_INET6` when subclassing\n    socketserver.TCPServer.\n\n    For IPv4 integration tests (above), we simply assign each integration test runner a unique\n    IP address under 127.0.0.0/8, and the proxy knows how to route those IP addresses. Pebble's\n    validation connects to the proxy because all of 127.0.0.0/8 is defined to be loopback.\n\n    However, under IPv6 there is exactly one loopback address, so we can't use the same trick.\n    Instead, we ensure that this is the only test that cares about IPv6, and bind [::1]:5002\n    for IPv6 (via `--http-01-address`). When Pebble reaches out to validate `::1`, it reaches\n    this test runner rather than the proxy.\n\n    Note: This works because this is the only test case that binds [::1]:5002. If additional\n    IPv6 tests are added they could conflict. In that case we might try using the\n    @pytest.mark.xdist_group annotation along with --dist loadgroup.\n    https://pytest-xdist.readthedocs.io/en/stable/distribution.html\n    \"\"\"\n    context.certbot([\n         'certonly', '--ip-address', '::1', '--standalone',\n            '--http-01-address', '::1',\n            '--http-01-port', str(constants.DEFAULT_HTTP_01_PORT),\n    ])\n    assert_cert_count_for_lineage(context.config_dir, '::1', 1)\n\n\ndef test_manual_http_auth(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the HTTP-01 challenge using manual plugin.\"\"\"\n    with misc.create_http_server(context.http_01_port) as webroot,\\\n            misc.manual_http_hooks(webroot) as scripts:\n\n        certname = context.get_domain()\n        context.certbot([\n            'certonly', '-a', 'manual', '-d', certname,\n            '--cert-name', certname,\n            '--manual-auth-hook', scripts[0],\n            '--manual-cleanup-hook', scripts[1],\n            '--pre-hook', misc.echo('wtf_pre', context.hook_probe),\n            '--post-hook', misc.echo('wtf_post', context.hook_probe),\n            '--deploy-hook', misc.echo('deploy', context.hook_probe),\n        ])\n\n    assert_hook_execution(context.hook_probe, 'deploy')\n    assert_saved_deploy_hook(context.config_dir, certname)\n\n\ndef test_manual_dns_auth(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the DNS-01 challenge using manual plugin.\"\"\"\n    certname = context.get_domain('dns')\n    context.certbot([\n        '-a', 'manual', '-d', certname, '--preferred-challenges', 'dns',\n        'run', '--cert-name', certname,\n        '--manual-auth-hook', context.manual_dns_auth_hook,\n        '--manual-cleanup-hook', context.manual_dns_cleanup_hook,\n        '--pre-hook', misc.echo('wtf_pre', context.hook_probe),\n        '--post-hook', misc.echo('wtf_post', context.hook_probe),\n        '--deploy-hook', misc.echo('deploy', context.hook_probe),\n    ])\n\n    assert_hook_execution(context.hook_probe, 'deploy')\n    assert_saved_deploy_hook(context.config_dir, certname)\n\n    context.certbot(['renew', '--cert-name', certname, '--authenticator', 'manual'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n\n\ndef test_certonly(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the certonly verb on certbot.\"\"\"\n    context.certbot(['certonly', '--cert-name', 'newname', '-d', context.get_domain('newname')])\n\n    assert_cert_count_for_lineage(context.config_dir, 'newname', 1)\n\n\ndef test_certonly_webroot(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the certonly verb with webroot plugin\"\"\"\n    with misc.create_http_server(context.http_01_port) as webroot:\n        certname = context.get_domain('webroot')\n        context.certbot(['certonly', '-a', 'webroot', '--webroot-path', webroot, '-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n\n@pytest.mark.skipif(sys.platform == 'darwin',\n                    reason='macOS has one IPv4 loopback address by default')\ndef test_certonly_webroot_ipv4(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the HTTP-01 challenge with an IPv4 address using webroot authenticator.\n\n    This test relies on proxy.py being able to forward requests for, e.g. `127.0.0.2`,\n    (`local_ip`) to this test runner. That works on Linux because 127.0.0.0/8 all routes\n    to localhost. However, on macOS by default only 127.0.0.1 is routed, so this test is\n    skipped. If you want to run it, configure additional loopback addresses:\n        for n in $(seq 2 127) ; do sudo ifconfig lo0 alias \"127.0.0.${n}\" up ; done.\n    \"\"\"\n    with misc.create_http_server(context.http_01_port) as webroot:\n        context.certbot(['certonly', '-a', 'webroot', '--webroot-path', webroot,\n                          '--ip-address', context.local_ip])\n\n    assert_cert_count_for_lineage(context.config_dir, context.local_ip, 1)\n\n\ndef test_auth_and_install_with_csr(context: IntegrationTestsContext) -> None:\n    \"\"\"Test certificate issuance and install using an existing CSR.\"\"\"\n    certname = context.get_domain('le3')\n    key_path = join(context.workspace, 'key.pem')\n    csr_path = join(context.workspace, 'csr.der')\n\n    misc.generate_csr([certname], key_path, csr_path)\n\n    cert_path = join(context.workspace, 'csr', 'cert.pem')\n    chain_path = join(context.workspace, 'csr', 'chain.pem')\n\n    context.certbot([\n        'auth', '--csr', csr_path,\n        '--cert-path', cert_path,\n        '--chain-path', chain_path\n    ])\n\n    print(misc.read_certificate(cert_path))\n    print(misc.read_certificate(chain_path))\n\n    context.certbot([\n        '--domains', certname, 'install',\n        '--cert-path', cert_path,\n        '--key-path', key_path\n    ])\n\n\ndef test_renew_files_permissions(context: IntegrationTestsContext) -> None:\n    \"\"\"Test proper certificate file permissions upon renewal\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')\n    privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n    assert_world_no_permissions(privkey1)\n\n    context.certbot(['renew'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    assert_world_no_permissions(privkey2)\n    assert_equals_group_owner(privkey1, privkey2)\n    assert_equals_world_read_permissions(privkey1, privkey2)\n    assert_equals_group_permissions(privkey1, privkey2)\n\n\ndef test_renew_with_hook_scripts(context: IntegrationTestsContext) -> None:\n    \"\"\"Test certificate renewal with script hooks.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    misc.generate_test_file_hooks(context.config_dir, context.hook_probe)\n    context.certbot(['renew'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    assert_hook_execution(context.hook_probe, 'deploy')\n\n\ndef test_renew_files_propagate_permissions(context: IntegrationTestsContext) -> None:\n    \"\"\"Test proper certificate renewal with custom permissions propagated on private key.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')\n    privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')\n\n    if os.name != 'nt':\n        os.chmod(privkey1, 0o444)\n    else:\n        import ntsecuritycon  # pylint: disable=import-error\n        import win32security  # pylint: disable=import-error\n\n        # Get the current DACL of the private key\n        security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION)\n        dacl = security.GetSecurityDescriptorDacl()\n        # Create a read permission for Everybody group\n        everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)\n        dacl.AddAccessAllowedAce(\n            win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody\n        )\n        # Apply the updated DACL to the private key\n        security.SetSecurityDescriptorDacl(1, dacl, 0)\n        win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security)\n\n    context.certbot(['renew'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    if os.name != 'nt':\n        # On Linux, read world permissions + all group permissions\n        # will be copied from the previous private key\n        assert_world_read_permissions(privkey2)\n        assert_equals_world_read_permissions(privkey1, privkey2)\n        assert_equals_group_permissions(privkey1, privkey2)\n    else:\n        # On Windows, world will never have any permissions, and\n        # group permission is irrelevant for this platform\n        assert_world_no_permissions(privkey2)\n\n\ndef test_graceful_renew_it_is_not_time(context: IntegrationTestsContext) -> None:\n    \"\"\"Test graceful renew is not done when it is not due time.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],\n                    force_renew=False)\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n    with pytest.raises(AssertionError):\n        assert_hook_execution(context.hook_probe, 'deploy')\n\n\ndef test_graceful_renew_it_is_time(context: IntegrationTestsContext) -> None:\n    \"\"\"Test graceful renew is done when it is due time.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'r') as file:\n        lines = file.readlines()\n    lines.insert(4, 'renew_before_expiry = 100 years{0}'.format(os.linesep))\n    with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file:\n        file.writelines(lines)\n\n    context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],\n                    force_renew=False)\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    assert_hook_execution(context.hook_probe, 'deploy')\n\n\ndef test_renew_when_ari_says_its_time(context: IntegrationTestsContext) -> None:\n    \"\"\"Test graceful renew is done when it is due time.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    # Tell Pebble to make ARI look urgent\n    with open(join(context.config_dir, 'live', certname, 'cert.pem'), 'r') as c:\n        certificate_pem = c.read()\n\n    misc.set_ari_response(certificate_pem, json.dumps({\n        'suggestedWindow': {\n            'start': '2020-01-01T00:00:00Z',\n            'end': '2020-01-01T00:00:00Z'\n        }\n    }))\n\n    # For the renew call only, avoid passing `--server` to the `certbot` command, so\n    # we fall back on the hardcoded default of `https://acme-v02.api.letsencrypt.org`.\n    # No requests should be made to that URL because the lineage has a baked-in Pebble\n    # URL in its config from the issuance earlier in this test case. If there's a bug\n    # an ARI _is_ called against that URL it will fail because Let's Encrypt doesn't\n    # know about certificates issued by Pebble.\n    context.directory_url = None\n    context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],\n                    force_renew=False)\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    assert_hook_execution(context.hook_probe, 'deploy')\n\n\ndef test_renew_with_changed_private_key_complexity(context: IntegrationTestsContext) -> None:\n    \"\"\"Test proper renew with updated private key complexity.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname, '--key-type', 'rsa', '--rsa-key-size', '4096'])\n\n    key1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')\n    assert_rsa_key(key1, 4096)\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    context.certbot(['renew'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')\n    assert_rsa_key(key2, 4096)\n\n    context.certbot(['renew', '--rsa-key-size', '2048'])\n\n    key3 = join(context.config_dir, 'archive', certname, 'privkey3.pem')\n    assert_rsa_key(key3, 2048)\n\ndef test_certonly_non_default_key_size_kept(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that certonly keeps key type but uses default key size when unspecified.\"\"\"\n\n    # create rsa 4096 key cert\n    certname = context.get_domain('renew')\n    context.certbot([\n        'certonly',\n        '--cert-name', certname,\n        '--key-type', 'rsa', '--rsa-key-size', '4096',\n        '--force-renewal', '-d', certname,\n    ])\n    key1 = join(context.config_dir, \"archive\", certname, 'privkey1.pem')\n    assert_rsa_key(key1, 4096)\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n    assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'rsa')\n\n    # When running non-interactively, if --key-type is unspecified but the default value differs\n    # to the lineage key type, Certbot should keep the lineage key type. The key size will still\n    # change to the default value, in order to stay consistent with the behavior of certonly.\n    context.certbot(['certonly', '--force-renewal', '-d', certname])\n    key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')\n    assert_rsa_key(key2, 2048)\n\n\ndef test_renew_ignoring_directory_hooks(context: IntegrationTestsContext) -> None:\n    \"\"\"Test hooks are ignored during renewal with relevant CLI flag.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    misc.generate_test_file_hooks(context.config_dir, context.hook_probe)\n    context.certbot(['renew', '--no-directory-hooks'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    with pytest.raises(AssertionError):\n        assert_hook_execution(context.hook_probe, 'deploy')\n\n\ndef test_renew_empty_hook_scripts(context: IntegrationTestsContext) -> None:\n    \"\"\"Test proper renew with empty hook scripts.\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot(['-d', certname])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    misc.generate_test_file_hooks(context.config_dir, context.hook_probe)\n    for hook_dir in misc.list_renewal_hooks_dirs(context.config_dir):\n        shutil.rmtree(hook_dir)\n        os.makedirs(join(hook_dir, 'dir'))\n        with open(join(hook_dir, 'file'), 'w'):\n            pass\n    context.certbot(['renew'])\n\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n\n\ndef test_renew_hook_override(context: IntegrationTestsContext) -> None:\n    \"\"\"Test correct hook override on renew.\"\"\"\n    certname = context.get_domain('override')\n    context.certbot([\n        'certonly', '-d', certname,\n        '--preferred-challenges', 'http-01',\n        '--pre-hook', misc.echo('pre', context.hook_probe),\n        '--post-hook', misc.echo('post', context.hook_probe),\n        '--deploy-hook', misc.echo('deploy', context.hook_probe),\n    ])\n\n    assert_hook_execution(context.hook_probe, 'pre')\n    assert_hook_execution(context.hook_probe, 'post')\n    assert_hook_execution(context.hook_probe, 'deploy')\n\n    # Now we override all previous hooks during next renew.\n    with open(context.hook_probe, 'w'):\n        pass\n    context.certbot([\n        'renew', '--cert-name', certname,\n        '--pre-hook', misc.echo('pre_override', context.hook_probe),\n        '--post-hook', misc.echo('post_override', context.hook_probe),\n        '--deploy-hook', misc.echo('deploy_override', context.hook_probe),\n    ])\n\n    assert_hook_execution(context.hook_probe, 'pre_override')\n    assert_hook_execution(context.hook_probe, 'post_override')\n    assert_hook_execution(context.hook_probe, 'deploy_override')\n    with pytest.raises(AssertionError):\n        assert_hook_execution(context.hook_probe, 'pre')\n    with pytest.raises(AssertionError):\n        assert_hook_execution(context.hook_probe, 'post')\n    with pytest.raises(AssertionError):\n        assert_hook_execution(context.hook_probe, 'deploy')\n\n    # Expect that this renew will reuse new hooks registered in the previous renew.\n    with open(context.hook_probe, 'w'):\n        pass\n    context.certbot(['renew', '--cert-name', certname])\n\n    assert_hook_execution(context.hook_probe, 'pre_override')\n    assert_hook_execution(context.hook_probe, 'post_override')\n    assert_hook_execution(context.hook_probe, 'deploy_override')\n\n\ndef test_renew_hook_env_vars(context: IntegrationTestsContext) -> None:\n    fail_domain = context.get_domain('fail-env')\n    context.certbot([\n        'certonly', '-d', fail_domain,\n        '--preferred-challenges', 'http-01'\n    ])\n\n    context.certbot([\n        'renew',\n        '--post-hook', f'printenv RENEWED_DOMAINS >> {context.hook_probe}'\n    ])\n\n    assert_hook_execution(context.hook_probe, fail_domain)\n\n    # Clear probe\n    with open(context.hook_probe, 'w'):\n        pass\n\n    # now renew using manual dns, which will fail on renew\n    # manual_dns_auth_hook from misc is designed to fail if the domain contains 'fail-*'.\n    with pytest.raises(subprocess.CalledProcessError):\n        context.certbot([\n            'renew', '--cert-name', fail_domain,\n            '--preferred-challenges', 'dns',\n            '--manual-auth-hook', context.manual_dns_auth_hook,\n            '--manual-cleanup-hook', context.manual_dns_cleanup_hook,\n            '-a', 'manual',\n            '--post-hook', f'printenv FAILED_DOMAINS >> {context.hook_probe}',\n            '--dry-run', # use dry run here to deactivate previous authz, or this will pass\n        ])\n\n    assert_hook_execution(context.hook_probe, fail_domain)\n\n\ndef test_invalid_domain_with_dns_challenge(context: IntegrationTestsContext) -> None:\n    \"\"\"Test certificate issuance failure with DNS-01 challenge.\"\"\"\n    # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'.\n    domains = ','.join([context.get_domain('dns1'), context.get_domain('fail-dns1')])\n    context.certbot([\n        '-a', 'manual', '-d', domains,\n        '--allow-subset-of-names',\n        '--preferred-challenges', 'dns',\n        '--manual-auth-hook', context.manual_dns_auth_hook,\n        '--manual-cleanup-hook', context.manual_dns_cleanup_hook\n    ])\n\n    stdout, _ = context.certbot(['certificates'])\n\n    assert context.get_domain('fail-dns1') not in stdout\n\n\ndef test_reuse_key(context: IntegrationTestsContext) -> None:\n    \"\"\"Test various scenarios where a key is reused.\"\"\"\n    certname = context.get_domain('reusekey')\n    context.certbot(['--domains', certname, '--reuse-key'])\n    context.certbot(['renew', '--cert-name', certname])\n\n    with open(join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname), 'r') as file:\n        privkey1 = file.read()\n    with open(join(context.config_dir, 'archive/{0}/cert1.pem').format(certname), 'r') as file:\n        cert1 = file.read()\n    with open(join(context.config_dir, 'archive/{0}/privkey2.pem').format(certname), 'r') as file:\n        privkey2 = file.read()\n    with open(join(context.config_dir, 'archive/{0}/cert2.pem').format(certname), 'r') as file:\n        cert2 = file.read()\n    assert privkey1 == privkey2\n\n    context.certbot(['--cert-name', certname, '--domains', certname, '--force-renewal'])\n\n    with open(join(context.config_dir, 'archive/{0}/privkey3.pem').format(certname), 'r') as file:\n        privkey3 = file.read()\n    with open(join(context.config_dir, 'archive/{0}/cert3.pem').format(certname), 'r') as file:\n        cert3 = file.read()\n    assert privkey2 != privkey3\n\n    context.certbot(['--cert-name', certname, '--domains', certname,\n                     '--reuse-key','--force-renewal'])\n    with open(join(context.config_dir, 'archive/{0}/privkey4.pem').format(certname), 'r') as file:\n        privkey4 = file.read()\n    context.certbot(['renew', '--cert-name', certname, '--no-reuse-key', '--force-renewal'])\n    with open(join(context.config_dir, 'archive/{0}/privkey5.pem').format(certname), 'r') as file:\n        privkey5 = file.read()\n    context.certbot(['renew', '--cert-name', certname, '--force-renewal'])\n    with open(join(context.config_dir, 'archive/{0}/privkey6.pem').format(certname), 'r') as file:\n        privkey6 = file.read()\n\n    assert privkey3 == privkey4\n    assert privkey4 != privkey5\n    assert privkey5 != privkey6\n\n    assert len({cert1, cert2, cert3}) == 3\n\n\ndef test_reuse_key_allow_subset_of_names(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that key is reused when allow_subset_of_names is set.\"\"\"\n    certname = context.get_domain('dns1')\n    domains = ','.join([certname, context.get_domain('fail-dns1')])\n    context.certbot([\n        '-a', 'manual', '-d', domains,\n        '--allow-subset-of-names',\n        '--preferred-challenges', 'dns',\n        '--manual-auth-hook', context.manual_dns_auth_hook_allow_fail,\n        '--manual-cleanup-hook', context.manual_dns_cleanup_hook,\n        '--reuse-key'\n    ])\n\n    stdout, _ = context.certbot(['certificates'])\n    assert context.get_domain('fail-dns1') in stdout\n    certname = context.get_domain('dns1')\n\n    # manual_dns_auth_hook from misc is designed to fail if the domain contains 'fail-*'.\n    context.certbot([\n        'reconfigure',\n        '--cert-name', certname,\n        '--manual-auth-hook', context.manual_dns_auth_hook,\n        '-a', 'manual' # needed to override --standalone passed automatically\n    ])\n\n    context.certbot(['renew', '--cert-name', certname, '--force-renewal', '-a', 'manual'])\n    stdout, _ = context.certbot(['certificates'])\n    assert context.get_domain('fail-dns1') not in stdout\n\n    with open(join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname), 'r') as file:\n        privkey1 = file.read()\n    with open(join(context.config_dir, 'archive/{0}/privkey2.pem').format(certname), 'r') as file:\n        privkey2 = file.read()\n    assert privkey1 == privkey2\n\n\ndef test_new_key(context: IntegrationTestsContext) -> None:\n    \"\"\"Tests --new-key and its interactions with --reuse-key\"\"\"\n    def private_key(generation: int) -> tuple[str, str]:\n        pk_path = join(context.config_dir, f'archive/{certname}/privkey{generation}.pem')\n        with open(pk_path, 'r') as file:\n            return file.read(), pk_path\n\n    certname = context.get_domain('newkey')\n\n    context.certbot(['--domains', certname, '--reuse-key',\n                     '--key-type', 'ecdsa', '--elliptic-curve', 'secp384r1'])\n    privkey1, _ = private_key(1)\n\n    # renew: --new-key should replace the key, but keep reuse_key and the key type + params\n    context.certbot(['renew', '--cert-name', certname, '--new-key'])\n    privkey2, privkey2_path = private_key(2)\n    assert privkey1 != privkey2\n    assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')\n    assert_elliptic_key(privkey2_path, SECP384R1)\n\n    # certonly: it should replace the key but the elliptic curve will change\n    context.certbot(['certonly', '-d', certname, '--reuse-key', '--new-key'])\n    privkey3, privkey3_path = private_key(3)\n    assert privkey2 != privkey3\n    assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')\n    assert_elliptic_key(privkey3_path, SECP256R1)\n\n    # certonly: it should be possible to change the key type and keep reuse_key\n    context.certbot(['certonly', '-d', certname, '--reuse-key', '--new-key', '--key-type', 'rsa',\n                     '--rsa-key-size', '4096', '--cert-name', certname])\n    privkey4, privkey4_path = private_key(4)\n    assert privkey3 != privkey4\n    assert_saved_lineage_option(context.config_dir, certname, 'reuse_key', 'True')\n    assert_rsa_key(privkey4_path, 4096)\n\n    # certonly: it should not be possible to change a key parameter without --new-key\n    with pytest.raises(subprocess.CalledProcessError) as error:\n        context.certbot(['certonly', '-d', certname, '--key-type', 'rsa', '--reuse-key',\n                         '--rsa-key-size', '2048'])\n    assert 'Unable to change the --rsa-key-size' in error.value.stderr\n\n    # certonly: not specifying --key-type should keep the existing key type (non-interactively).\n    context.certbot(['certonly', '-d', certname, '--no-reuse-key'])\n    privkey5, privkey5_path = private_key(5)\n    assert_rsa_key(privkey5_path, 2048)\n    assert privkey4 != privkey5\n\n\ndef test_incorrect_key_type(context: IntegrationTestsContext) -> None:\n    with pytest.raises(subprocess.CalledProcessError):\n        context.certbot(['--key-type=\"failwhale\"'])\n\n\ndef test_ecdsa(context: IntegrationTestsContext) -> None:\n    \"\"\"Test issuance for ECDSA CSR based request (legacy supported mode).\"\"\"\n    key_path = join(context.workspace, 'privkey-p384.pem')\n    csr_path = join(context.workspace, 'csr-p384.der')\n    cert_path = join(context.workspace, 'cert-p384.pem')\n    chain_path = join(context.workspace, 'chain-p384.pem')\n\n    misc.generate_csr(\n        [context.get_domain('ecdsa')],\n        key_path, csr_path,\n        key_type=misc.ECDSA_KEY_TYPE\n    )\n    context.certbot([\n        'auth', '--csr', csr_path, '--cert-path', cert_path,\n        '--chain-path', chain_path,\n    ])\n\n    certificate = misc.read_certificate(cert_path)\n    # Check that the certificate uses SECP384R1 curve\n    public_key = certificate.public_key()\n    assert isinstance(public_key, ec.EllipticCurvePublicKey)\n    assert isinstance(public_key.curve, ec.SECP384R1)\n\n\ndef test_default_key_type(context: IntegrationTestsContext) -> None:\n    \"\"\"Test default key type is ECDSA\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot([\n        'certonly',\n        '--cert-name', certname, '-d', certname\n    ])\n    filename = join(context.config_dir, 'archive/{0}/privkey1.pem').format(certname)\n    assert_elliptic_key(filename, SECP256R1)\n\n\ndef test_default_rsa_size(context: IntegrationTestsContext) -> None:\n    \"\"\"test that the RSA key size used when not specifying any is 2048\"\"\"\n    certname = context.get_domain('renew')\n    context.certbot([\n        '--key-type', 'rsa', '--cert-name', certname, '-d', certname\n    ])\n    key1 = join(context.config_dir, 'archive/{0}/privkey1.pem'.format(certname))\n    assert_rsa_key(key1, 2048)\n\n\n@pytest.mark.parametrize('curve,curve_cls', [\n    # Curve name, Curve class, ACME servers to skip\n    ('secp256r1', SECP256R1),\n    ('secp384r1', SECP384R1),\n    ('secp521r1', SECP521R1)]\n)\ndef test_ecdsa_curves(context: IntegrationTestsContext, curve: str,\n                      curve_cls: type[EllipticCurve]) -> None:\n    \"\"\"Test issuance for each supported ECDSA curve\"\"\"\n    domain = context.get_domain('curve')\n    context.certbot([\n        'certonly',\n        '--key-type', 'ecdsa', '--elliptic-curve', curve,\n        '--force-renewal', '-d', domain,\n    ])\n    key = join(context.config_dir, \"live\", domain, 'privkey.pem')\n    assert_elliptic_key(key, curve_cls)\n\n\ndef test_renew_with_ec_keys(context: IntegrationTestsContext) -> None:\n    \"\"\"Test proper renew with updated private key complexity.\"\"\"\n    # create ecdsa 256 key cert\n    certname = context.get_domain('renew')\n    context.certbot([\n        'certonly',\n        '--cert-name', certname,\n        '--key-type', 'ecdsa', '--elliptic-curve', 'secp256r1',\n        '--force-renewal', '-d', certname,\n    ])\n    key1 = join(context.config_dir, \"archive\", certname, 'privkey1.pem')\n    assert 200 < os.stat(key1).st_size < 250  # ec keys of 256 bits are ~225 bytes\n    assert_elliptic_key(key1, SECP256R1)\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n    assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'ecdsa')\n\n    # renew using 384 ecdsa key instead\n    context.certbot(['renew', '--elliptic-curve', 'secp384r1'])\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n    key2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')\n    assert 280 < os.stat(key2).st_size < 320  # ec keys of 384 bits are ~310 bytes\n    assert_elliptic_key(key2, SECP384R1)\n\n    # When running non-interactively, if --key-type is unspecified, Certbot should keep the\n    # lineage key type. The curve will still change to the default value, in order to stay\n    # consistent with the behavior of certonly.\n    context.certbot(['certonly', '--force-renewal', '-d', certname])\n    key3 = join(context.config_dir, 'archive', certname, 'privkey3.pem')\n    assert 200 < os.stat(key3).st_size < 250  # ec keys of 256 bits are ~225 bytes\n    assert_elliptic_key(key3, SECP256R1)\n\n    # When running non-interactively, specifying a different --key-type requires user confirmation\n    # with both --key-type and --cert-name.\n    with pytest.raises(subprocess.CalledProcessError) as error:\n        context.certbot(['certonly', '--force-renewal', '-d', certname,\n                         '--key-type', 'rsa'])\n    assert 'Please provide both --cert-name and --key-type' in error.value.stderr\n\n    context.certbot(['certonly', '--force-renewal', '-d', certname,\n                     '--key-type', 'rsa', '--cert-name', certname])\n    key4 = join(context.config_dir, 'archive', certname, 'privkey4.pem')\n    assert_rsa_key(key4)\n\n    # We expect that the previous behavior of requiring both --cert-name and\n    # --key-type to be set to not apply to the renew subcommand.\n    context.certbot(['renew', '--force-renewal', '--key-type', 'ecdsa'])\n    key5 = join(context.config_dir, 'archive', certname, 'privkey5.pem')\n    assert 200 < os.stat(key5).st_size < 250  # ec keys of 256 bits are ~225 bytes\n    assert_elliptic_key(key5, SECP256R1)\n\n\ndef test_ocsp_must_staple(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that OCSP Must-Staple is correctly set in the generated certificate.\"\"\"\n    certname = context.get_domain('must-staple')\n    context.certbot(['auth', '--must-staple', '--domains', certname])\n\n    certificate = misc.read_certificate(join(context.config_dir,\n                                             'live/{0}/cert.pem').format(certname))\n\n    try:\n        tls_feature_ext = certificate.extensions.get_extension_for_class(x509.TLSFeature)\n        assert TLSFeatureType.status_request in list(tls_feature_ext.value)\n    except ExtensionNotFound:\n        assert False, \"OCSP Must-Staple requires TLS Feature extension\"\n\n\ndef test_revoke_simple(context: IntegrationTestsContext) -> None:\n    \"\"\"Test various scenarios that revokes a certificate.\"\"\"\n    # Default action after revoke is to delete the certificate.\n    certname = context.get_domain()\n    cert_path = join(context.config_dir, 'live', certname, 'cert.pem')\n    context.certbot(['-d', certname])\n    context.certbot(['revoke', '--cert-path', cert_path, '--delete-after-revoke'])\n\n    assert not exists(cert_path)\n\n    # Check default deletion is overridden.\n    certname = context.get_domain('le1')\n    cert_path = join(context.config_dir, 'live', certname, 'cert.pem')\n    context.certbot(['-d', certname])\n    context.certbot(['revoke', '--cert-path', cert_path, '--no-delete-after-revoke'])\n\n    assert exists(cert_path)\n\n    context.certbot(['delete', '--cert-name', certname])\n\n    assert not exists(join(context.config_dir, 'archive', certname))\n    assert not exists(join(context.config_dir, 'live', certname))\n    assert not exists(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)))\n\n    certname = context.get_domain('le2')\n    key_path = join(context.config_dir, 'live', certname, 'privkey.pem')\n    cert_path = join(context.config_dir, 'live', certname, 'cert.pem')\n    context.certbot(['-d', certname])\n    context.certbot(['revoke', '--cert-path', cert_path, '--key-path', key_path])\n\n\ndef test_revoke_and_unregister(context: IntegrationTestsContext) -> None:\n    \"\"\"Test revoke with a reason then unregister.\"\"\"\n    cert1 = context.get_domain('le1')\n    cert2 = context.get_domain('le2')\n    cert3 = context.get_domain('le3')\n\n    cert_path1 = join(context.config_dir, 'live', cert1, 'cert.pem')\n    key_path2 = join(context.config_dir, 'live', cert2, 'privkey.pem')\n    cert_path2 = join(context.config_dir, 'live', cert2, 'cert.pem')\n\n    context.certbot(['-d', cert1])\n    context.certbot(['-d', cert2])\n    context.certbot(['-d', cert3])\n\n    context.certbot(['revoke', '--cert-path', cert_path1,\n                    '--reason', 'cessationOfOperation'])\n    context.certbot(['revoke', '--cert-path', cert_path2, '--key-path', key_path2,\n                    '--reason', 'keyCompromise'])\n\n    context.certbot(['unregister'])\n\n    stdout, _ = context.certbot(['certificates'])\n\n    assert cert1 not in stdout\n    assert cert2 not in stdout\n    assert cert3 in stdout\n\n\n@pytest.mark.parametrize('curve,curve_cls', [\n    ('secp256r1', SECP256R1),\n    ('secp384r1', SECP384R1),\n    ('secp521r1', SECP521R1)]\n)\ndef test_revoke_ecdsa_cert_key(\n    context: IntegrationTestsContext, curve: str, curve_cls: type[EllipticCurve]) -> None:\n    \"\"\"Test revoking a certificate \"\"\"\n    cert: str = context.get_domain('curve')\n    context.certbot([\n        'certonly',\n        '--key-type', 'ecdsa', '--elliptic-curve', curve,\n        '-d', cert,\n    ])\n    key = join(context.config_dir, \"live\", cert, 'privkey.pem')\n    cert_path = join(context.config_dir, \"live\", cert, 'cert.pem')\n    assert_elliptic_key(key, curve_cls)\n    context.certbot([\n        'revoke', '--cert-path', cert_path, '--key-path', key,\n        '--no-delete-after-revoke',\n    ])\n    stdout, _ = context.certbot(['certificates'])\n    assert stdout.count('INVALID: REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)\n\n\n@pytest.mark.parametrize('curve,curve_cls', [\n    ('secp256r1', SECP256R1),\n    ('secp384r1', SECP384R1),\n    ('secp521r1', SECP521R1)]\n)\ndef test_revoke_ecdsa_cert_key_delete(\n    context: IntegrationTestsContext, curve: str, curve_cls: type[EllipticCurve]) -> None:\n    \"\"\"Test revoke and deletion for each supported curve type\"\"\"\n    cert: str = context.get_domain('curve')\n    context.certbot([\n        'certonly',\n        '--key-type', 'ecdsa', '--elliptic-curve', curve,\n        '-d', cert,\n    ])\n    key = join(context.config_dir, \"live\", cert, 'privkey.pem')\n    cert_path = join(context.config_dir, \"live\", cert, 'cert.pem')\n    assert_elliptic_key(key, curve_cls)\n    context.certbot([\n        'revoke', '--cert-path', cert_path, '--key-path', key,\n        '--delete-after-revoke',\n    ])\n    assert not exists(cert_path)\n\n\ndef test_revoke_mutual_exclusive_flags(context: IntegrationTestsContext) -> None:\n    \"\"\"Test --cert-path and --cert-name cannot be used during revoke.\"\"\"\n    cert = context.get_domain('le1')\n    context.certbot(['-d', cert])\n    with pytest.raises(subprocess.CalledProcessError) as error:\n        context.certbot([\n            'revoke', '--cert-name', cert,\n            '--cert-path', join(context.config_dir, 'live', cert, 'fullchain.pem')\n        ])\n    assert 'Exactly one of --cert-path or --cert-name must be specified' in error.value.stderr\n\n\ndef test_revoke_multiple_lineages(context: IntegrationTestsContext) -> None:\n    \"\"\"Test revoke does not delete certs if multiple lineages share the same dir.\"\"\"\n    cert1 = context.get_domain('le1')\n    context.certbot(['-d', cert1])\n\n    assert os.path.isfile(join(context.config_dir, 'renewal', '{0}.conf'.format(cert1)))\n\n    cert2 = context.get_domain('le2')\n    context.certbot(['-d', cert2])\n\n    # Copy over renewal configuration of cert1 into renewal configuration of cert2.\n    with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'r') as file:\n        data = file.read()\n\n    data = re.sub(\n        'archive_dir = .*\\n',\n        'archive_dir = {0}\\n'.format(\n            join(context.config_dir, 'archive', cert1).replace('\\\\', '\\\\\\\\')\n        ), data\n    )\n\n    with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file:\n        file.write(data)\n\n    context.certbot([\n        'revoke', '--cert-path', join(context.config_dir, 'live', cert1, 'cert.pem')\n    ])\n\n    with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:\n        assert 'Not deleting revoked certificates due to overlapping archive dirs' in f.read()\n\n\ndef test_reconfigure(context: IntegrationTestsContext) -> None:\n    \"\"\"Test the reconfigure verb\"\"\"\n    certname = context.get_domain()\n    context.certbot(['-d', certname])\n    conf_path = join(context.config_dir, 'renewal', '{}.conf'.format(certname))\n\n    with misc.create_http_server(context.http_01_port) as webroot:\n        context.certbot(['reconfigure', '--cert-name', certname,\n                         '-a', 'webroot', '--webroot-path', webroot])\n        with open(conf_path, 'r') as f:\n            file_contents = f.read()\n            # Check changed value\n            assert 'authenticator = webroot' in file_contents, \\\n                   'Expected authenticator to be changed to webroot in renewal config'\n            # Check added value\n            assert f'webroot_path = {webroot}' in file_contents, \\\n                   'Expected new webroot path to be added to renewal config'\n\n\ndef test_wildcard_certificates(context: IntegrationTestsContext) -> None:\n    \"\"\"Test wildcard certificate issuance.\"\"\"\n    certname = context.get_domain('wild')\n\n    context.certbot([\n        '-a', 'manual', '-d', '*.{0},{0}'.format(certname),\n        '--preferred-challenge', 'dns',\n        '--manual-auth-hook', context.manual_dns_auth_hook,\n        '--manual-cleanup-hook', context.manual_dns_cleanup_hook\n    ])\n\n    assert exists(join(context.config_dir, 'live', certname, 'fullchain.pem'))\n\n\ndef test_ocsp_status_stale(context: IntegrationTestsContext) -> None:\n    \"\"\"Test retrieval of OCSP statuses for staled config\"\"\"\n    sample_data_path = misc.load_sample_data_path(context.workspace)\n    stdout, _ = context.certbot(['certificates', '--config-dir', sample_data_path])\n\n    assert stdout.count('TEST_CERT') == 2, ('Did not find two test certs as expected ({0})'\n                                            .format(stdout.count('TEST_CERT')))\n    assert stdout.count('EXPIRED') == 2, ('Did not find two expired certs as expected ({0})'\n                                          .format(stdout.count('EXPIRED')))\n\n\ndef test_ocsp_status_live(context: IntegrationTestsContext) -> None:\n    \"\"\"Test retrieval of OCSP statuses for live config\"\"\"\n    cert = context.get_domain('ocsp-check')\n\n    # OSCP 1: Check live certificate OCSP status (VALID)\n    context.certbot(['--domains', cert])\n    stdout, _ = context.certbot(['certificates'])\n\n    assert stdout.count('VALID') == 1, 'Expected {0} to be VALID'.format(cert)\n    assert stdout.count('EXPIRED') == 0, 'Did not expect {0} to be EXPIRED'.format(cert)\n\n    # OSCP 2: Check live certificate OCSP status (REVOKED)\n    context.certbot(['revoke', '--cert-name', cert, '--no-delete-after-revoke'])\n    stdout, _ = context.certbot(['certificates'])\n\n    assert stdout.count('INVALID') == 1, 'Expected {0} to be INVALID'.format(cert)\n    assert stdout.count('REVOKED') == 1, 'Expected {0} to be REVOKED'.format(cert)\n\n\ndef test_ocsp_renew(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that revoked certificates are renewed.\"\"\"\n    # Obtain a certificate\n    certname = context.get_domain('ocsp-renew')\n    context.certbot(['--domains', certname])\n\n    # Test that \"certbot renew\" does not renew the certificate\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n    context.certbot(['renew'], force_renew=False)\n    assert_cert_count_for_lineage(context.config_dir, certname, 1)\n\n    # Revoke the certificate and test that it does renew the certificate\n    context.certbot(['revoke', '--cert-name', certname, '--no-delete-after-revoke'])\n    context.certbot(['renew'], force_renew=False)\n    assert_cert_count_for_lineage(context.config_dir, certname, 2)\n\n\ndef test_dry_run_deactivate_authzs(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that Certbot deactivates authorizations when performing a dry run\"\"\"\n\n    name = context.get_domain('dry-run-authz-deactivation')\n    args = ['certonly', '--cert-name', name, '-d', name, '--dry-run']\n    log_line = 'Recreating order after authz deactivation'\n\n    # First order will not need deactivation\n    context.certbot(args)\n    with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:\n        assert log_line not in f.read(), 'First order should not have had any authz reuse'\n\n    # Second order will require deactivation\n    context.certbot(args)\n    with open(join(context.workspace, 'logs', 'letsencrypt.log'), 'r') as f:\n        assert log_line in f.read(), 'Second order should have been recreated due to authz reuse'\n\n\ndef test_preferred_chain(context: IntegrationTestsContext) -> None:\n    \"\"\"Test that --preferred-chain results in the correct chain.pem being produced\"\"\"\n    try:\n        issuers = misc.get_acme_issuers()\n    except NotImplementedError:\n        pytest.skip('This ACME server does not support alternative issuers.')\n\n    names = [str(i.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value) \\\n             for i in issuers]\n\n    domain = context.get_domain('preferred-chain')\n    cert_path = join(context.config_dir, 'live', domain, 'chain.pem')\n    conf_path = join(context.config_dir, 'renewal', '{}.conf'.format(domain))\n\n    for (requested, expected) in [(n, n) for n in names] + [('nonexistent', names[0])]:\n        args = ['certonly', '--cert-name', domain, '-d', domain,\n                '--preferred-chain', requested, '--force-renewal']\n        context.certbot(args)\n\n        certificate = misc.read_certificate(cert_path)\n        issuer_cn = certificate.issuer.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value\n        assert expected == issuer_cn, \\\n               f'Expected chain issuer to be {expected} when preferring {requested}'\n\n        with open(conf_path, 'r') as f:\n            assert f'preferred_chain = {requested}' in f.read(), \\\n                   'Expected preferred_chain to be set in renewal config'\n\n\ndef test_ancient_rsa_key_type_preserved(context: IntegrationTestsContext) -> None:\n    certname = context.get_domain('newname')\n    context.certbot(['certonly', '-d', certname, '--key-type', 'rsa'])\n    assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'rsa')\n\n    # Remove `key_type = rsa` from the renewal config to emulate a <v1.25.0 Certbot certificate.\n    conf_path = join(context.config_dir, 'renewal', f'{certname}.conf')\n    conf_contents: str = ''\n    with open(conf_path) as f:\n        conf_contents = f.read()\n    conf_contents = conf_contents.replace('key_type = rsa', '')\n    with open(conf_path, 'w') as f:\n        f.write(conf_contents)\n\n    context.certbot(['renew', '--cert-name', certname, '--force-renewal'])\n\n    assert_saved_lineage_option(context.config_dir, certname, 'key_type', 'rsa')\n    key2 = join(context.config_dir, 'archive/{0}/privkey2.pem'.format(certname))\n    assert_rsa_key(key2, 2048)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/conftest.py",
    "content": "# type: ignore\n\"\"\"\nGeneral conftest for pytest execution of all integration tests lying\nin the certbot_integration tests package.\nAs stated by pytest documentation, conftest module is used to set on\nfor a directory a specific configuration using built-in pytest hooks.\n\nSee https://docs.pytest.org/en/latest/reference.html#hook-reference\n\"\"\"\nimport contextlib\nimport sys\n\nfrom certbot_integration_tests.utils import acme_server as acme_lib\nfrom certbot_integration_tests.utils import dns_server as dns_lib\n\n\ndef pytest_addoption(parser):\n    \"\"\"\n    Standard pytest hook to add options to the pytest parser.\n    :param parser: current pytest parser that will be used on the CLI\n    \"\"\"\n    parser.addoption('--dns-server', default='challtestsrv',\n                     choices=['bind', 'challtestsrv'],\n                     help='select the DNS server to use (bind, challtestsrv), '\n                          'defaulting to challtestsrv')\n\n\ndef pytest_configure(config):\n    \"\"\"\n    Standard pytest hook used to add a configuration logic for each node of a pytest run.\n    :param config: the current pytest configuration\n    \"\"\"\n    if not hasattr(config, 'workerinput'):  # If true, this is the primary node\n        with _print_on_err():\n            _setup_primary_node(config)\n\n\ndef pytest_configure_node(node):\n    \"\"\"\n    Standard pytest-xdist hook used to configure a worker node.\n    :param node: current worker node\n    \"\"\"\n    node.workerinput['acme_xdist'] = node.config.acme_xdist\n    node.workerinput['dns_xdist'] = node.config.dns_xdist\n\n\n@contextlib.contextmanager\ndef _print_on_err():\n    \"\"\"\n    During pytest-xdist setup, stdout is used for nodes communication, so print is useless.\n    However, stderr is still available. This context manager transfers stdout to stderr\n    for the duration of the context, allowing to display prints to the user.\n    \"\"\"\n    old_stdout = sys.stdout\n    sys.stdout = sys.stderr\n    try:\n        yield\n    finally:\n        sys.stdout = old_stdout\n\n\ndef _setup_primary_node(config):\n    \"\"\"\n    Setup the environment for integration tests.\n\n    This function will:\n        - check runtime compatibility (Docker, docker compose, Nginx)\n        - create a temporary workspace and the persistent GIT repositories space\n        - configure and start a DNS server using Docker, if configured\n        - configure and start paralleled ACME CA servers using Docker\n        - transfer ACME CA and DNS servers configurations to pytest nodes using env variables\n\n    This function modifies `config` by injecting the ACME CA and DNS server configurations,\n    in addition to cleanup functions for those servers.\n\n    :param config: Configuration of the pytest primary node. Is modified by this function.\n    \"\"\"\n    # Parameter numprocesses is added to option by pytest-xdist\n    workers = ['primary'] if not config.option.numprocesses\\\n        else ['gw{0}'.format(i) for i in range(config.option.numprocesses)]\n\n    # If a non-default DNS server is configured, start it and feed it to the ACME server\n    dns_server = None\n    acme_dns_server = None\n    if config.option.dns_server == 'bind':\n        dns_server = dns_lib.DNSServer(workers)\n        config.add_cleanup(dns_server.stop)\n        print('DNS xdist config:\\n{0}'.format(dns_server.dns_xdist))\n        dns_server.start()\n        acme_dns_server = '{}:{}'.format(\n            dns_server.dns_xdist['address'],\n            dns_server.dns_xdist['port']\n        )\n\n    # By calling setup_acme_server we ensure that all necessary acme server instances will be\n    # fully started. This runtime is reflected by the acme_xdist returned.\n    acme_server = acme_lib.ACMEServer(workers, dns_server=acme_dns_server)\n    config.add_cleanup(acme_server.stop)\n    print('ACME xdist config:\\n{0}'.format(acme_server.acme_xdist))\n    acme_server.start()\n\n    config.acme_xdist = acme_server.acme_xdist\n    config.dns_xdist = dns_server.dns_xdist if dns_server else None\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/nginx_tests/__init__.py",
    "content": ""
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/nginx_tests/context.py",
    "content": "\"\"\"Module to handle the context of nginx integration tests.\"\"\"\nimport os\nimport subprocess\nfrom typing import Iterable\n\nimport pytest\n\nfrom certbot_integration_tests.certbot_tests import context as certbot_context\nfrom certbot_integration_tests.nginx_tests import nginx_config as config\nfrom certbot_integration_tests.utils import certbot_call\nfrom certbot_integration_tests.utils import constants\nfrom certbot_integration_tests.utils import misc\n\n\nclass IntegrationTestsContext(certbot_context.IntegrationTestsContext):\n    \"\"\"General fixture describing a certbot-nginx integration tests context\"\"\"\n    def __init__(self, request: pytest.FixtureRequest) -> None:\n        super().__init__(request)\n\n        self.nginx_root = os.path.join(self.workspace, 'nginx')\n        os.mkdir(self.nginx_root)\n\n        self.webroot = os.path.join(self.nginx_root, 'webroot')\n        os.mkdir(self.webroot)\n        with open(os.path.join(self.webroot, 'index.html'), 'w') as file_handler:\n            file_handler.write('Hello World!')\n\n        self.nginx_config_path = os.path.join(self.nginx_root, 'nginx.conf')\n        self.nginx_config: str\n\n        default_server = request.param['default_server']\n        self.process = self._start_nginx(default_server)\n\n    def cleanup(self) -> None:\n        self._stop_nginx()\n        super().cleanup()\n\n    def certbot_test_nginx(self, args: Iterable[str]) -> tuple[str, str]:\n        \"\"\"\n        Main command to execute certbot using the nginx plugin.\n        :param list args: list of arguments to pass to nginx\n        :param bool force_renew: set to False to not renew by default\n        \"\"\"\n        command = ['--authenticator', 'nginx', '--installer', 'nginx',\n                   '--nginx-server-root', self.nginx_root]\n        command.extend(args)\n        return certbot_call.certbot_test(\n            command, self.directory_url, self.http_01_port, self.https_port,\n            self.config_dir, self.workspace, force_renew=True)\n\n    def _start_nginx(self, default_server: bool) -> subprocess.Popen[bytes]:\n        self.nginx_config = config.construct_nginx_config(\n            self.nginx_root, self.webroot, self.http_01_port, self.https_port,\n            self.other_port, default_server, wtf_prefix=self.worker_id)\n        with open(self.nginx_config_path, 'w') as file:\n            file.write(self.nginx_config)\n\n        # pylint: disable=consider-using-with\n        process = subprocess.Popen(['nginx', '-c', self.nginx_config_path, '-g', 'daemon off;'])\n\n        assert process.poll() is None\n        misc.check_until_timeout('http://localhost:{0}'.format(self.http_01_port))\n        return process\n\n    def _stop_nginx(self) -> None:\n        assert self.process.poll() is None\n        self.process.terminate()\n        self.process.wait(constants.MAX_SUBPROCESS_WAIT)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/nginx_tests/nginx_config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"General purpose nginx test configuration generator.\"\"\"\nimport atexit\nimport getpass\nimport importlib.resources\nfrom contextlib import ExitStack\nfrom typing import Optional\n\n\ndef construct_nginx_config(nginx_root: str, nginx_webroot: str, http_port: int, https_port: int,\n                           other_port: int, default_server: bool, key_path: Optional[str] = None,\n                           cert_path: Optional[str] = None, wtf_prefix: str = 'le') -> str:\n    \"\"\"\n    This method returns a full nginx configuration suitable for integration tests.\n    :param str nginx_root: nginx root configuration path\n    :param str nginx_webroot: nginx webroot path\n    :param int http_port: HTTP port to listen on\n    :param int https_port: HTTPS port to listen on\n    :param int other_port: other HTTP port to listen on\n    :param bool default_server: True to set a default server in nginx config, False otherwise\n    :param str key_path: the path to a SSL key\n    :param str cert_path: the path to a SSL certificate\n    :param str wtf_prefix: the prefix to use in all domains handled by this nginx config\n    :return: a string containing the full nginx configuration\n    :rtype: str\n    \"\"\"\n    if not key_path:\n        file_manager = ExitStack()\n        atexit.register(file_manager.close)\n        ref = (importlib.resources.files('certbot_integration_tests').joinpath('assets')\n               .joinpath('key.pem'))\n        key_path = str(file_manager.enter_context(importlib.resources.as_file(ref)))\n\n    if not cert_path:\n        file_manager = ExitStack()\n        atexit.register(file_manager.close)\n        ref = (importlib.resources.files('certbot_integration_tests').joinpath('assets')\n               .joinpath('cert.pem'))\n        cert_path = str(file_manager.enter_context(importlib.resources.as_file(ref)))\n\n    return '''\\\n# This error log will be written regardless of server scope error_log\n# definitions, so we have to set this here in the main scope.\n#\n# Even doing this, Nginx will still try to create the default error file, and\n# log a non-fatal error when it fails. After that things will work, however.\nerror_log {nginx_root}/error.log;\n\n# The pidfile will be written to /var/run unless this is set.\npid {nginx_root}/nginx.pid;\n\nuser {user};\nworker_processes 1;\n\nevents {{\n  worker_connections 1024;\n}}\n\n# “This comment contains valid Unicode”.\n\nhttp {{\n  # Set an array of temp, cache and log file options that will otherwise default to\n  # restricted locations accessible only to root.\n  client_body_temp_path {nginx_root}/client_body;\n  fastcgi_temp_path {nginx_root}/fastcgi_temp;\n  proxy_temp_path {nginx_root}/proxy_temp;\n  #scgi_temp_path {nginx_root}/scgi_temp;\n  #uwsgi_temp_path {nginx_root}/uwsgi_temp;\n  access_log {nginx_root}/error.log;\n\n  # This should be turned off in a Virtualbox VM, as it can cause some\n  # interesting issues with data corruption in delivered files.\n  sendfile off;\n\n  tcp_nopush on;\n  tcp_nodelay on;\n  keepalive_timeout 65;\n  types_hash_max_size 2048;\n\n  #include /etc/nginx/mime.types;\n  index index.html index.htm index.php;\n\n  log_format   main '$remote_addr - $remote_user [$time_local] $status '\n    '\"$request\" $body_bytes_sent \"$http_referer\" '\n    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n  default_type application/octet-stream;\n\n  server {{\n    # IPv4.\n    listen {http_port} {default_server};\n    # IPv6.\n    listen [::]:{http_port} {default_server};\n    server_name nginx.{wtf_prefix}.wtf nginx2.{wtf_prefix}.wtf;\n\n    root {nginx_webroot};\n\n    location / {{\n      # First attempt to serve request as file, then as directory, then fall\n      # back to index.html.\n      try_files $uri $uri/ /index.html;\n    }}\n  }}\n\n  server {{\n    listen {http_port};\n    listen [::]:{http_port};\n    server_name nginx3.{wtf_prefix}.wtf;\n\n    root {nginx_webroot};\n\n    location /.well-known/ {{\n      return 404;\n    }}\n\n    return 301 https://$host$request_uri;\n  }}\n\n  server {{\n    listen {other_port};\n    listen [::]:{other_port};\n    server_name nginx4.{wtf_prefix}.wtf nginx5.{wtf_prefix}.wtf;\n  }}\n\n  server {{\n    listen {http_port};\n    listen [::]:{http_port};\n    listen {https_port} ssl;\n    listen [::]:{https_port} ssl;\n    if ($scheme != \"https\") {{\n      return 301 https://$host$request_uri;\n    }}\n    server_name nginx6.{wtf_prefix}.wtf nginx7.{wtf_prefix}.wtf;\n\n    ssl_certificate {cert_path};\n    ssl_certificate_key {key_path};\n  }}\n}}\n'''.format(nginx_root=nginx_root, nginx_webroot=nginx_webroot, user=getpass.getuser(),\n           http_port=http_port, https_port=https_port, other_port=other_port,\n           default_server='default_server' if default_server else '', wtf_prefix=wtf_prefix,\n           key_path=key_path, cert_path=cert_path)\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/nginx_tests/test_main.py",
    "content": "\"\"\"Module executing integration tests against certbot with nginx plugin.\"\"\"\nimport os\nimport ssl\nfrom typing import Generator\n\nimport pytest\n\nfrom certbot_integration_tests.nginx_tests.context import IntegrationTestsContext\n\n\n@pytest.fixture(name='context')\ndef test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]:\n    # Fixture request is a built-in pytest fixture describing current test request.\n    integration_test_context = IntegrationTestsContext(request)\n    try:\n        yield integration_test_context\n    finally:\n        integration_test_context.cleanup()\n\n\n@pytest.mark.parametrize('certname_pattern, params, context', [\n    ('nginx.{0}.wtf', ['run'], {'default_server': True}),\n    ('nginx2.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),\n    # Overlapping location block and server-block-level return 301\n    ('nginx3.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),\n    # No matching server block; default_server exists\n    ('nginx4.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': True}),\n    # No matching server block; default_server does not exist\n    ('nginx5.{0}.wtf', ['--preferred-challenges', 'http'], {'default_server': False}),\n    # Multiple domains, mix of matching and not\n    ('nginx6.{0}.wtf,nginx7.{0}.wtf', [\n        '--preferred-challenges', 'http'\n    ], {'default_server': False}),\n], indirect=['context'])\ndef test_certificate_deployment(certname_pattern: str, params: list[str],\n                                context: IntegrationTestsContext) -> None:\n    \"\"\"\n    Test various scenarios to deploy a certificate to nginx using certbot.\n    \"\"\"\n    domains = certname_pattern.format(context.worker_id)\n    command = ['--domains', domains]\n    command.extend(params)\n    context.certbot_test_nginx(command)\n\n    lineage = domains.split(',')[0]\n    server_cert = ssl.get_server_certificate(('localhost', context.https_port))\n    with open(os.path.join(\n        context.workspace, 'conf/live/{0}/cert.pem'.format(lineage)), 'r'\n    ) as file:\n        certbot_cert = file.read()\n\n    assert server_cert == certbot_cert\n\n    context.certbot_test_nginx(['rollback', '--checkpoints', '1'])\n\n    with open(context.nginx_config_path, 'r') as file_h:\n        current_nginx_config = file_h.read()\n\n    assert context.nginx_config == current_nginx_config\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/py.typed",
    "content": ""
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/rfc2136_tests/__init__.py",
    "content": ""
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/rfc2136_tests/context.py",
    "content": "\"\"\"Module to handle the context of RFC2136 integration tests.\"\"\"\nfrom contextlib import contextmanager\nimport importlib.resources\nimport tempfile\nfrom typing import Generator\nfrom typing import Iterable\n\nimport pytest\n\nfrom certbot_integration_tests.certbot_tests import context as certbot_context\nfrom certbot_integration_tests.utils import certbot_call\n\n\nclass IntegrationTestsContext(certbot_context.IntegrationTestsContext):\n    \"\"\"Integration test context for certbot-dns-rfc2136\"\"\"\n    def __init__(self, request: pytest.FixtureRequest) -> None:\n        super().__init__(request)\n\n        self.request = request\n\n        if hasattr(request.config, 'workerinput'):  # Worker node\n            self._dns_xdist = request.config.workerinput['dns_xdist']\n        else:  # Primary node\n            self._dns_xdist = request.config.dns_xdist  # type: ignore[attr-defined]\n\n    def certbot_test_rfc2136(self, args: Iterable[str]) -> tuple[str, str]:\n        \"\"\"\n        Main command to execute certbot using the RFC2136 DNS authenticator.\n        :param list args: list of arguments to pass to Certbot\n        \"\"\"\n        command = ['--authenticator', 'dns-rfc2136', '--dns-rfc2136-propagation-seconds', '2']\n        command.extend(args)\n        return certbot_call.certbot_test(\n            command, self.directory_url, self.http_01_port, self.https_port,\n            self.config_dir, self.workspace, force_renew=True)\n\n    @contextmanager\n    def rfc2136_credentials(self, label: str = 'default') -> Generator[str, None, None]:\n        \"\"\"\n        Produces the contents of a certbot-dns-rfc2136 credentials file.\n        :param str label: which RFC2136 credential to use\n        :yields: Path to credentials file\n        :rtype: str\n        \"\"\"\n        src_ref_file = (importlib.resources.files('certbot_integration_tests').joinpath('assets')\n                        .joinpath('bind-config').joinpath(f'rfc2136-credentials-{label}.ini.tpl'))\n        with importlib.resources.as_file(src_ref_file) as src_file:\n            with open(src_file, 'r') as f:\n                contents = f.read().format(\n                    server_address=self._dns_xdist['address'],\n                    server_port=self._dns_xdist['port']\n                )\n\n        with tempfile.NamedTemporaryFile('w+', prefix='rfc2136-creds-{}'.format(label),\n                                         suffix='.ini', dir=self.workspace) as fp:\n            fp.write(contents)\n            fp.flush()\n            yield fp.name\n\n    def skip_if_no_bind9_server(self) -> None:\n        \"\"\"Skips the test if there was no RFC2136-capable DNS server configured\n        in the test environment\"\"\"\n        if not self._dns_xdist:\n            pytest.skip('No RFC2136-capable DNS server is configured')\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/rfc2136_tests/test_main.py",
    "content": "\"\"\"Module executing integration tests against Certbot with the RFC2136 DNS authenticator.\"\"\"\nfrom typing import Generator\n\nimport pytest\n\nfrom certbot_integration_tests.rfc2136_tests.context import IntegrationTestsContext\n\n\n@pytest.fixture(name=\"context\")\ndef test_context(request: pytest.FixtureRequest) -> Generator[IntegrationTestsContext, None, None]:\n    # pylint: disable=missing-function-docstring\n    # Fixture request is a built-in pytest fixture describing current test request.\n    integration_test_context = IntegrationTestsContext(request)\n    try:\n        yield integration_test_context\n    finally:\n        integration_test_context.cleanup()\n\n\n@pytest.mark.parametrize('domain', [('example.com'), ('sub.example.com')])\ndef test_get_certificate(domain: str, context: IntegrationTestsContext) -> None:\n    context.skip_if_no_bind9_server()\n\n    with context.rfc2136_credentials() as creds:\n        context.certbot_test_rfc2136([\n            'certonly', '--dns-rfc2136-credentials', creds,\n            '-d', domain, '-d', '*.{}'.format(domain)\n        ])\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/__init__.py",
    "content": ""
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/acme_server.py",
    "content": "#!/usr/bin/env python\n\"\"\"Module to setup an ACME CA server environment able to run multiple tests in parallel\"\"\"\n\nimport argparse\nimport errno\nimport json\nimport os\nimport shutil\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom types import TracebackType\nfrom typing import Any\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\n\n# pylint: disable=wildcard-import,unused-wildcard-import\nfrom certbot_integration_tests.utils import misc\nfrom certbot_integration_tests.utils import pebble_artifacts\nfrom certbot_integration_tests.utils import pebble_ocsp_server\nfrom certbot_integration_tests.utils import proxy\nfrom certbot_integration_tests.utils.constants import DEFAULT_HTTP_01_PORT\nfrom certbot_integration_tests.utils.constants import CHALLTESTSRV_PORT\nfrom certbot_integration_tests.utils.constants import PEBBLE_DIRECTORY_URL\nfrom certbot_integration_tests.utils.constants import PEBBLE_CHALLTESTSRV_URL\nfrom certbot_integration_tests.utils.constants import PEBBLE_ALTERNATE_ROOTS\nfrom certbot_integration_tests.utils.constants import MAX_SUBPROCESS_WAIT\n\n\nclass ACMEServer:\n    \"\"\"\n    ACMEServer configures and handles the lifecycle of an ACME CA server and an HTTP reverse proxy\n    instance, to allow parallel execution of integration tests against the unique http-01 port\n    expected by the ACME CA server.\n    Typically all pytest integration tests will be executed in this context.\n    ACMEServer gives access the acme_xdist parameter, listing the ports and directory url to use\n    for each pytest node. It exposes also start and stop methods in order to start the stack, and\n    stop it with proper resources cleanup.\n    ACMEServer is also a context manager, and so can be used to ensure ACME server is\n    started/stopped upon context enter/exit.\n    \"\"\"\n    def __init__(self, nodes: list[str], http_proxy: bool = True,\n                 stdout: bool = False, dns_server: Optional[str] = None,\n                 http_01_port: Optional[int] = None) -> None:\n        \"\"\"\n        Create an ACMEServer instance.\n        :param list nodes: list of node names that will be setup by pytest xdist\n        :param bool http_proxy: if False do not start the HTTP proxy\n        :param bool stdout: if True stream all subprocesses stdout to standard stdout\n        :param str dns_server: if set, Pebble will use it to resolve domains\n        :param int http_01_port: port to use for http-01 validation; currently\n            only supported for pebble without an HTTP proxy\n        \"\"\"\n        self._construct_acme_xdist(nodes)\n\n        self._proxy = http_proxy\n        self._workspace = tempfile.mkdtemp()\n        self._processes: list[subprocess.Popen[bytes]] = []\n        self._stdout = sys.stdout if stdout else open(os.devnull, 'w') # pylint: disable=consider-using-with\n        self._dns_server = dns_server\n        self._http_01_port = DEFAULT_HTTP_01_PORT\n        if http_01_port:\n            if self._proxy:\n                raise ValueError('Setting http_01_port is not currently supported when '\n                                 'using the HTTP proxy')\n            self._http_01_port = http_01_port\n\n    def start(self) -> None:\n        \"\"\"Start the test stack\"\"\"\n        try:\n            if self._proxy:\n                self._prepare_http_proxy()\n            self._prepare_pebble_server()\n        except BaseException as e:\n            self.stop()\n            raise e\n\n    def stop(self) -> None:\n        \"\"\"Stop the test stack, and clean its resources\"\"\"\n        print('=> Tear down the test infrastructure...')\n        try:\n            for process in self._processes:\n                try:\n                    process.terminate()\n                except OSError as e:\n                    # Process may be not started yet, so no PID and terminate fails.\n                    # Then the process never started, and the situation is acceptable.\n                    if e.errno != errno.ESRCH:\n                        raise\n            for process in self._processes:\n                process.wait(MAX_SUBPROCESS_WAIT)\n        finally:\n            if os.path.exists(self._workspace):\n                shutil.rmtree(self._workspace)\n        if self._stdout != sys.stdout:\n            self._stdout.close()\n        print('=> Test infrastructure stopped and cleaned up.')\n\n    def __enter__(self) -> dict[str, Any]:\n        self.start()\n        return self.acme_xdist\n\n    def __exit__(self, exc_type: Optional[type[BaseException]], exc: Optional[BaseException],\n                 traceback: Optional[TracebackType]) -> None:\n        self.stop()\n\n    def _construct_acme_xdist(self, nodes: list[str]) -> None:\n        \"\"\"Generate and return the acme_xdist dict\"\"\"\n        acme_xdist: dict[str, Any] = {}\n\n        # Directory and ACME port are set implicitly in the docker-compose.yml\n        # files of Pebble.\n        acme_xdist['directory_url'] = PEBBLE_DIRECTORY_URL\n        acme_xdist['challtestsrv_url'] = PEBBLE_CHALLTESTSRV_URL\n        acme_xdist['http_port'] = dict(zip(nodes, range(5200, 5200 + len(nodes))))\n        # When testing the standalone plugin with IP address certificates, we need a way for\n        # the proxy to map incoming requests to workers. Since on Linux all 127.* addresses are\n        # loopback, we give each worker a 127.0.0.n address. The proxy will route all requests for\n        # that IP address to the http_port assigned for that worker.\n        acme_xdist['local_ip'] = {}\n        for i, node in enumerate(nodes):\n            acme_xdist['local_ip'][node] = f\"127.0.0.{i+2}\"\n        acme_xdist['https_port'] = dict(zip(nodes, range(5100, 5100 + len(nodes))))\n        acme_xdist['other_port'] = dict(zip(nodes, range(5300, 5300 + len(nodes))))\n\n        self.acme_xdist = acme_xdist\n\n    def _prepare_pebble_server(self) -> None:\n        \"\"\"Configure and launch the Pebble server\"\"\"\n        print('=> Starting pebble instance deployment...')\n        pebble_artifacts_rv = pebble_artifacts.fetch(self._workspace, self._http_01_port)\n        pebble_path, challtestsrv_path, pebble_config_path = pebble_artifacts_rv\n\n        # Configure Pebble at full speed (PEBBLE_VA_NOSLEEP=1) and not randomly refusing valid\n        # nonce (PEBBLE_WFE_NONCEREJECT=0) to have a stable test environment.\n        environ = os.environ.copy()\n        environ['PEBBLE_VA_NOSLEEP'] = '1'\n        environ['PEBBLE_WFE_NONCEREJECT'] = '0'\n        environ['PEBBLE_AUTHZREUSE'] = '100'\n        environ['PEBBLE_ALTERNATE_ROOTS'] = str(PEBBLE_ALTERNATE_ROOTS)\n\n        if self._dns_server:\n            dns_server = self._dns_server\n        else:\n            dns_server = '127.0.0.1:8053'\n            self._launch_process(\n                [challtestsrv_path, '-management', ':{0}'.format(CHALLTESTSRV_PORT),\n                 '-defaultIPv6', '\"\"', '-defaultIPv4', '127.0.0.1', '-http01', '\"\"',\n                 '-tlsalpn01', '\"\"', '-https01', '\"\"'])\n\n        self._launch_process(\n            [pebble_path, '-config', pebble_config_path, '-dnsserver', dns_server, '-strict'],\n            env=environ)\n\n        self._launch_process([sys.executable, pebble_ocsp_server.__file__])\n\n        # Wait for the ACME CA server to be up.\n        print('=> Waiting for pebble instance to respond...')\n        misc.check_until_timeout(self.acme_xdist['directory_url'])\n\n        print('=> Finished pebble instance deployment.')\n\n    def _prepare_http_proxy(self) -> None:\n        \"\"\"Configure and launch an HTTP proxy\"\"\"\n        print(f'=> Configuring the HTTP proxy on port {self._http_01_port}...')\n        http_port_map = cast(dict[str, int], self.acme_xdist['http_port'])\n        mapping = {r'.+\\.{0}\\.wtf'.format(node): 'http://127.0.0.1:{0}'.format(port)\n                   for node, port in http_port_map.items()}\n\n        # Each worker gets a specific loopback IP address. All traffic to that IP address gets\n        # routed to that worker's http_port.\n        local_ip_map = cast(dict[str, str], self.acme_xdist['local_ip'])\n        for node, local_ip in local_ip_map.items():\n            mapping[r'{0}:.+'.format(local_ip)] = \\\n                'http://{0}:{1}'.format(local_ip, http_port_map[node])\n\n        command = [sys.executable, proxy.__file__, str(self._http_01_port), json.dumps(mapping)]\n        self._launch_process(command)\n        print('=> Finished configuring the HTTP proxy.')\n\n    def _launch_process(self, command: list[str], cwd: str = os.getcwd(),\n                        env: Optional[Mapping[str, str]] = None,\n                        force_stderr: bool = False) -> subprocess.Popen[bytes]:\n        \"\"\"Launch silently a subprocess OS command\"\"\"\n        if not env:\n            env = os.environ\n        stdout = sys.stderr if force_stderr else self._stdout\n        # pylint: disable=consider-using-with\n        process = subprocess.Popen(\n            command, stdout=stdout, stderr=subprocess.STDOUT, cwd=cwd, env=env\n        )\n        self._processes.append(process)\n        return process\n\n\ndef main() -> None:\n    # pylint: disable=missing-function-docstring\n    parser = argparse.ArgumentParser(\n        description='CLI tool to start a local instance of Pebble CA server.')\n    parser.add_argument('--dns-server', '-d',\n                        help='specify the DNS server as `IP:PORT` to use by '\n                             'Pebble; if not specified, a local mock DNS server will be used to '\n                             'resolve domains to localhost.')\n    parser.add_argument('--http-01-port', type=int, default=DEFAULT_HTTP_01_PORT,\n                        help='specify the port to use for http-01 validation; '\n                             'this is currently only supported for Pebble.')\n    args = parser.parse_args()\n\n    acme_server = ACMEServer(\n        [], http_proxy=False, stdout=True, dns_server=args.dns_server,\n        http_01_port=args.http_01_port,\n    )\n\n    try:\n        with acme_server as acme_xdist:\n            print('--> Instance of {0} is running, directory URL is {0}'\n                  .format(acme_xdist['directory_url']))\n            print('--> Press CTRL+C to stop the ACME server.')\n\n            while True:\n                time.sleep(3600)\n    except KeyboardInterrupt:\n        pass\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/certbot_call.py",
    "content": "#!/usr/bin/env python\n\"\"\"Module to call certbot in test mode\"\"\"\n\nimport os\nimport subprocess\nimport sys\nfrom typing import Optional\n\nimport certbot_integration_tests\nfrom certbot_integration_tests.utils.constants import DEFAULT_HTTP_01_PORT\nfrom certbot_integration_tests.utils.constants import HTTPS_PORT\nfrom certbot_integration_tests.utils.constants import PEBBLE_DIRECTORY_URL\n\n\ndef certbot_test(certbot_args: list[str], directory_url: Optional[str], http_01_port: int,\n                 https_port: int, config_dir: str, workspace: str,\n                 force_renew: bool = True) -> tuple[str, str]:\n    \"\"\"\n    Invoke the certbot executable available in PATH in a test context for the given args.\n    The test context consists in running certbot in debug mode, with various flags suitable\n    for tests (eg. no ssl check, customizable ACME challenge ports and config directory ...).\n    This command captures both stdout and stderr and returns it to the caller.\n    :param list certbot_args: the arguments to pass to the certbot executable\n    :param str directory_url: URL of the ACME directory server to use\n    :param int http_01_port: port for the HTTP-01 challenges\n    :param int https_port: port Nginx expects will serve HTTPS\n    :param str config_dir: certbot configuration directory to use\n    :param str workspace: certbot current directory to use\n    :param bool force_renew: set False to not force renew existing certificates (default: True)\n    :return: stdout and stderr as strings\n    :rtype: `tuple` of `str`\n    \"\"\"\n    command, env = _prepare_args_env(certbot_args, directory_url, http_01_port, https_port,\n                                     config_dir, workspace, force_renew)\n\n    proc = subprocess.run(command, stdout=subprocess.PIPE,\n                          stderr=subprocess.PIPE, check=False, universal_newlines=True,\n                          cwd=workspace, env=env)\n    print('--> Certbot log output was:')\n    print(proc.stderr)\n    proc.check_returncode()\n    return proc.stdout, proc.stderr\n\n\ndef _prepare_environ(workspace: str) -> dict[str, str]:\n    # pylint: disable=missing-function-docstring\n\n    new_environ = os.environ.copy()\n    new_environ['TMPDIR'] = workspace\n\n    # So, pytest is nice, and a little too nice for our usage.\n    # In order to help user to call seamlessly any piece of python code without requiring to\n    # install it as a full-fledged Python package for instance, it may inject the path\n    # to the test files into the PYTHONPATH. This allows the python interpreter to import\n    # as modules any python file available at this path.\n    # See https://docs.pytest.org/en/3.2.5/pythonpath.html for the explanation and description.\n    # However this behavior is not good in integration tests, in particular the nginx oldest ones.\n    # Indeed during these kind of tests certbot is installed as a transitive dependency to\n    # certbot-nginx. Here is the trick: this certbot version is not necessarily the same as\n    # the certbot codebase lying in current working directory. For instance in oldest tests\n    # certbot==0.36.0 may be installed while the codebase corresponds to certbot==0.37.0.dev0.\n    # Then during a pytest run, PYTHONPATH contains the path to the Certbot codebase, so invoking\n    # certbot will import the modules from the codebase (0.37.0.dev0), not from the\n    # required/installed version (0.36.0).\n    # This will lead to funny and totally incomprehensible errors. To avoid that, we ensure that\n    # if PYTHONPATH is set, it does not contain the path to the root of the codebase.\n    if new_environ.get('PYTHONPATH'):\n        # certbot_integration_tests.__file__ is:\n        # '/path/to/certbot/certbot-ci/src/certbot_integration_tests/__init__.pyc'\n        # ... and we want '/path/to/certbot'\n        certbot_root = os.path.dirname(os.path.dirname(\n            os.path.dirname(certbot_integration_tests.__file__))\n        )\n        python_paths = [\n            path for path in new_environ['PYTHONPATH'].split(':')\n            if path != certbot_root\n        ]\n        new_environ['PYTHONPATH'] = ':'.join(python_paths)\n\n    return new_environ\n\n\ndef _prepare_args_env(certbot_args: list[str], directory_url: Optional[str], http_01_port: int,\n                      https_port: int, config_dir: str, workspace: str,\n                      force_renew: bool) -> tuple[list[str], dict[str, str]]:\n\n    new_environ = _prepare_environ(workspace)\n    additional_args = ['--no-random-sleep-on-renew']\n    if force_renew:\n        additional_args.append('--renew-by-default')\n\n    if directory_url:\n        additional_args.extend(['--server', directory_url])\n\n\n    command = [\n        'certbot',\n        '--no-verify-ssl',\n        '--http-01-port', str(http_01_port),\n        '--https-port', str(https_port),\n        '--config-dir', config_dir,\n        '--work-dir', os.path.join(workspace, 'work'),\n        '--logs-dir', os.path.join(workspace, 'logs'),\n        '--non-interactive',\n        '--no-redirect',\n        '--agree-tos',\n        '--register-unsafely-without-email',\n        '--debug',\n        '-vv'\n    ]\n\n    command.extend(certbot_args)\n    command.extend(additional_args)\n\n    print('--> Invoke command:\\n=====\\n{0}\\n====='.format(subprocess.list2cmdline(command)))\n\n    return command, new_environ\n\n\ndef main() -> None:\n    # pylint: disable=missing-function-docstring\n    args = sys.argv[1:]\n\n    # Default config is pebble\n    directory_url = os.environ.get('SERVER', PEBBLE_DIRECTORY_URL)\n    http_01_port = int(os.environ.get('HTTP_01_PORT', DEFAULT_HTTP_01_PORT))\n    https_port = int(os.environ.get('HTTPS_PORT', HTTPS_PORT))\n\n    # Execution of certbot in a self-contained workspace\n    workspace = os.environ.get('WORKSPACE', os.path.join(os.getcwd(), '.certbot_test_workspace'))\n    if not os.path.exists(workspace):\n        print('--> Creating a workspace for certbot_test: {0}'.format(workspace))\n        os.mkdir(workspace)\n    else:\n        print('--> Using an existing workspace for certbot_test: {0}'.format(workspace))\n    config_dir = os.path.join(workspace, 'conf')\n\n    # Invoke certbot in test mode, without capturing output so users see directly the outcome.\n    command, env = _prepare_args_env(args, directory_url, http_01_port, https_port,\n                                     config_dir, workspace, False)\n    subprocess.check_call(command, universal_newlines=True, cwd=workspace, env=env)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/constants.py",
    "content": "\"\"\"Some useful constants to use throughout certbot-ci integration tests\"\"\"\nDEFAULT_HTTP_01_PORT = 5002\nHTTPS_PORT = 5001\nCHALLTESTSRV_PORT = 8055\nPEBBLE_DIRECTORY_URL = 'https://localhost:14000/dir'\nPEBBLE_MANAGEMENT_URL = 'https://localhost:15000'\nPEBBLE_CHALLTESTSRV_URL = f'http://localhost:{CHALLTESTSRV_PORT}'\nMOCK_OCSP_SERVER_PORT = 4002\nPEBBLE_ALTERNATE_ROOTS = 2\nMAX_SUBPROCESS_WAIT = 120\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/dns_server.py",
    "content": "#!/usr/bin/env python\n\"\"\"Module to setup an RFC2136-capable DNS server\"\"\"\nimport importlib.resources\nimport os\nimport os.path\nimport shutil\nimport socket\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom types import TracebackType\nfrom typing import Any\nfrom typing import Optional\n\nfrom certbot_integration_tests.utils import constants\n\nBIND_DOCKER_IMAGE = \"internetsystemsconsortium/bind9:9.20\"\nBIND_BIND_ADDRESS = (\"127.0.0.1\", 45953)\n\n# A TCP DNS message which is a query for '. CH A' transaction ID 0xcb37. This is used\n# by _wait_until_ready to check that BIND is responding without depending on dnspython.\nBIND_TEST_QUERY = bytearray.fromhex(\"0011cb37000000010000000000000000010003\")\n\n\nclass DNSServer:\n    \"\"\"\n    DNSServer configures and handles the lifetime of an RFC2136-capable server.\n    DNServer provides access to the dns_xdist parameter, listing the address and port\n    to use for each pytest node.\n\n    At this time, DNSServer should only be used with a single node, but may be expanded in\n    future to support parallelization (https://github.com/certbot/certbot/issues/8455).\n    \"\"\"\n\n    def __init__(self, unused_nodes: list[str], show_output: bool = False) -> None:\n        \"\"\"\n        Create an DNSServer instance.\n        :param list nodes: list of node names that will be setup by pytest xdist\n        :param bool show_output: if True, print the output of the DNS server\n        \"\"\"\n\n        self.bind_root = tempfile.mkdtemp()\n\n        self.process: Optional[subprocess.Popen[bytes]] = None\n\n        self.dns_xdist = {\"address\": BIND_BIND_ADDRESS[0], \"port\": BIND_BIND_ADDRESS[1]}\n\n        # Unfortunately the BIND9 image forces everything to stderr with -g and we can't\n        # modify the verbosity.\n        # pylint: disable=consider-using-with\n        self._output = sys.stderr if show_output else open(os.devnull, \"w\")\n\n    def start(self) -> None:\n        \"\"\"Start the DNS server\"\"\"\n        try:\n            self._configure_bind()\n            self._start_bind()\n        except:\n            self.stop()\n            raise\n\n    def stop(self) -> None:\n        \"\"\"Stop the DNS server, and clean its resources\"\"\"\n        if self.process:\n            try:\n                self.process.terminate()\n                self.process.wait(constants.MAX_SUBPROCESS_WAIT)\n            except BaseException as e:  # pylint: disable=broad-except\n                print(\"BIND9 did not stop cleanly: {}\".format(e), file=sys.stderr)\n\n        shutil.rmtree(self.bind_root, ignore_errors=True)\n\n        if self._output != sys.stderr:\n            self._output.close()\n\n    def _configure_bind(self) -> None:\n        \"\"\"Configure the BIND9 server based on the prebaked configuration\"\"\"\n        ref = importlib.resources.files(\"certbot_integration_tests\") / \"assets\" / \"bind-config\"\n        with importlib.resources.as_file(ref) as path:\n            for directory in (\"conf\", \"zones\"):\n                shutil.copytree(\n                    os.path.join(path, directory), os.path.join(self.bind_root, directory)\n                )\n\n    def _start_bind(self) -> None:\n        \"\"\"Launch the BIND9 server as a Docker container\"\"\"\n        addr_str = \"{}:{}\".format(BIND_BIND_ADDRESS[0], BIND_BIND_ADDRESS[1])\n        # pylint: disable=consider-using-with\n        self.process = subprocess.Popen(\n            [\n                \"docker\",\n                \"run\",\n                \"--rm\",\n                \"-p\",\n                \"{}:53/udp\".format(addr_str),\n                \"-p\",\n                \"{}:53/tcp\".format(addr_str),\n                \"-v\",\n                \"{}/conf:/etc/bind\".format(self.bind_root),\n                \"-v\",\n                \"{}/zones:/var/lib/bind\".format(self.bind_root),\n                BIND_DOCKER_IMAGE,\n            ],\n            stdout=self._output,\n            stderr=self._output,\n        )\n\n        if self.process.poll():\n            raise ValueError(\"BIND9 server stopped unexpectedly\")\n\n        try:\n            self._wait_until_ready()\n        except:\n            # The container might be running even if we think it isn't\n            self.stop()\n            raise\n\n    def _wait_until_ready(self, attempts: int = 30) -> None:\n        \"\"\"\n        Polls the DNS server over TCP until it gets a response, or until\n        it runs out of attempts and raises a ValueError.\n        The DNS response message must match the txn_id of the DNS query message,\n        but otherwise the contents are ignored.\n        :param int attempts: The number of attempts to make.\n        \"\"\"\n        if not self.process:\n            raise ValueError(\"DNS server has not been started. Please run start() first.\")\n\n        for _ in range(attempts):\n            if self.process.poll():\n                raise ValueError(\"BIND9 server stopped unexpectedly\")\n\n            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            sock.settimeout(5.0)\n            try:\n                sock.connect(BIND_BIND_ADDRESS)\n                sock.sendall(BIND_TEST_QUERY)\n                buf = sock.recv(1024)\n                # We should receive a DNS message with the same tx_id\n                if buf and len(buf) > 4 and buf[2:4] == BIND_TEST_QUERY[2:4]:\n                    return\n                # If we got a response but it wasn't the one we wanted, wait a little\n                time.sleep(1)\n            except:  # pylint: disable=bare-except\n                # If there was a network error, wait a little\n                time.sleep(1)\n            finally:\n                sock.close()\n\n        raise ValueError(\n            \"Gave up waiting for DNS server {} to respond\".format(BIND_BIND_ADDRESS)\n        )\n\n    def __start__(self) -> dict[str, Any]:\n        self.start()\n        return self.dns_xdist\n\n    def __exit__(self, exc_type: Optional[type[BaseException]], exc: Optional[BaseException],\n                 traceback: Optional[TracebackType]) -> None:\n        self.stop()\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/misc.py",
    "content": "\"\"\"\nMisc module contains stateless functions that could be used during pytest execution,\nor outside during setup/teardown of the integration tests environment.\n\"\"\"\nimport atexit\nimport contextlib\nimport errno\nimport functools\nimport http.server as SimpleHTTPServer\nimport importlib.resources\nimport json\nimport os\nimport re\nimport shutil\nimport socket\nimport socketserver\nimport stat\nimport sys\nimport tempfile\nimport threading\nimport time\nfrom typing import Generator\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import Union\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.asymmetric import rsa\nfrom cryptography.hazmat.primitives.serialization import Encoding\nfrom cryptography.hazmat.primitives.serialization import NoEncryption\nfrom cryptography.hazmat.primitives.serialization import PrivateFormat\nfrom cryptography.x509 import Certificate\nfrom cryptography.x509 import load_pem_x509_certificate\n\nimport requests\n\nfrom certbot_integration_tests.utils.constants import PEBBLE_ALTERNATE_ROOTS\nfrom certbot_integration_tests.utils.constants import PEBBLE_MANAGEMENT_URL\n\nRSA_KEY_TYPE = 'rsa'\nECDSA_KEY_TYPE = 'ecdsa'\n\n\ndef _suppress_x509_verification_warnings() -> None:\n    import urllib3\n    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n\ndef check_until_timeout(url: str, attempts: int = 30) -> None:\n    \"\"\"\n    Wait and block until given url responds with status 200, or raise an exception\n    after the specified number of attempts.\n    :param str url: the URL to test\n    :param int attempts: the number of times to try to connect to the URL\n    :raise ValueError: exception raised if unable to reach the URL\n    \"\"\"\n    _suppress_x509_verification_warnings()\n    for _ in range(attempts):\n        time.sleep(1)\n        try:\n            if requests.get(url, verify=False, timeout=10).status_code == 200:\n                return\n        except requests.exceptions.RequestException:\n            pass\n\n    raise ValueError('Error, url did not respond after {0} attempts: {1}'.format(attempts, url))\n\n\nclass GracefulTCPServer(socketserver.TCPServer):\n    \"\"\"\n    This subclass of TCPServer allows graceful reuse of an address that has\n    just been released by another instance of TCPServer.\n    \"\"\"\n    allow_reuse_address = True\n    # AF_INET is the default, but make it explicit. We don't want to conflict with\n    # test_ipv6_address_standalone in the integration tests, which listens on [::1]:5002\n    # (that is, AF_INET6).\n    address_family = socket.AF_INET\n\n\n@contextlib.contextmanager\ndef create_http_server(port: int) -> Generator[str, None, None]:\n    \"\"\"\n    Setup and start an HTTP server for the given TCP port.\n    This server stays active for the lifetime of the context, and is automatically\n    stopped with context exit, while its temporary webroot is deleted.\n    :param int port: the TCP port to use\n    :return str: the temporary webroot attached to this server\n    \"\"\"\n    with tempfile.TemporaryDirectory() as webroot:\n        # Setting the directory argument of SimpleHTTPRequestHandler causes\n        # files to be served from that directory.\n        handler = functools.partial(SimpleHTTPServer.SimpleHTTPRequestHandler, directory=webroot)\n        server = GracefulTCPServer(('', port), handler)\n        thread = threading.Thread(target=server.serve_forever)\n        thread.start()\n        try:\n            check_until_timeout('http://localhost:{0}/'.format(port))\n            yield webroot\n        finally:\n            server.shutdown()\n            thread.join()\n            server.server_close()\n\n\ndef list_renewal_hooks_dirs(config_dir: str) -> list[str]:\n    \"\"\"\n    Find and return paths of all hook directories for the given certbot config directory\n    :param str config_dir: path to the certbot config directory\n    :return str[]: list of path to the standard hooks directory for this certbot instance\n    \"\"\"\n    renewal_hooks_root = os.path.join(config_dir, 'renewal-hooks')\n    return [os.path.join(renewal_hooks_root, item) for item in ['pre', 'deploy', 'post']]\n\n\ndef generate_test_file_hooks(config_dir: str, hook_probe: str) -> None:\n    \"\"\"\n    Create a suite of certbot hook scripts and put them in the relevant hook directory\n    for the given certbot configuration directory. These scripts, when executed, will write\n    specific verbs in the given hook_probe file to allow asserting they have effectively\n    been executed. The deploy hook also checks that the renewal environment variables are set.\n    :param str config_dir: current certbot config directory\n    :param str hook_probe: path to the hook probe to test hook scripts execution\n    \"\"\"\n    file_manager = contextlib.ExitStack()\n    atexit.register(file_manager.close)\n    hook_path_ref = (importlib.resources.files('certbot_integration_tests').joinpath('assets')\n                     .joinpath('hook.py'))\n    hook_path = str(file_manager.enter_context(importlib.resources.as_file(hook_path_ref)))\n\n    for hook_dir in list_renewal_hooks_dirs(config_dir):\n        # We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of\n        # the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an\n        # optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not.\n        # So we pass through a try except pass for it. To be removed with dropped support on py27.\n        try:\n            os.makedirs(hook_dir)\n        except OSError as error:\n            if error.errno != errno.EEXIST:\n                raise\n\n        if os.name != 'nt':\n            entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.sh')\n            entrypoint_script = '''\\\n#!/usr/bin/env bash\nset -e\n\"{0}\" \"{1}\" \"{2}\" >> \"{3}\"\n'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)\n        else:\n            entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.ps1')\n            entrypoint_script = '''\\\n& \"{0}\" \"{1}\" \"{2}\" >> \"{3}\"\n            '''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)\n\n        with open(entrypoint_script_path, 'w') as file_h:\n            file_h.write(entrypoint_script)\n\n        os.chmod(entrypoint_script_path, os.stat(entrypoint_script_path).st_mode | stat.S_IEXEC)\n\n\n@contextlib.contextmanager\ndef manual_http_hooks(http_server_root: str) -> Generator[tuple[str, str], None, None]:\n    \"\"\"\n    Generate suitable http-01 hooks command for test purpose in the given HTTP\n    server webroot directory. These hooks command use temporary python scripts\n    that are deleted upon context exit.\n    :param str http_server_root: path to the HTTP server configured to serve http-01 challenges\n    :return (str, str): a tuple containing the authentication hook and cleanup hook commands\n    \"\"\"\n    tempdir = tempfile.mkdtemp()\n    try:\n        auth_script_path = os.path.join(tempdir, 'auth.py')\n        with open(auth_script_path, 'w') as file_h:\n            file_h.write('''\\\n#!/usr/bin/env python\nimport os\nchallenge_dir = os.path.join('{0}', '.well-known', 'acme-challenge')\nos.makedirs(challenge_dir)\nchallenge_file = os.path.join(challenge_dir, os.environ.get('CERTBOT_TOKEN'))\nwith open(challenge_file, 'w') as file_h:\n    file_h.write(os.environ.get('CERTBOT_VALIDATION'))\n'''.format(http_server_root.replace('\\\\', '\\\\\\\\')))\n        os.chmod(auth_script_path, 0o755)\n\n        cleanup_script_path = os.path.join(tempdir, 'cleanup.py')\n        with open(cleanup_script_path, 'w') as file_h:\n            file_h.write('''\\\n#!/usr/bin/env python\nimport os\nimport shutil\nwell_known = os.path.join('{0}', '.well-known')\nshutil.rmtree(well_known)\n'''.format(http_server_root.replace('\\\\', '\\\\\\\\')))\n        os.chmod(cleanup_script_path, 0o755)\n\n        yield ('{0} {1}'.format(sys.executable, auth_script_path),\n               '{0} {1}'.format(sys.executable, cleanup_script_path))\n    finally:\n        shutil.rmtree(tempdir)\n\n\ndef generate_csr(\n    domains: Iterable[str], key_path: str, csr_path: str, key_type: str = RSA_KEY_TYPE\n) -> None:\n    \"\"\"\n    Generate a private key, and a CSR for the given domains using this key.\n    :param domains: the domain names to include in the CSR\n    :type domains: `list` of `str`\n    :param str key_path: path to the private key that will be generated\n    :param str csr_path: path to the CSR that will be generated\n    :param str key_type: type of the key (misc.RSA_KEY_TYPE or misc.ECDSA_KEY_TYPE)\n    \"\"\"\n    key: Union[rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey]\n    if key_type == RSA_KEY_TYPE:\n        key = rsa.generate_private_key(public_exponent=65537, key_size=2048)\n    elif key_type == ECDSA_KEY_TYPE:\n        key = ec.generate_private_key(ec.SECP384R1())\n    else:\n        raise ValueError(\"Invalid key type: {0}\".format(key_type))\n\n    with open(key_path, \"wb\") as file_h:\n        file_h.write(\n            key.private_bytes(\n                Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()\n            )\n        )\n\n    csr = (\n        x509.CertificateSigningRequestBuilder()\n        .subject_name(x509.Name([]))\n        .add_extension(\n            x509.SubjectAlternativeName([x509.DNSName(d) for d in domains]),\n            critical=False,\n        )\n        .sign(key, hashes.SHA256())\n    )\n\n    with open(csr_path, \"wb\") as file_h:\n        file_h.write(csr.public_bytes(Encoding.DER))\n\n\ndef read_certificate(cert_path: str) -> Certificate:\n    \"\"\"\n    Load the certificate from the provided path and return the certificate object.\n    :param str cert_path: the path to the certificate\n    :returns: a cryptography.x509.Certificate object\n    \"\"\"\n    with open(cert_path, \"rb\") as file:\n        data = file.read()\n\n    return x509.load_pem_x509_certificate(data)\n\n\ndef load_sample_data_path(workspace: str) -> str:\n    \"\"\"\n    Load the certbot configuration example designed to make OCSP tests, and return its path\n    :param str workspace: current test workspace directory path\n    :returns: the path to the loaded sample data directory\n    :rtype: str\n    \"\"\"\n    original_ref = (importlib.resources.files('certbot_integration_tests').joinpath('assets')\n                    .joinpath('sample-config'))\n    with importlib.resources.as_file(original_ref) as original:\n        copied = os.path.join(workspace, 'sample-config')\n        shutil.copytree(original, copied, symlinks=True)\n\n    if os.name == 'nt':\n        # Fix the symlinks on Windows if GIT is not configured to create them upon checkout\n        for lineage in [\n            'a.encryption-example.com',\n            'b.encryption-example.com',\n            'c.encryption-example.com',\n        ]:\n            current_live = os.path.join(copied, 'live', lineage)\n            for name in os.listdir(current_live):\n                if name != 'README':\n                    current_file = os.path.join(current_live, name)\n                    if not os.path.islink(current_file):\n                        with open(current_file) as file_h:\n                            src = file_h.read()\n                        os.unlink(current_file)\n                        os.symlink(os.path.join(current_live, src), current_file)\n\n    return copied\n\n\ndef echo(keyword: str, path: Optional[str] = None) -> str:\n    \"\"\"\n    Generate a platform independent executable command\n    that echoes the given keyword into the given file.\n    :param keyword: the keyword to echo (must be a single keyword)\n    :param path: path to the file were keyword is echoed\n    :return: the executable command\n    \"\"\"\n    if not re.match(r'^\\w+$', keyword):\n        raise ValueError('Error, keyword `{0}` is not a single keyword.'\n                         .format(keyword))\n    return '{0} -c \"print(\\'{1}\\')\"{2}'.format(\n        os.path.basename(sys.executable), keyword, ' >> \"{0}\"'.format(path) if path else '')\n\n\ndef get_acme_issuers() -> list[Certificate]:\n    \"\"\"Gets the list of one or more issuer certificates from the ACME server used by the\n    context.\n    :param context: the testing context.\n    :return: the `list of x509.Certificate` representing the list of issuers.\n    \"\"\"\n    _suppress_x509_verification_warnings()\n\n    issuers = []\n    for i in range(PEBBLE_ALTERNATE_ROOTS + 1):\n        request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/{}'.format(i),\n                               verify=False,\n                               timeout=10)\n        issuers.append(load_pem_x509_certificate(request.content, default_backend()))\n\n    return issuers\n\ndef set_ari_response(certificate_pem: str, response_json: str) -> None:\n    \"\"\"POST to an endpoint on the Pebble server setting the ARI response\n    for the given certificate.\"\"\"\n    set_renewal_info_body = json.dumps(\n        {\n            'certificate': certificate_pem,\n            'ariResponse': response_json,\n        })\n\n    _suppress_x509_verification_warnings()\n    url = PEBBLE_MANAGEMENT_URL + '/set-renewal-info/'\n    print(f'sending to {url}: {set_renewal_info_body}')\n    resp = requests.post(url, verify=False, timeout=10, data=set_renewal_info_body)\n    if resp.status_code != 200:\n        print(f'setting renewal info: {resp.status_code} {resp.text}')\n    assert resp.status_code == 200\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/pebble_artifacts.py",
    "content": "# pylint: disable=missing-module-docstring\nimport atexit\nimport importlib.resources\nimport io\nimport json\nimport os\nimport platform\nimport stat\nimport zipfile\nfrom contextlib import ExitStack\nfrom typing import Optional\n\nimport requests\n\nfrom certbot_integration_tests.utils.constants import DEFAULT_HTTP_01_PORT\nfrom certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT\n\nPEBBLE_VERSION = 'v2.8.0'\n\n\ndef fetch(workspace: str, http_01_port: int = DEFAULT_HTTP_01_PORT) -> tuple[str, str, str]:\n    # pylint: disable=missing-function-docstring\n    file_manager = ExitStack()\n    atexit.register(file_manager.close)\n    pebble_path_ref = importlib.resources.files('certbot_integration_tests') / 'assets'\n    assets_path = str(file_manager.enter_context(importlib.resources.as_file(pebble_path_ref)))\n\n    pebble_path = _fetch_asset('pebble', assets_path)\n    challtestsrv_path = _fetch_asset('pebble-challtestsrv', assets_path)\n    pebble_config_path = _build_pebble_config(workspace, http_01_port, assets_path)\n\n    return pebble_path, challtestsrv_path, pebble_config_path\n\n\ndef _fetch_asset(asset: str, assets_path: str) -> str:\n    base_url = 'https://github.com/letsencrypt/pebble/releases/download'\n    os_type, architecture = _get_validated_os_and_architecture()\n    asset_path = os.path.join(assets_path, f'{asset}_{PEBBLE_VERSION}_{os_type}_{architecture}')\n    if not os.path.exists(asset_path):\n        asset_url = f'{base_url}/{PEBBLE_VERSION}/{asset}-{os_type}-{architecture}.zip'\n        response = requests.get(asset_url, timeout=30)\n        response.raise_for_status()\n        asset_data = _unzip_asset(response.content, asset)\n        if asset_data is None:\n            raise ValueError(f\"zipfile {asset_url} didn't contain file {asset}\")\n        with open(asset_path, 'wb') as file_h:\n            file_h.write(asset_data)\n    os.chmod(asset_path, os.stat(asset_path).st_mode | stat.S_IEXEC)\n\n    return asset_path\n\n\ndef _get_validated_os_and_architecture() -> tuple[str, str]:\n    os_type = platform.system().lower()\n    if os_type not in ('darwin', 'linux'):\n        raise ValueError(f'this code has not been tested on {os_type} systems')\n\n    architecture = platform.machine()\n    if architecture in ('amd64', 'x86_64'):\n        architecture = 'amd64'\n    elif architecture in ('aarch64' 'arm64'):\n        architecture = 'arm64'\n    else:\n        raise ValueError(f'this code has not been tested on {architecture} systems')\n\n    return os_type, architecture\n\n\ndef _unzip_asset(zipped_data: bytes, asset_name: str) -> Optional[bytes]:\n    with zipfile.ZipFile(io.BytesIO(zipped_data)) as zip_file:\n        for entry in zip_file.filelist:\n            if not entry.is_dir() and entry.filename.endswith(asset_name):\n                return zip_file.read(entry)\n    return None\n\n\ndef _build_pebble_config(workspace: str, http_01_port: int, assets_path: str) -> str:\n    config_path = os.path.join(workspace, 'pebble-config.json')\n    with open(config_path, 'w') as file_h:\n        file_h.write(json.dumps({\n            'pebble': {\n                'listenAddress': '0.0.0.0:14000',\n                'managementListenAddress': '0.0.0.0:15000',\n                'certificate': os.path.join(assets_path, 'cert.pem'),\n                'privateKey': os.path.join(assets_path, 'key.pem'),\n                'httpPort': http_01_port,\n                'tlsPort': 5001,\n                'ocspResponderURL': 'http://127.0.0.1:{0}'.format(MOCK_OCSP_SERVER_PORT),\n            },\n        }))\n\n    return config_path\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/pebble_ocsp_server.py",
    "content": "#!/usr/bin/env python\n\"\"\"\nThis runnable module interfaces itself with the Pebble management interface in order\nto serve a mock OCSP responder during integration tests against Pebble.\n\"\"\"\nimport datetime\nimport http.server as BaseHTTPServer\nimport re\nfrom typing import cast\nfrom typing import Union\n\nfrom cryptography import x509\nfrom cryptography.hazmat.backends import default_backend\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives import serialization\nfrom cryptography.hazmat.primitives.asymmetric.ec import EllipticCurvePrivateKey\nfrom cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey\nfrom cryptography.x509 import ocsp\nfrom dateutil import parser\nimport requests\n\nfrom certbot_integration_tests.utils.constants import MOCK_OCSP_SERVER_PORT\nfrom certbot_integration_tests.utils.constants import PEBBLE_MANAGEMENT_URL\nfrom certbot_integration_tests.utils.misc import GracefulTCPServer\n\n\nclass _ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):\n    # pylint: disable=missing-function-docstring\n    def do_POST(self) -> None:\n        request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediate-keys/0',\n                               verify=False, timeout=10)\n        issuer_key = cast(\n            Union[RSAPrivateKey, EllipticCurvePrivateKey],\n            serialization.load_pem_private_key(request.content, None, default_backend()))\n\n        request = requests.get(PEBBLE_MANAGEMENT_URL + '/intermediates/0',\n                               verify=False, timeout=10)\n        issuer_cert = x509.load_pem_x509_certificate(request.content, default_backend())\n\n        raw_content_len = self.headers.get('Content-Length')\n        assert isinstance(raw_content_len, str)\n        content_len = int(raw_content_len)\n\n        ocsp_request = ocsp.load_der_ocsp_request(self.rfile.read(content_len))\n        response = requests.get('{0}/cert-status-by-serial/{1}'.format(\n            PEBBLE_MANAGEMENT_URL, str(hex(ocsp_request.serial_number)).replace('0x', '')),\n            verify=False, timeout=10\n        )\n\n        if not response.ok:\n            ocsp_response = ocsp.OCSPResponseBuilder.build_unsuccessful(\n                ocsp.OCSPResponseStatus.UNAUTHORIZED\n            )\n        else:\n            data = response.json()\n\n            now = datetime.datetime.now(datetime.timezone.utc)\n            cert = x509.load_pem_x509_certificate(data['Certificate'].encode(), default_backend())\n            if data['Status'] != 'Revoked':\n                ocsp_status = ocsp.OCSPCertStatus.GOOD\n                revocation_time = None\n                revocation_reason = None\n            else:\n                ocsp_status = ocsp.OCSPCertStatus.REVOKED\n                revocation_reason = x509.ReasonFlags.unspecified\n                # \"... +0000 UTC\" => \"+0000\"\n                revoked_at = re.sub(r'( \\+\\d{4}).*$', r'\\1', data['RevokedAt'])\n                revocation_time = parser.parse(revoked_at)\n\n            ocsp_response = ocsp.OCSPResponseBuilder().add_response(\n                cert=cert, issuer=issuer_cert, algorithm=hashes.SHA1(),\n                cert_status=ocsp_status,\n                this_update=now, next_update=now + datetime.timedelta(hours=1),\n                revocation_time=revocation_time, revocation_reason=revocation_reason\n            ).responder_id(\n                ocsp.OCSPResponderEncoding.NAME, issuer_cert\n            ).sign(issuer_key, hashes.SHA256())\n\n        self.send_response(200)\n        self.end_headers()\n        self.wfile.write(ocsp_response.public_bytes(serialization.Encoding.DER))\n\n\nif __name__ == '__main__':\n    try:\n        GracefulTCPServer(('', MOCK_OCSP_SERVER_PORT), _ProxyHandler).serve_forever()\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "certbot-ci/src/certbot_integration_tests/utils/proxy.py",
    "content": "#!/usr/bin/env python\n# pylint: disable=missing-module-docstring\n#\n# Note: If you're having trouble with this module, there was some discussion\n# about how it could be ripped out entirely at\n# https://github.com/certbot/certbot/pull/10495#discussion_r2699618989 that you\n# may want to read.\n\nimport http.server as BaseHTTPServer\nimport json\nimport re\nimport sys\nfrom typing import Mapping\n\nimport requests\n\nfrom certbot_integration_tests.utils.misc import GracefulTCPServer\n\ndef _create_proxy(mapping: Mapping[str, str]) -> type[BaseHTTPServer.BaseHTTPRequestHandler]:\n    # pylint: disable=missing-function-docstring\n    class ProxyHandler(BaseHTTPServer.BaseHTTPRequestHandler):\n        # pylint: disable=missing-class-docstring\n        def do_GET(self) -> None:\n            headers = {key.lower(): value for key, value in self.headers.items()}\n            host = headers['host']\n            for pattern, backend in mapping.items():\n                if re.match(pattern, host):\n                    response = requests.get(backend + self.path, headers=headers, timeout=10)\n\n                    self.send_response(response.status_code)\n                    for key, value in response.headers.items():\n                        self.send_header(key, value)\n                    self.end_headers()\n                    self.wfile.write(response.content)\n                    return\n\n            # We should never hit this if the tests are written correctly, but if we do, this may\n            # be helpful debugging output.\n            print(f\"proxy.py: do_GET for {host}: No backend\")\n            self.send_response(200, \"No backend\")\n            self.end_headers()\n            self.wfile.write(bytes(f\"No backend found for {host}\\n\", 'utf-8'))\n\n    return ProxyHandler\n\n\nif __name__ == '__main__':\n    http_port = int(sys.argv[1])\n    port_mapping = json.loads(sys.argv[2])\n    httpd = GracefulTCPServer(('', http_port), _create_proxy(port_mapping))\n    try:\n        httpd.serve_forever()\n    except KeyboardInterrupt:\n        pass\n"
  },
  {
    "path": "certbot-ci/src/snap_integration_tests/__init__.py",
    "content": ""
  },
  {
    "path": "certbot-ci/src/snap_integration_tests/conftest.py",
    "content": "# type: ignore\n\"\"\"\nGeneral conftest for pytest execution of all integration tests lying\nin the snap_installer_integration tests package.\nAs stated by pytest documentation, conftest module is used to set on\nfor a directory a specific configuration using built-in pytest hooks.\n\nSee https://docs.pytest.org/en/latest/reference.html#hook-reference\n\"\"\"\nimport glob\nimport os\n\n\ndef pytest_addoption(parser):\n    \"\"\"\n    Standard pytest hook to add options to the pytest parser.\n    :param parser: current pytest parser that will be used on the CLI\n    \"\"\"\n    parser.addoption('--snap-folder', required=True,\n                     help='set the folder path where snaps to test are located')\n    parser.addoption('--snap-arch', default='amd64',\n                     help='set the architecture do test (default: amd64)')\n    parser.addoption('--allow-persistent-changes', action='store_true',\n                     help='needs to be set, and confirm that the test will make persistent '\n                          'changes on this machine')\n\n\ndef pytest_configure(config):\n    \"\"\"\n    Standard pytest hook used to add a configuration logic for each node of a pytest run.\n    :param config: the current pytest configuration\n    \"\"\"\n    if not config.option.allow_persistent_changes:\n        raise RuntimeError('This integration test would install the Certbot snap on your machine. '\n                           'Please run it again with the `--allow-persistent-changes` flag set '\n                           'to acknowledge.')\n\n\ndef pytest_generate_tests(metafunc):\n    \"\"\"\n    Generate (multiple) parametrized calls to a test function.\n    \"\"\"\n    if \"dns_snap_path\" in metafunc.fixturenames:\n        snap_arch = metafunc.config.getoption('snap_arch')\n        snap_folder = metafunc.config.getoption('snap_folder')\n        snap_dns_path_list = glob.glob(os.path.join(snap_folder,\n                                                    'certbot-dns-*_{0}.snap'.format(snap_arch)))\n        metafunc.parametrize(\"dns_snap_path\", snap_dns_path_list)\n"
  },
  {
    "path": "certbot-ci/src/snap_integration_tests/dns_tests/__init__.py",
    "content": ""
  },
  {
    "path": "certbot-ci/src/snap_integration_tests/dns_tests/test_main.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Module executing integration tests against certbot snap.\"\"\"\nimport glob\nimport os\nimport re\nimport subprocess\nfrom typing import Generator\n\nimport pytest\n\n\n@pytest.fixture(autouse=True, scope=\"module\")\ndef install_certbot_snap(request: pytest.FixtureRequest) -> Generator[None, None, None]:\n    \"\"\"Fixture ensuring the certbot snap is installed before each test.\"\"\"\n    with pytest.raises(Exception):\n        subprocess.check_call(['certbot', '--version'])\n    try:\n        snap_folder = request.config.getoption(\"snap_folder\")\n        snap_arch = request.config.getoption(\"snap_arch\")\n        snap_path = glob.glob(os.path.join(snap_folder, 'certbot_*_{0}.snap'.format(snap_arch)))[0]\n        subprocess.check_call(['snap', 'install', '--classic', '--dangerous', snap_path])\n        subprocess.check_call(['certbot', '--version'])\n        yield\n    finally:\n        subprocess.call(['snap', 'remove', 'certbot'])\n\n\ndef test_dns_plugin_install(dns_snap_path: str) -> None:\n    \"\"\"\n    Test that each DNS plugin Certbot snap can be installed\n    and is usable with the Certbot snap.\n    \"\"\"\n    match = re.match(r'^certbot-(dns-\\w+)_.*\\.snap$', os.path.basename(dns_snap_path))\n    assert match\n    plugin_name = match.group(1)\n    snap_name = 'certbot-{0}'.format(plugin_name)\n    assert plugin_name not in subprocess.check_output(['certbot', 'plugins', '--prepare'],\n                                                      universal_newlines=True)\n\n    try:\n        subprocess.check_call(['snap', 'install', '--dangerous', dns_snap_path])\n        subprocess.check_call(['snap', 'set', 'certbot', 'trust-plugin-with-root=ok'])\n        subprocess.check_call(['snap', 'connect', 'certbot:plugin', snap_name])\n\n        assert plugin_name in subprocess.check_output(['certbot', 'plugins', '--prepare'],\n                                                      universal_newlines=True)\n        subprocess.check_call(['snap', 'connect', snap_name + ':certbot-metadata',\n            'certbot:certbot-metadata'])\n        subprocess.check_call(['snap', 'install', '--dangerous', dns_snap_path])\n    finally:\n        subprocess.call(['snap', 'remove', plugin_name])\n"
  },
  {
    "path": "certbot-ci/src/snap_integration_tests/py.typed",
    "content": ""
  },
  {
    "path": "certbot-compatibility-test/Dockerfile",
    "content": "FROM docker.io/python:3.13-bookworm\nLABEL org.opencontainers.image.authors=\"certbot-dev@eff.org\"\n\n# This does not include the dependencies needed to build cryptography. See\n# https://cryptography.io/en/latest/installation/#building-cryptography-on-linux\nRUN apt-get update && \\\n    apt install python3-venv libaugeas-dev -y\n\nWORKDIR /opt/certbot/src\n\n# We copy all contents of the build directory to allow us to easily use\n# things like tools/venv.py which expects all of our packages to be available.\nCOPY . .\n\nRUN tools/venv.py\nENV PATH=/opt/certbot/src/venv/bin:$PATH\n\n# install in editable mode (-e) to save space: it's not possible to\n# \"rm -rf /opt/certbot/src\" (it's stays in the underlying image);\n# this might also help in debugging: you can \"docker run --entrypoint\n# bash\" and investigate, apply patches, etc.\n\nWORKDIR /opt/certbot/src/certbot-compatibility-test/src/certbot_compatibility_test/testdata\n"
  },
  {
    "path": "certbot-compatibility-test/Dockerfile-apache",
    "content": "FROM certbot-compatibility-test\nLABEL org.opencontainers.image.authors=\"certbot-dev@eff.org\"\n\nRUN apt-get install apache2 -y\n\nENTRYPOINT [ \"certbot-compatibility-test\", \"-p\", \"apache\" ]\n"
  },
  {
    "path": "certbot-compatibility-test/Dockerfile-nginx",
    "content": "FROM certbot-compatibility-test\nLABEL org.opencontainers.image.authors=\"certbot-dev@eff.org\"\n\nRUN apt-get install nginx -y\n\nENTRYPOINT [ \"certbot-compatibility-test\", \"-p\", \"nginx\" ]\n"
  },
  {
    "path": "certbot-compatibility-test/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-compatibility-test/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\ninclude src/certbot_compatibility_test/configurators/apache/a2enmod.sh\ninclude src/certbot_compatibility_test/configurators/apache/a2dismod.sh\ninclude src/certbot_compatibility_test/configurators/apache/Dockerfile\nrecursive-include src/certbot_compatibility_test/testdata *\n"
  },
  {
    "path": "certbot-compatibility-test/README.rst",
    "content": "Compatibility tests for Certbot\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/README",
    "content": "Eventually there will also be a compatibility test here like the Apache one.\n\nRight now, this is data for the roundtrip test (checking that the parser\ncan parse each file and that the reserialized config file it generates is\nidentical to the original).\n\nIf run in a virtualenv or otherwise so that certbot_nginx can be imported,\nthe roundtrip test can run as\n\npython roundtrip.py nginx-roundtrip-testdata\n\nIt gives exit status 0 for success and 1 if at least one parse or roundtrip\nfailure occurred.\n\n\nThe directory nginx-roundtrip-testdata includes some config files that were\ncontributed to our project as well as most of the configs linked from\n\nhttps://www.nginx.com/resources/wiki/start/\n\nSome exceptions that were skipped are\n\nhttps://www.nginx.com/resources/wiki/start/topics/recipes/moinmoin/\nhttps://www.nginx.com/resources/wiki/start/topics/examples/SSL-Offloader/ (not much nginx configuration)\nhttps://www.nginx.com/resources/wiki/start/topics/examples/xsendfile/ (likewise)\nhttps://www.nginx.com/resources/wiki/start/topics/examples/x-accel/\nhttps://www.nginx.com/resources/wiki/start/topics/examples/fcgiwrap/\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10033",
    "content": "upstream django_server_random18709.example.org {\n    server unix:/srv/http/random22194/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random18709.example.org;\n\n    location /media/ {\n        alias /srv/http/random22194/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random22194/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random18709.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random22194/live/access.log combined_plus;\n    error_log  /var/log/nginx/random22194/live/error.log;\n}\n\nserver {\n    server_name www.random18709.example.org;\n    server_name random24607.example.org www.random24607.example.org;\n    return 301 http://random18709.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10571",
    "content": "upstream django_server_random1413.example.org {\n    server unix:/srv/http/random25151/live/website.sock;\n}\n\nserver {\n    listen 443;\n    server_name www.random25266.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random25266.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random25266.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random25151/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random25151/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1413.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random25151/live/access.log combined_plus;\n    error_log  /var/log/nginx/random25151/live/error.log;\n}\n\n\nserver {\n    listen 443;\n    server_name random1413.example.org www.random1413.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random1413.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random1413.example.org.key;\n\n    location / {\n        return 301 https://www.random25266.example.org$request_uri;\n    }\n}\n\nserver {\n    listen 443;\n    server_name random25266.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random25266.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random25266.example.org.key;\n\n    location / {\n        return 301 https://www.random25266.example.org$request_uri;\n    }\n}\n\nserver {\n    listen 80;\n    server_name random1413.example.org www.random1413.example.org;\n    server_name random25266.example.org www.random25266.example.org;\n    server_name random26791.example.org www.random26791.example.org;\n\n    location / {\n        return 301 https://www.random25266.example.org$request_uri;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10591",
    "content": "upstream django_server_random11921.example.org {\n    server unix:/srv/http/random9726/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random11921.example.org www.random11921.example.org;\n\n    if ($host != 'random11921.example.org') {\n        rewrite  ^/(.*)$  http://random11921.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random9726/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random9726/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random11921.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n\n        error_page   502 503 504  /50x.html;\n    }\n\n    location /50x.html {\n      root /usr/share/nginx/www/;\n    }\n\n    access_log /var/log/nginx/random9726/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random9726/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10920",
    "content": "server {\n        listen 80 default; \n\n        location / {\n          proxy_set_header X-Real-IP  $remote_addr;\n          proxy_set_header X-Forwarded-For $remote_addr;\n          proxy_set_header Host $host;\n          proxy_pass http://127.0.0.1:81;\n        }\n\n        location ~ /\\.ht {\n          deny all;\n        }\n\n        access_log /var/log/nginx/random27802/access.log combined_plus;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-10947",
    "content": "upstream django_server_acceptance.random8289.random17507.example.org {\n    server unix:/srv/http/random8289/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random23045.example.org;\n\n    location /media/ {\n        alias /srv/http/random8289/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random8289/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_acceptance.random8289.random17507.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n\n        satisfy any;\n        auth_basic           'random8289 acceptance';\n        auth_basic_user_file /srv/http/random8289/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random8289/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random8289/acceptance/error.log;\n}\n\nserver {\n    server_name www.random23045.example.org;\n    return 301 http://random23045.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11018",
    "content": "upstream django_server_random24036.example.org {\n    server unix:/srv/http/random1006/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random24036.example.org;\n    gzip on;\n    gzip_http_version 1.0;\n    gzip_types *;\n    gzip_vary on;\n    gzip_proxied any;\n\n    location ~ /media/(.*)$ {\n        alias /srv/http/random1006/live/website/static/$1;\n        expires 7d;\n        gzip on;\n    }\n\n\n    location / {\n        proxy_pass http://django_server_random24036.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random1006/live/access.log combined_plus;\n    error_log  /var/log/nginx/random1006/live/error.log;\n}\n\nserver {\n    server_name www.random24036.example.org;\n    server_name random32349.example.org www.random32349.example.org;\n    server_name random23794.example.org www.random23794.example.org;\n    rewrite ^ http://random24036.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11046",
    "content": "upstream django_server_random25979.example.org {\n    server unix:/srv/http/random24211/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random25979.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random24211/internal/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random24211/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random25979.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random24211';\n        auth_basic_user_file /srv/http/random24211/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random24211/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random24211/internal/error.log;\n}\n\nserver {\n    server_name www.random25979.example.org;\n    rewrite ^ http://intern.random24211.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11382",
    "content": "server {\n    listen 80;\n    listen 7891; # User0\n    listen 8080; # User1\n    listen 8900; # User2\n    listen 8912; # User3\n    listen 3567; # User4\n\n    server_name random666.example.org www.random666.example.org;\n\n    root /srv/http/random666.example.org;\n    index index.html index.htm;\n\n    location /duif_assets/ {\n        try_files $uri $uri/ =404;\n    }\n  \n    location /index.html {\n        try_files $uri $uri/ =404;\n    }\n\n    location / {\n        rewrite ^.+$ / break;\n        try_files $uri $uri/ =404;\n    }\n\n    access_log /var/log/nginx/random666.example.org/access.log combined_plus;\n    error_log  /var/log/nginx/random666.example.org/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1167",
    "content": "upstream django_server_random23900.example.org {\n    server unix:/srv/http/random29467/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random23900.example.org www.random23900.example.org;\n\n    if ($host != 'random23900.example.org') {\n        rewrite  ^/(.*)$  http://random23900.example.org/$1  permanent;\n    }\n    \n    location ^~ /media/ {\n        alias /srv/http/random29467/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random29467/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random23900.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n  \n        satisfy any;\n        allow 89.188.25.162;\n        auth_basic            \"random29467 acceptance\";\n        auth_basic_user_file  htpasswords/random29467_acceptance;\n\n    }\n\n    access_log /var/log/nginx/random29467/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random29467/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-11849",
    "content": "upstream django_server_random3140.example.org {\n    server unix:/srv/http/random2912/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random3140.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random2912/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random2912/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random3140.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random2912/live/access.log combined_plus;\n    error_log  /var/log/nginx/random2912/live/error.log;\n}\n\nserver {\n    server_name www.random3140.example.org;\n    server_name random28398.example.org;\n    server_name random23689.example.org www.random23689.example.org;\n\n    rewrite ^ http://random3140.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12027",
    "content": "upstream django_server_random6410.example.org {\n    server unix:/srv/http/random28641/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name www.random6410.example.org;\n\n    location ~ /static/(.*)$ {\n          alias /srv/http/random28641/live/website/static/$1;\n          expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random6410.example.org;\n        include          /etc/nginx/proxy_params;\n\n        proxy_connect_timeout 240;\n        proxy_read_timeout 240;\n    }\n\n    access_log /var/log/nginx/random28641/live/access.log combined_plus;\n    error_log  /var/log/nginx/random28641/live/error.log;\n}\n\nserver {\n    server_name random6410.example.org;\n    rewrite ^ http://www.random6410.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12235",
    "content": "server {\n    server_name random18267.example.org;\n    gzip             on;\n    gzip_min_length  2000;\n    gzip_proxied     any;\n    gzip_types       application/json; \n\n    client_max_body_size 30M;\n\n    root /srv/http/random23264/data;\n    \n    # Security\n    satisfy any;\n    include /etc/nginx/allow_ytec_ips_params;\n    deny all;\n\n    # try serving docs and (md5/immutable) directly\n    location ~ \\+(f|doc)/ {\n        try_files $uri @proxy_to_app;\n    }\n    location / {\n        # XXX how to tell nginx to just refer to @proxy_to_app here?\n        try_files /.lqkwje @proxy_to_app;\n    }   \n    location @proxy_to_app {\n        proxy_pass http://random20604.example.org:4040;\n        proxy_set_header  X-outside-url $scheme://$host;\n        proxy_set_header  X-Real-IP $remote_addr;\n    }   \n\n    access_log /var/log/nginx/random23264/access.log combined_plus;\n    error_log /var/log/nginx/random23264/error.log;\n} \n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-12649",
    "content": "upstream django_server_random10305.example.org {\n    server unix:/srv/http/random23322/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random10305.example.org;\n\n    location /media/ {\n        alias /srv/http/random23322/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random23322/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random10305.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random23322/live/access.log combined_plus;\n    error_log  /var/log/nginx/random23322/live/error.log;\n}\n\nserver {\n    listen 80;\n\n    server_name random13399.example.org;\n    server_name www.random10305.example.org;\n    server_name random17958.example.org www.random17958.example.org;\n    server_name random15266.example.org  www.random15266.example.org;\n    server_name random21296.example.org  www.random21296.example.org;\n    server_name random5261.example.org   www.random5261.example.org;\n    server_name random679.example.org   www.random679.example.org;\n    server_name random31788.example.org          www.random31788.example.org;\n    server_name random22704.example.org         www.random22704.example.org;\n    server_name random17411.example.org          www.random17411.example.org;\n\n    return 301 http://random10305.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-13577",
    "content": "upstream django_server_random30837.example.org {\n    server unix:/srv/http/random30992/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name www.random30837.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random30992/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random30992/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random30837.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random30992/live/access.log combined_plus;\n    error_log  /var/log/nginx/random30992/live/error.log;\n}\n\nserver {\n    server_name random30837.example.org;\n    server_name random3263.example.org www.random3263.example.org;\n    server_name random6771.example.org www.random6771.example.org;\n    server_name random17696.example.org www.random17696.example.org;\n    server_name random7179.example.org www.random7179.example.org;\n    server_name random8127.example.org www.random8127.example.org;\n\n    rewrite ^ http://www.random30837.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14402",
    "content": "upstream django_server_random17705.example.org {\n    server unix:/srv/http/random8289/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random17705.example.org;\n\n    location /media/ {\n        alias /srv/http/random8289/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random8289/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random17705.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random8289/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random8289/internal/error.log;\n}\n\nserver {\n    server_name www.random17705.example.org;\n    return 301 http://random17705.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-14430",
    "content": "upstream django_server_random17507.example.org {\n    server unix:/srv/http/random7740/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random17507.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random7740/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random7740/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random17507.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random7740/live/access.log combined_plus;\n    error_log  /var/log/nginx/random7740/live/error.log;\n}\n\nserver {\n    server_name www.random17507.example.org;\n    server_name random31197.example.org www.random31197.example.org;\n    server_name random19579.example.org www.random19579.example.org;\n    server_name random16629.example.org www.random16629.example.org;\n    server_name random28363.example.org www.random28363.example.org;\n    server_name random30185.example.org www.random30185.example.org;\n    server_name random22326.example.org www.random22326.example.org;\n    server_name random3622.example.org www.random3622.example.org;\n    server_name random1463.example.org www.random1463.example.org;\n    server_name random23341.example.org www.random23341.example.org;\n    server_name random2214.example.org www.random2214.example.org;\n    server_name random22684.example.org www.random22684.example.org;\n    server_name random6606.example.org www.random6606.example.org;\n    server_name random29138.example.org www.random29138.example.org;\n    server_name random15109.example.org www.random15109.example.org;\n    server_name random8002.example.org www.random8002.example.org;\n    server_name random16836.example.org www.random16836.example.org;\n    server_name random22283.example.org www.random22283.example.org;\n\n    location  = /googleXXXXXXXXXXXXXXXX.html {\n        alias /srv/http/random7740/live/website/templates/googleXXXXXXXXXXXXXXXX.html;\n    }\n\n    rewrite ^ http://random17507.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15141",
    "content": "upstream django_server_acceptatie.random20374.nl {\n    server unix:/srv/http/random20374/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random28586.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random20374/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random20374/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_acceptatie.random20374.nl;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random20374';\n        auth_basic_user_file /srv/http/random20374/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random20374/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random20374/acceptance/error.log;\n}\n\nserver {\n    server_name www.random28586.example.org;\n    rewrite ^ http://random28586.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15270",
    "content": "upstream django_server_random6822.example.org {\n    server unix:/srv/http/random7047/live/website.sock;\n}\n\nserver {\n    listen 8443;\n    server_name random6822.example.org;\n\n    ssl on;\n    ssl_certificate       /etc/ssl/public/random6822.example.org.complete-bundle.crt;\n    ssl_certificate_key   /etc/ssl/private/random6822.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random7047/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random7047/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random6822.example.org;\n        include          /etc/nginx/proxy_params;\n    }\n\n    access_log /var/log/nginx/random7047/live/access.log combined_plus;\n    error_log  /var/log/nginx/random7047/live/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random6822.example.org;\n\n    rewrite ^/(.*) https://random6822.example.org:8443/$1;\n}\n\n\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15291",
    "content": "# You may add here your\n# server {\n#\t...\n# }\n# statements for each of your virtual hosts to this file\n\n##\n# You should look at the following URL's in order to grasp a solid understanding\n# of Nginx configuration files in order to fully unleash the power of Nginx.\n# http://wiki.nginx.org/Pitfalls\n# http://wiki.nginx.org/QuickStart\n# http://wiki.nginx.org/Configuration\n#\n# Generally, you will want to move this file somewhere, and start with a clean\n# file but keep this around for reference. Or just disable in sites-enabled.\n#\n# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.\n##\n\nserver {\n\tlisten 80 default_server;\n\n\troot /usr/share/nginx/html;\n\tindex index.html index.htm;\n\n\t# Make site accessible from http://random20604.example.org/\n\tserver_name random20604.example.org;\n\n\tlocation / {\n\t\t# First attempt to serve request as file, then\n\t\t# as directory, then fall back to displaying a 404.\n\t\ttry_files $uri $uri/ =404;\n\t\t# Uncomment to enable naxsi on this location\n\t\t# include /etc/nginx/naxsi.rules\n\t}\n\n\t# Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests\n\t#location /RequestDenied {\n\t#\tproxy_pass http://127.0.0.1:8080;    \n\t#}\n\n\t#error_page 404 /404.html;\n\n\t# redirect server error pages to the static page /50x.html\n\t#\n\t#error_page 500 502 503 504 /50x.html;\n\t#location = /50x.html {\n\t#\troot /usr/share/nginx/html;\n\t#}\n\n\t# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n\t#\n\t#location ~ \\.php$ {\n\t#\tfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\t#\t# NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n\t#\n\t#\t# With php5-cgi alone:\n\t#\tfastcgi_pass 127.0.0.1:9000;\n\t#\t# With php5-fpm:\n\t#\tfastcgi_pass unix:/var/run/php5-fpm.sock;\n\t#\tfastcgi_index index.php;\n\t#\tinclude fastcgi_params;\n\t#}\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\t#\n\t#location ~ /\\.ht {\n\t#\tdeny all;\n\t#}\n}\n\n\n# another virtual host using mix of IP-, name-, and port-based configuration\n#\n#server {\n#\tlisten 8000;\n#\tlisten random20605.example.org:8080;\n#\tserver_name random20605.example.org alias another.alias;\n#\troot html;\n#\tindex index.html index.htm;\n#\n#\tlocation / {\n#\t\ttry_files $uri $uri/ =404;\n#\t}\n#}\n\n\n# HTTPS server\n#\n#server {\n#\tlisten 443;\n#\tserver_name random20604.example.org;\n#\n#\troot html;\n#\tindex index.html index.htm;\n#\n#\tssl on;\n#\tssl_certificate cert.pem;\n#\tssl_certificate_key cert.key;\n#\n#\tssl_session_timeout 5m;\n#\n#\tssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;\n#\tssl_ciphers \"HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES\";\n#\tssl_prefer_server_ciphers on;\n#\n#\tlocation / {\n#\t\ttry_files $uri $uri/ =404;\n#\t}\n#}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15456",
    "content": "upstream django_server_random29275.example.org {\n    server unix:/srv/http/random14353/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random29275.example.org;\n\n    location /media/ {\n        alias /srv/http/random14353/internal/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random14353/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random29275.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n\n        satisfy any;\n        auth_basic           'internal for random14353';\n        auth_basic_user_file /srv/http/random14353/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random14353/internal/access.log;\n    error_log  /var/log/nginx/random14353/internal/error.log;\n}\n\nserver {\n    server_name www.random29275.example.org;\n    return 301 http://random29275.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15497",
    "content": "upstream django_server_random16112.example.org {\n    server unix:/srv/http/random29227/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random16112.example.org;\n\n    location /media/ {\n        alias /srv/http/random29227/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random29227/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random16112.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random29227/live/access.log combined_plus;\n    error_log  /var/log/nginx/random29227/live/error.log;\n}\nserver {\n    server_name random5297.example.org www.random5297.example.org;\n    server_name random17050.example.org www.random17050.example.org;\n    server_name www.random16112.example.org;\n\n    return 301 http://random16112.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-15852",
    "content": "upstream django_server_random7474.example.org {\n    server unix:/srv/http/random4886/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random7474.example.org;\n\n    location /media/ {\n        alias /srv/http/random4886/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random4886/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random7474.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random4886';\n        auth_basic_user_file /srv/http/random4886/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    client_max_body_size 20m;\n\n    access_log /var/log/nginx/random4886/acceptance/access.log;\n    error_log  /var/log/nginx/random4886/acceptance/error.log;\n}\n\nserver {\n    server_name www.random7474.example.org;\n    return 301 http://random7474.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-16345",
    "content": "upstream django_server_random25713.example.org {\n    server unix:/srv/http/random24922/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random25713.example.org;\n\n    location /media/ {\n        alias /srv/http/random24922/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random24922/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random25713.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        include /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random24922/live/access.log;\n    error_log  /var/log/nginx/random24922/live/error.log;\n}\n\nserver {\n    server_name www.random25713.example.org;\n    return 301 http://random25713.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17175",
    "content": "server {\n    listen 80;\n    server_name random25647.example.org www.random25647.example.org random10963.example.org www.random10963.example.org;\n\n    if ($host != 'random25647.example.org') {\n        rewrite  ^/(.*)$  http://random25647.example.org/$1  permanent;\n    }\n    \n    index index.html index.htm;\n    root /srv/http/random11461/countdown/;\n\n    access_log /var/log/nginx/random11461/live/access.log combined_plus;\n    error_log  /var/log/nginx/random11461/live/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17832",
    "content": "upstream django_server_random6430.example.org {\n    server unix:/srv/http/random550/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random6430.example.org;\n\n    location /media/ {\n        alias /srv/http/random550/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random550/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random6430.example.org;\n        include          /etc/nginx/django_proxy_params;\n\n    }\n\n    access_log /var/log/nginx/random550/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random550/internal/error.log;\n}\n\nserver {\n    server_name www.random6430.example.org;\n    return 301 http://random6430.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-17942",
    "content": "upstream django_server_random25647.example.org {\n    server unix:/srv/http/random11461/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random25647.example.org www.random25647.example.org random10963.example.org www.random10963.example.org;\n\n    if ($host != 'random25647.example.org') {\n        rewrite  ^/(.*)$  http://random25647.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random11461/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random11461/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random25647.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random11461/live/access.log combined_plus;\n    error_log  /var/log/nginx/random11461/live/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18018",
    "content": "upstream django_server_intern.random20374.nl {\n    server unix:/srv/http/random20374/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random23818.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random20374/internal/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random20374/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_intern.random20374.nl;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random20374';\n        auth_basic_user_file /srv/http/random20374/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random20374/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random20374/internal/error.log;\n}\n\nserver {\n    server_name www.random23818.example.org;\n    rewrite ^ http://random23818.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-18069",
    "content": "upstream django_server_random7949.example.org {\n    server unix:/srv/http/random1006/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random7949.example.org;\n    gzip on;\n    gzip_http_version 1.0;\n    gzip_types *;\n    gzip_vary on;\n    gzip_proxied any;\n\n    location ~ /media/(.*)$ {\n        alias /srv/http/random1006/acceptance/website/static/$1;\n        expires 7d;\n        gzip on;\n    }\n\n\n    location / {\n        proxy_pass http://django_server_random7949.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random1006';\n        auth_basic_user_file /srv/http/random1006/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random1006/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random1006/acceptance/error.log;\n}\n\nserver {\n    server_name www.random7949.example.org;\n    rewrite ^ http://random7949.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19334",
    "content": "upstream django_server_random1515.example.org {\n  server unix:/srv/http/random15255/acceptance/website.sock fail_timeout=5;\n} \n\nserver {\n    listen 80;\n     server_name random1515.example.org www.random1515.example.org;\n \n  if  ($host != 'random1515.example.org') {\n     rewrite  ^/(.*)$  http://random1515.example.org/$1  permanent;\n  } \n     \n    location /media/ {\n        alias /srv/http/random15255/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    } \n    location /static/ {\n        alias /srv/http/random15255/acceptance/static_collected/;\n        expires 7d;\n    } \n     \n  location / { \n    proxy_pass http://django_server_random1515.example.org;\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Protocol $scheme;\n\n    satisfy any;\n    auth_basic           'random191 acceptance';\n    auth_basic_user_file /srv/http/random15255/acceptance/htpasswords;\n    include              /etc/nginx/allow_ytec_ips_params;\n    deny all;\n  }\n\n  access_log /var/log/nginx/random15255/acceptance/access.log combined_plus;\n  error_log /var/log/nginx/random15255/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19639",
    "content": "upstream django_server_live.random8289.random17507.example.org {\n    server unix:/srv/http/random8289/live/website.sock;\n}\n\nserver {\n    listen 443;\n    server_name random23886.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random23886.example.org.complete-bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random23886.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random8289/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random8289/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_live.random8289.random17507.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random8289/live/access.log combined_plus;\n    error_log  /var/log/nginx/random8289/live/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random23886.example.org;\n    return 301 https://random23886.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-1966",
    "content": "upstream django_server_random31523.example.org {\n    server unix:/srv/http/random16722.example.org/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random31523.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random16722.example.org/internal/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random16722.example.org/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random31523.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random16722.example.org';\n        auth_basic_user_file /srv/http/random16722.example.org/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random16722.example.org/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random16722.example.org/internal/error.log;\n}\n\nserver {\n    server_name www.random31523.example.org;\n    rewrite ^ http://random31523.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19791",
    "content": "upstream django_server_random1413.example.org {\n    server unix:/srv/http/random25151/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random1413.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random25151/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random25151/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1413.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random25151/live/access.log combined_plus;\n    error_log  /var/log/nginx/random25151/live/error.log;\n}\n\nserver {\n    server_name www.random1413.example.org;\n    rewrite ^ http://random1413.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-19955",
    "content": "upstream django_server_random9619.example.org {\n    server unix:/srv/http/random28641/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random9619.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random28641/internal/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random28641/internal/website/static/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random9619.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random28641';\n        auth_basic_user_file /srv/http/random28641/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random28641/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random28641/internal/error.log;\n}\n\nserver {\n    server_name www.random9619.example.org;\n    rewrite ^ http://random9619.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21369",
    "content": "upstream django_server_random31758.example.org {\n    server unix:/srv/http/random21623/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random31758.example.org www.random31758.example.org;\n\n    if ($host != 'random31758.example.org') {\n        rewrite  ^/(.*)$  http://random31758.example.org/$1 permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random21623/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random21623/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random31758.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random21623/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random21623/internal/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-21549",
    "content": "upstream django_server_random1688.example.org {\n    server unix:/srv/http/random6470/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random5078.example.org random1688.example.org www.random1688.example.org;\n\n    if ($host != 'random5078.example.org') {\n        rewrite  ^/(.*)$  http://random5078.example.org/$1  permanent;\n    }\n    \n    location ^~ /media/ {\n        alias /srv/http/random6470/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random6470/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1688.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random6470/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random6470/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-230",
    "content": "upstream django_server_random22746.example.org {\n    server unix:/srv/http/random6344/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random22746.example.org;\n\n    if ($host != 'random22746.example.org') {\n        rewrite  ^/(.*)$  http://random22746.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random6344/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random6344/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random22746.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random6344/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random6344/internal/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23325",
    "content": "upstream django_server_random15255_live {\n  server unix:/srv/http/random15255/live/website.sock fail_timeout=5;\n}\n\nserver {\n    listen 443;\n    server_name random7381.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random7381.example.org_chained.crt;\n    ssl_certificate_key /etc/ssl/private/random7381.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random15255/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    } \n    \n    location /static/ {\n        alias /srv/http/random15255/live/static_collected/;\n        expires 7d;\n    }\n    \n    location / {\n        proxy_pass http://django_server_random15255_live;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n  access_log /var/log/nginx/random15255/live/access.log combined_plus;\n  error_log /var/log/nginx/random15255/live/error.log;\n}\n\nserver {\n  listen 80; \n  server_name random7381.example.org www.random7381.example.org;\n\n  return 301 https://random7381.example.org$request_uri;\n}\n\nserver {\n  listen 8445;\n  server_name random7381.example.org www.random7381.example.org;\n\n  ssl on;\n  ssl_certificate     /etc/ssl/public/random7381.example.org_chained.crt;\n  ssl_certificate_key /etc/ssl/private/random7381.example.org.key;\n\n  return 301 https://random7381.example.org$request_uri;\n}\n\nserver {\n  listen 1000;\n  server_name random7381.example.org www.random7381.example.org;\n\n  ssl on;\n  ssl_certificate     /etc/ssl/public/random7381.example.org_chained.crt;\n  ssl_certificate_key /etc/ssl/private/random7381.example.org.key;\n\n  return 301 https://random7381.example.org$request_uri;\n}\n\nserver {\n  listen 443;\n  server_name www.random7381.example.org;\n\n  ssl on;\n  ssl_certificate     /etc/ssl/public/random7381.example.org_chained.crt;\n  ssl_certificate_key /etc/ssl/private/random7381.example.org.key;\n\n  return 301 https://random7381.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23470",
    "content": "upstream django_server_random27579.example.org {\n    server unix:/srv/http/random21623/live/website.sock;\n}\n\nserver {\n    listen 443;\n    server_name random27579.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random27579.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random27579.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random21623/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random21623/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random27579.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random21623/live/access.log combined_plus;\n    error_log  /var/log/nginx/random21623/live/error.log;\n}\n\nserver {\n    listen 443;\n    server_name www.random27579.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random27579.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random27579.example.org.key;\n\n    return 301 https://random27579.example.org$request_uri;\n}\n\nserver {\n    listen 80;\n\n    server_name random27579.example.org www.random27579.example.org random11512.example.org;\n    server_name random18003.example.org www.random18003.example.org;\n    server_name random26730.example.org www.random26730.example.org;\n    server_name random3968.example.org www.random3968.example.org;\n    server_name random11925.example.org www.random11925.example.org;\n\n    return 301 https://random27579.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23791",
    "content": "upstream django_server_random31057.example.org {\n    server unix:/srv/http/random22194/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random31057.example.org www.random31057.example.org;\n\n    if ($host != 'random31057.example.org') {\n        rewrite  ^/(.*)$  http://random31057.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random22194/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random22194/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random31057.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_read_timeout 120;\n    }\n\n    access_log /var/log/nginx/random22194/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random22194/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23803",
    "content": "upstream django_server_random16722.example.org {\n    server unix:/srv/http/random16722.example.org/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random16722.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random16722.example.org/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random16722.example.org/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random16722.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random16722.example.org/live/access.log combined_plus;\n    error_log  /var/log/nginx/random16722.example.org/live/error.log;\n}\n\nserver {\n    server_name www.random16722.example.org;\n    rewrite ^ http://random16722.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-23838",
    "content": "upstream django_server_random14388.example.org {\n    server unix:/srv/http/random4886/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random14388.example.org;\n\n    location /media/ {\n        alias /srv/http/random4886/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random4886/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random14388.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random4886/live/access.log;\n    error_log  /var/log/nginx/random4886/live/error.log;\n}\n\nserver {\n    server_name www.random14388.example.org;\n    return 301 http://random14388.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24125",
    "content": "server {\n    listen 80;\n    server_name random14996.example.org;\n\n    root /srv/http/random23392/;\n    index index.html;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24193",
    "content": "upstream django_server_random6177.example.org {\n    server unix:/srv/http/random550/live/website.sock;\n}\n\nserver {\n    listen 443 ssl;\n    server_name random2179.example.org;\n\n    ssl_certificate       /etc/ssl/public/random2179.example.org.bundle.crt;\n    ssl_certificate_key   /etc/ssl/private/random2179.example.org.key;\n\n\n    location /media/ {\n        alias /srv/http/random550/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random550/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random6177.example.org;\n        include          /etc/nginx/django_proxy_params;\n    }\n\n    access_log /var/log/nginx/random550/live/access.log combined_plus;\n    error_log  /var/log/nginx/random550/live/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random2179.example.org;\n\n    location /media/ {\n        alias /srv/http/random550/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random550/live/static_collected/;\n        expires 7d;\n    }\n\n    #location = / {\n    #    return 301 https://random2179.example.org$request_uri;\n    #}\n\n    location / {\n        proxy_pass http://django_server_random6177.example.org;\n        include          /etc/nginx/django_proxy_params;\n    }\n\n    access_log /var/log/nginx/random550/live/access_http.log combined_plus;\n    error_log  /var/log/nginx/random550/live/error_http.log;\n}\n\nserver {\n    server_name random6177.example.org www.random6177.example.org;\n    return 301 http://random2179.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-24213",
    "content": "upstream django_server_random22047.example.org {\n    server unix:/srv/http/random26975/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random22047.example.org;\n\n    location /media/ {\n        alias /srv/http/random26975/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random26975/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random22047.example.org;\n        include          /etc/nginx/django_proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random26975';\n        auth_basic_user_file /srv/http/random26975/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random26975/acceptance/access.log;\n    error_log  /var/log/nginx/random26975/acceptance/error.log;\n}\n\nserver {\n    server_name www.random22047.example.org;\n    return 301 http://random22047.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-25480",
    "content": "upstream django_server_random6193.example.org {\n    server unix:/srv/http/random4755/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random6193.example.org www.random6193.example.org;\n\n    if ($host != 'random6193.example.org') {\n        rewrite  ^/(.*)$  http://random6193.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random4755/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random4755/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random6193.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random4755/live/access.log combined_plus;\n    error_log  /var/log/nginx/random4755/live/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26195",
    "content": "server {\n    listen 80;\n    server_name www.random25446.example.org random25446.example.org;\n\n        if ($host != 'random25446.example.org') {\n                rewrite  ^/(.*)$  http://random25446.example.org/$1  permanent;\n        }\n    \n    location ^~ /media {\n        alias /srv/http/random17476/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location ^~ /static {\n        alias /srv/http/random17476/internal/static_collected/;\n        expires 7d;\n    }\n    \n    location / {\n        include fastcgi_params;\n        fastcgi_pass unix:/srv/http/random17476/internal/website.sock;\n    }\n\n  access_log /var/log/nginx/random17476/internal/access.log combined_plus;\n  error_log /var/log/nginx/random17476/internal/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26221",
    "content": "upstream django_server_random4030.example.org {\n    server unix:/srv/http/random26975/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random4030.example.org;\n\n    location /media/ {\n        alias /srv/http/random26975/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random26975/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random4030.example.org;\n        include          /etc/nginx/django_proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random26975/live/access.log;\n    error_log  /var/log/nginx/random26975/live/error.log;\n}\n\nserver {\n    server_name www.random4030.example.org;\n    return 301 http://random4030.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26637",
    "content": "upstream django_server_random5890.example.org {\n    server unix:/srv/http/random4755/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random5890.example.org;\n\n    if ($host != 'random5890.example.org') {\n        rewrite  ^/(.*)$  http://random5890.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random4755/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random4755/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random5890.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random4755/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random4755/internal/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-26758",
    "content": "server {\n\tlisten 80 default_server;\n\t#listen [::]:80 default_server ipv6only=on;\n\troot /var/www/default/;\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\tlocation ~ /\\.ht {\n\t\tdeny all;\n\t}\n\n  location /nginx_status {\n    stub_status on;\n    access_log   off;\n    allow 127.0.0.1;\n    deny all;\n  }\n\n  access_log /var/log/nginx/access.log combined_plus;\n  error_log /var/log/nginx/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27646",
    "content": "upstream django_server_random10783.example.org {\n    server unix:/srv/http/random4711/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random10783.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random4711/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random4711/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random10783.example.org;\n        include          /etc/nginx/proxy_params;\n        proxy_read_timeout 4m;\n\n        satisfy any;\n        auth_basic           'acceptance for random4711';\n        auth_basic_user_file /srv/http/random4711/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random4711/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random4711/acceptance/error.log;\n}\n\nserver {\n    server_name www.random10783.example.org;\n    rewrite ^ http://random10783.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27728",
    "content": "server {\n  location =/ {\n    return 404;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27736",
    "content": "upstream django_server_random17112.example.org {\n    server unix:/srv/http/random29467/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random17112.example.org www.random17112.example.org;\n\n    if ($host != 'random17112.example.org') {\n        rewrite  ^/(.*)$  http://random17112.example.org/$1  permanent;\n    }\n    \n    location ^~ /media/ {\n        alias /srv/http/random29467/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random29467/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random17112.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random29467/live/access.log combined_plus;\n    error_log  /var/log/nginx/random29467/live/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-27812",
    "content": "upstream django_server_random1296.example.org {\n    server unix:/srv/http/random2912/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random1296.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random2912/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random2912/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1296.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random2912';\n        auth_basic_user_file /srv/http/random2912/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random2912/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random2912/acceptance/error.log;\n}\n\nserver {\n    server_name www.random1296.example.org;\n    rewrite ^ http://random1296.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28050",
    "content": "upstream django_server_random11685.example.org {\n    server unix:/srv/http/random4886/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random11685.example.org;\n\n    location /media/ {\n        alias /srv/http/random4886/internal/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random4886/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random11685.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random4886';\n        auth_basic_user_file /srv/http/random4886/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random4886/internal/access.log;\n    error_log  /var/log/nginx/random4886/internal/error.log;\n}\n\nserver {\n    server_name www.random11685.example.org;\n    return 301 http://random11685.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-28690",
    "content": "upstream django_server_random16112.example.org {\n    server unix:/srv/http/random24645/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random16112.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random24645/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random24645/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random16112.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random24645/live/access.log;\n    error_log  /var/log/nginx/random24645/live/error.log;\n}\n\nserver {\n    server_name www.random16112.example.org;\n    rewrite ^ http://random16112.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-29159",
    "content": "upstream django_server_random29198.example.org {\n    server unix:/srv/http/random28641/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random29198.example.org;\n\n    location ~ /static/(.*)$ {\n          alias /srv/http/random28641/acceptance/website/static/$1;\n          expires 7d;\n    }\n\n\n    location / {\n        proxy_pass http://django_server_random29198.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random28641';\n        auth_basic_user_file /srv/http/random28641/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random28641/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random28641/acceptance/error.log;\n}\n\nserver {\n    server_name www.random29198.example.org;\n    rewrite ^ http://random29198.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-2951",
    "content": "server {\n\tlisten 80;\n\t#listen [::]:80 default_server ipv6only=on;\n\troot /var/www/random616_log/;\n\tserver_name random12800.example.org;\n\n\t# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n\tlocation ~ \\.php$ {\n\t\tfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\t\t# NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n\t\n\t\t# With php5-fpm:\n\t\tfastcgi_pass unix:/var/run/php5-fpm.sock;\n\t\tfastcgi_index index.php;\n\t\tinclude fastcgi_params;\n\t}\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\tlocation ~ /\\.ht {\n\t\tdeny all;\n\t}\n\n  location /nginx_status {\n    stub_status on;\n    access_log   off;\n    allow 127.0.0.1;\n    deny all;\n  }\n\n  access_log /var/log/nginx/random12543/access.log combined_plus;\n  error_log /var/log/nginx/random12543/error.log;\n}\n\nserver {\n\tlisten 443 default_server;\n\t#listen [::]:443 default_server ipv6only=on;\n\troot /var/www/random616_log/;\n\tserver_name random12800.example.org;\n\n  # We created (will create) this SSL certificate ourselves, using our own CA. This way, we can control strictly which CA the XXX trusts.\n  # See ytec #6244\n  # However, we're working on a fix for high SSL overhead. We're hoping to be able to keep the connections open between log POSTs, like SSL can.\n  ssl on;\n  ssl_certificate     /etc/ssl/public/random12800.example.org.crt;\n  ssl_certificate_key /etc/ssl/private/random12800.example.org.key;\n\n\t# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n\tlocation ~ \\.php$ {\n\t\tfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\t\t# NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n\t\n\t\t# With php5-fpm:\n\t\tfastcgi_pass unix:/var/run/php5-fpm.sock;\n\t\tfastcgi_index index.php;\n\t\tinclude fastcgi_params;\n\t}\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\tlocation ~ /\\.ht {\n\t\tdeny all;\n\t}\n\n  access_log /var/log/nginx/random12543/access.log combined_plus;\n  error_log /var/log/nginx/random12543/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30011",
    "content": "upstream django_server_random12785.example.org {\n    server unix:/srv/http/random14353/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random12785.example.org;\n\n    location /media/ {\n        alias /srv/http/random14353/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random14353/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random12785.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n\n        satisfy any;\n        include /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }    \n\n    access_log /var/log/nginx/random14353/live/access.log;\n    error_log  /var/log/nginx/random14353/live/error.log;\n}\n\nserver {\n    server_name www.random12785.example.org;\n    return 301 http://random12785.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-30571",
    "content": "upstream django_server_random7150.example.org {\n    server unix:/srv/http/random550/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random7150.example.org;\n\n    location /media/ {\n        alias /srv/http/random550/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random550/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random7150.example.org;\n        include          /etc/nginx/django_proxy_params;\n    }\n\n    access_log /var/log/nginx/random550/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random550/acceptance/error.log;\n}\n\nserver {\n    server_name www.random7150.example.org;\n    return 301 http://random7150.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-31900",
    "content": "upstream django_server_random31131.example.org {\n    server unix:/srv/http/random24334/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random31131.example.org;\n\n    location /media/ {\n        alias /srv/http/random24334/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n     }\n    location /static/ {\n        alias /srv/http/random24334/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random31131.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random24334/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random24334/internal/error.log;\n}\n\nserver {\n    server_name www.random31131.example.org;\n    return 301 http://random31131.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32190",
    "content": "server {\n    server_name www.random5115;\n    return 301 http://www.random10305.example.org;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32279",
    "content": "server {\n\tlisten 80;\n\troot /home/admin/random19651_log/;\n\tserver_name random16339.example.org;\n\n\t# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n\tlocation ~ \\.php$ {\n\t\tfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\t\t# NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n\t\n\t\t# With php5-fpm:\n\t\tfastcgi_pass unix:/var/run/php5-fpm.sock;\n\t\tfastcgi_index index.php;\n\t\tinclude fastcgi_params;\n\t}\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\tlocation ~ /\\.ht {\n\t\tdeny all;\n\t}\n\n  access_log /var/log/nginx/random4235/access.log combined_plus;\n  error_log /var/log/nginx/random4235/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32317",
    "content": "upstream django_server_random21989.example.org {\n    server unix:/srv/http/random28136/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random21989.example.org;\n\n    location ~ /static/(.*)$ {\n        alias /srv/http/random28136/acceptance/website/static/$1;\n        expires 7d;\n    } \n\n    location / {\n        proxy_pass http://django_server_random21989.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'acceptance for random28136';\n        auth_basic_user_file /srv/http/random28136/acceptance/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random28136/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random28136/acceptance/error.log;\n}\n\nserver {\n    server_name www.random21989.example.org;\n    rewrite ^ http://random21989.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-32438",
    "content": "upstream django_server_random1769.example.org {\n    server unix:/srv/http/random7047/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random1769.example.org;\n\n    if ($host != 'random1769.example.org') {\n        rewrite  ^/(.*)$  http://random1769.example.org/$1  permanent;\n    }\n\n    rewrite ^/(.*) https://$host:8444/$1;\n}\n\nserver {\n    listen 8444;\n    server_name random1769.example.org;\n\n    ssl on;\n    ssl_certificate       /etc/ssl/public/random6822.example.org.crt;\n    ssl_certificate_key   /etc/ssl/private/random6822.example.org.key;\n\n    location ^~ /media/ {\n        alias /srv/http/random7047/acceptance/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random7047/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1769.example.org;\n        include          /etc/nginx/proxy_params;\n\n        #satisfy any;\n        #auth_basic           'acceptance for random7047';\n        #auth_basic_user_file /srv/http/random7047/acceptance/htpasswords;\n        #include              /etc/nginx/allow_ytec_ips_params;\n        #deny all;\n    }\n\n    access_log /var/log/nginx/random7047/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random7047/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3483",
    "content": "server {\n        listen 80;\n        server_name random9761.example.org;\n\n\n        location ~ /static/(.*)$ {\n          alias /srv/http/random14537/static_collected/$1;\n          expires 7d;\n        }\n\n        location ~ /media/(.*)$ {\n          alias /srv/http/random14537/dynamic/public/$1;\n          expires 7d;\n          include upload_folder_security_params;\n        }\n\n\n        location / {\n          proxy_set_header X-Real-IP  $remote_addr;\n          proxy_set_header X-Forwarded-For $remote_addr;\n          proxy_set_header Host $host;\n          proxy_pass http://127.0.0.1:81;\n          proxy_connect_timeout 120;\n          proxy_read_timeout 120;\n        }\n\n        location ~ /\\.ht {\n          deny all;\n        }\n\n        access_log /var/log/nginx/random14537/access.log combined_plus;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3507",
    "content": "server {\n    listen 80;\n    server_name random3674.example.org www.random3674.example.org;\n\n    root /srv/http/random3674.example.org;\n    index index.html index.htm;\n\n    location / {\n        try_files $uri $uri/ =404;\n    }\n\n    access_log /var/log/nginx/random3674.example.org/access.log combined_plus;\n    error_log  /var/log/nginx/random3674.example.org/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random27569.example.org www.random27569.example.org;\n\n    root /srv/http/random27569.example.org;\n    index index.html index.htm;\n\n    location / {\n        try_files $uri $uri/ =404;\n    }\n\n    access_log /var/log/nginx/random27569.example.org/access.log combined_plus;\n    error_log  /var/log/nginx/random27569.example.org/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random11055.example.org www.random11055.example.org;\n\n    root /srv/http/random11055.example.org;\n    index index.html index.htm;\n\n    location / {\n        try_files $uri $uri/ =404;\n    }\n\n    access_log /var/log/nginx/random11055.example.org/access.log combined_plus;\n    error_log  /var/log/nginx/random11055.example.org/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-3874",
    "content": "upstream django_server_random7267.example.org {\n    server unix:/srv/http/random24334/live/website.sock;\n}\n\nserver {\n    listen 80;\n    listen 443 ssl;\n\n    server_name random7267.example.org;\n\n    ssl_certificate     /etc/ssl/public/random7267.example.org_chained.crt;\n    ssl_certificate_key /etc/ssl/private/random7267.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random24334/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random24334/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random7267.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random24334/live/access.log combined_plus;\n    error_log  /var/log/nginx/random24334/live/error.log;\n}\n\nserver {\n    listen 80;\n    listen 443 ssl;\n\n    server_name www.random7267.example.org;\n\n    ssl_certificate     /etc/ssl/public/random7267.example.org_chained.crt;\n    ssl_certificate_key /etc/ssl/private/random7267.example.org.key;\n\n    return 301 http://random7267.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4035",
    "content": "upstream django_server_random2104.example.org {\n    server unix:/srv/http/random28136/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name www.random2104.example.org;\n\n    location ~ /static/(.*)$ {\n        alias /srv/http/random28136/live/website/static/$1;\n        expires 7d;\n    }\n\n\n    location / {\n        proxy_pass http://django_server_random2104.example.org;\n        include          /etc/nginx/proxy_params;\n        proxy_connect_timeout 240;\n        proxy_read_timeout 240;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random28136/live/access.log combined_plus;\n    error_log  /var/log/nginx/random28136/live/error.log;\n}\n\nserver {\n    server_name random2104.example.org;\n    rewrite ^ http://www.random2104.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4143",
    "content": "upstream django_server_random24919.example.org {\n    server unix:/srv/http/random7831/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random24919.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random7831/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random7831/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random24919.example.org;\n        include          /etc/nginx/proxy_params;\n\n        proxy_connect_timeout 240;\n        proxy_read_timeout 240;\n    }\n\n    access_log /var/log/nginx/random7831/live/access.log combined_plus;\n    error_log  /var/log/nginx/random7831/live/error.log;\n}\n\nserver {\n    server_name www.random24919.example.org;\n    rewrite ^ http://random24919.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-4264",
    "content": "# vhost created by moving from marauder, but there it was an apache vhost.\n\nserver {\n    listen 80;\n    server_name random3080.example.org www.random3080.example.org random26833.example.org www.random26833.example.org;\n\n    root /srv/http/random10391.example.org/;\n\n    if ($request_uri != '/googleYYYYYYYYYYYYYYYY.html') {\n      rewrite ^ http://random10305.example.org/ permanent;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5826",
    "content": "upstream django_server_random1107.example.org {\n    server unix:/srv/http/random4755/acceptance/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random1107.example.org www.random1107.example.org;\n\n    if ($host != 'random1107.example.org') {\n        rewrite  ^/(.*)$  http://random1107.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random4755/acceptance/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random4755/acceptance/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random1107.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n  \n        satisfy any;\n        allow 89.188.25.162;\n        auth_basic            \"random4755 acceptance\";\n        auth_basic_user_file  htpasswords/random4755_acceptance;\n\n    }\n\n    access_log /var/log/nginx/random4755/acceptance/access.log combined_plus;\n    error_log  /var/log/nginx/random4755/acceptance/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-5872",
    "content": "upstream django_server_random8404.example.org {\n    server unix:/srv/http/random1006/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random8404.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random1006/internal/website/static/;\n        expires 7d;\n    }\n    #location ^~ /static/ {\n    #    alias /srv/http/random1006/internal/website/static/;\n    #    expires 7d;\n    #}\n\n    location / {\n        proxy_pass http://django_server_random8404.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random1006';\n        auth_basic_user_file /srv/http/random1006/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random1006/internal/access.log combined_plus;\n    error_log  /var/log/nginx/random1006/internal/error.log;\n}\n\nserver {\n    server_name www.random8404.example.org;\n    rewrite ^ http://random8404.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-6228",
    "content": "upstream django_server_random15255_intern {\n  server unix:/srv/http/random15255/intern/website.sock fail_timeout=5;\n} \n\nserver {\n    listen 80;\n    server_name random11459.example.org www.random11459.example.org;\n\n  if ($host != 'random11459.example.org') {\n    rewrite  ^/(.*)$  http://random11459.example.org/$1  permanent;\n  }\n    \n    location /media/ {\n        alias /srv/http/random15255/internal/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random15255/internal/static_collected/;\n        expires 7d;\n    }\n    \n  location / {\n    proxy_pass http://django_server_random15255_intern;\n    proxy_set_header Host $host;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Protocol $scheme;\n\n    satisfy any;\n    auth_basic           'random191 internal';\n    auth_basic_user_file /srv/http/random15255/internal/htpasswords;\n    include              /etc/nginx/allow_ytec_ips_params;\n    deny all;\n  }\n\n  access_log /var/log/nginx/random15255/internal/access.log combined_plus;\n  error_log /var/log/nginx/random15255/internal/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-7895",
    "content": "upstream django_server_random20084.example.org {\n    server unix:/srv/http/random1540/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random3969.example.org www.random20084.example.org random20084.example.org;\n\n    if ($host != 'www.random20084.example.org') {\n        rewrite  ^/(.*)$  http://www.random20084.example.org/$1  permanent;\n    }\n    \n    location /media/ {\n        alias /srv/http/random1540/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random1540/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random20084.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    }\n\n    access_log /var/log/nginx/random1540/live/access.log combined_plus;\n    error_log  /var/log/nginx/random1540/live/error.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8343",
    "content": "upstream django_server_random29577.example.org {\n    server unix:/srv/http/random24645/internal/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random29577.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random24645/internal/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random24645/internal/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random29577.example.org;\n        include          /etc/nginx/proxy_params;\n\n        satisfy any;\n        auth_basic           'internal for random24645';\n        auth_basic_user_file /srv/http/random24645/internal/htpasswords;\n        include              /etc/nginx/allow_ytec_ips_params;\n        deny all;\n    }\n\n    access_log /var/log/nginx/random24645/internal/access.log;\n    error_log  /var/log/nginx/random24645/internal/error.log;\n}\n\nserver {\n    server_name www.random29577.example.org;\n    rewrite ^ http://random29577.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8422",
    "content": "upstream django_server_random25771.example.org {\n    server unix:/srv/http/random4711/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random25771.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random4711/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random4711/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random25771.example.org;\n        include          /etc/nginx/proxy_params;\n        proxy_read_timeout 4m;\n\n        # You can configure access rules here\n    }\n\n    client_max_body_size 25m;\n\n    access_log /var/log/nginx/random4711/live/access.log combined_plus;\n    error_log  /var/log/nginx/random4711/live/error.log;\n}\n\nserver {\n    server_name www.random25771.example.org;\n    server_name *.random17707.example.org;\n    server_name *.random22274.example.org;\n    server_name *.random26333.example.org; \n    server_name *.random10742.example.org;\n    server_name *.random8297.example.org;\n    server_name *.random18250.example.org;\n    server_name *.random30184.example.org;\n    server_name *.random27005.example.org;\n    server_name *.random12286.example.org;\n    server_name *.random28076.example.org; \n    server_name *.random26194.example.org;\n    rewrite ^ http://random25771.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8637",
    "content": "upstream django_server_random27891.example.org {\n    server unix:/srv/http/random6344/live/website.sock;\n}\n \nserver {\n    listen 443;\n    server_name random27891.example.org;\n\n    ssl on;\n    ssl_certificate     /etc/ssl/public/random27891.example.org.bundle.crt;\n    ssl_certificate_key /etc/ssl/private/random27891.example.org.key;\n\n    location /media/ {\n        alias /srv/http/random6344/live/dynamic/public/;\n        expires 7d;\n        include upload_folder_security_params;\n    }\n    location /static/ {\n        alias /srv/http/random6344/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random27891.example.org;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Protocol $scheme;\n    }\n\n    access_log /var/log/nginx/random6344/live/access.log combined_plus;\n    error_log  /var/log/nginx/random6344/live/error.log;\n}\n\nserver {\n    listen 80;\n    server_name random27891.example.org;\n    \n    return 301 https://random27891.example.org$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-8662",
    "content": "upstream django_server_random27507.example.org {\n    server unix:/srv/http/random24211/live/website.sock;\n}\n\nserver {\n    listen 80;\n    server_name random27507.example.org;\n\n    location ^~ /media/ {\n        alias /srv/http/random24211/live/dynamic/public/;\n        expires 7d;\n    }\n    location ^~ /static/ {\n        alias /srv/http/random24211/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random27507.example.org;\n        include          /etc/nginx/proxy_params;\n\n        # You can configure access rules here\n    }\n\n    access_log /var/log/nginx/random24211/live/access.log combined_plus;\n    error_log  /var/log/nginx/random24211/live/error.log;\n}\n\nserver {\n    server_name www.random27507.example.org;\n    rewrite ^ http://random27507.example.org$request_uri permanent;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/79-configs/site-9426",
    "content": "upstream django_server_random20374.nl {\n    server unix:/srv/http/random20374/live/website.sock;\n}\n\nserver {\n    listen 80;\n\n    # Main domain\n    server_name random9123.example.org;\n\n    # So called mini-sites, resulting in landing pages for Google.\n    server_name random16942.example.org;\n    server_name random23560.example.org;\n    server_name random17636.example.org;\n    server_name random13969.example.org;\n    server_name random4892.example.org;\n    server_name random24240.example.org;\n    server_name random25863.example.org;\n    server_name random26503.example.org;\n    server_name random5090.example.org;\n    server_name random1856.example.org;\n    server_name random2911.example.org;\n    server_name random16405.example.org;\n\n    location /media/ {\n        alias /srv/http/random20374/live/dynamic/public/;\n        expires 7d;\n    }\n    location /static/ {\n        alias /srv/http/random20374/live/static_collected/;\n        expires 7d;\n    }\n\n    location / {\n        proxy_pass http://django_server_random20374.nl;\n        include          /etc/nginx/proxy_params;\n    }\n\n    access_log /var/log/nginx/random20374/live/access.log combined_plus;\n    error_log  /var/log/nginx/random20374/live/error.log;\n}\n\nserver {\n    server_name www.random9123.example.org;\n    return 301 $scheme://random9123.example.org$request_uri;\n}\n\nserver {\n    server_name www.random1825.example.org random1825.example.org;\n    return 301 $scheme://random9123.example.org$request_uri;\n}\n\nserver {\n    server_name www.random16942.example.org;\n    return 301 $scheme://random16942.example.org;\n}\n\nserver {\n    server_name www.random23560.example.org;\n    return 301 $scheme://random23560.example.org;\n}\n\nserver {\n    server_name www.random17636.example.org;\n    return 301 $scheme://random17636.example.org;\n}\n\nserver {\n    server_name www.random13969.example.org;\n    return 301 $scheme://random13969.example.org;\n}\n\nserver {\n    server_name www.random4892.example.org;\n    return 301 $scheme://random4892.example.org;\n}\n\nserver {\n    server_name www.random24240.example.org;\n    return 301 $scheme://random24240.example.org;\n}\n\nserver {\n    server_name www.random25863.example.org;\n    return 301 $scheme://random25863.example.org;\n}\n\nserver {\n    server_name www.random26503.example.org;\n    return 301 $scheme://random26503.example.org;\n}\n\nserver {\n    server_name www.random5090.example.org;\n    return 301 $scheme://random5090.example.org;\n}\n\nserver {\n    server_name www.random1856.example.org;\n    return 301 $scheme://random1856.example.org;\n}\n\nserver {\n    server_name www.random2911.example.org;\n    return 301 $scheme://random2911.example.org;\n}\n\nserver {\n    server_name www.random16405.example.org;\n    return 301 $scheme://random16405.example.org;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/activecolab/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       location / {\n                try_files $uri $uri/ /index.php?path_info=$uri&$args;\n                access_log off;\n                expires max;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass unix:/var/run/php5-fpm.sock;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/anothermapcase/nginx.conf",
    "content": "map $uri $blogname{\n    ~^(?P<blogpath>/[^/]+/)files/(.*)       $blogpath ;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi.conf",
    "content": "#-*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n### fastcgi configuration.\nfastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\ninclude fastcgi_params;\nfastcgi_buffers 256 4k;\nfastcgi_intercept_errors on;\n## allow 4 hrs - pass timeout responsibility to upstrea\nfastcgi_read_timeout 14400; \nfastcgi_index index.php;      \n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/fastcgi_params",
    "content": "# -*- mode: conf; mode: flyspell-prog;  ispell-local-dictionary: \"american\" -*-\n### fastcgi parameters.\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\n\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\n\nfastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;\n\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\nfastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\n\n## PHP only, required if PHP was built with --enable-force-cgi-redirect\nfastcgi_param  REDIRECT_STATUS    200;\n## HTTPS 'on' parameter.  This requires Nginx version 1.1.11 or\n## later. The if_not_empty flag was introduced in 1.1.11.  See:\n## http://nginx.org/en/CHANGES. If using a version that doesn't\n## support this comment out the line below.\nfastcgi_param  HTTPS              $https if_not_empty;\n## For Nginx versions below 1.1.11 uncomment the line below after commenting out the above.\n#fastcgi_param  HTTPS            $https  \n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-utf",
    "content": "\n# This map is not a full koi8-r <> utf8 map: it does not contain\n# box-drawing and some other characters.  Besides this map contains\n# several koi8-u and Byelorussian letters which are not in koi8-r.\n# If you need a full and standard map, use contrib/unicode2nginx/koi-utf\n# map instead.\n\ncharset_map  koi8-r  utf-8 {\n\n    80  E282AC ; # euro\n\n    95  E280A2 ; # bullet\n\n    9A  C2A0 ;   # &nbsp;\n\n    9E  C2B7 ;   # &middot;\n\n    A3  D191 ;   # small yo\n    A4  D194 ;   # small Ukrainian ye\n\n    A6  D196 ;   # small Ukrainian i\n    A7  D197 ;   # small Ukrainian yi\n\n    AD  D291 ;   # small Ukrainian soft g\n    AE  D19E ;   # small Byelorussian short u\n\n    B0  C2B0 ;   # &deg;\n\n    B3  D081 ;   # capital YO\n    B4  D084 ;   # capital Ukrainian YE\n\n    B6  D086 ;   # capital Ukrainian I\n    B7  D087 ;   # capital Ukrainian YI\n\n    B9  E28496 ; # numero sign\n\n    BD  D290 ;   # capital Ukrainian soft G\n    BE  D18E ;   # capital Byelorussian short U\n\n    BF  C2A9 ;   # (C)\n\n    C0  D18E ;   # small yu\n    C1  D0B0 ;   # small a\n    C2  D0B1 ;   # small b\n    C3  D186 ;   # small ts\n    C4  D0B4 ;   # small d\n    C5  D0B5 ;   # small ye\n    C6  D184 ;   # small f\n    C7  D0B3 ;   # small g\n    C8  D185 ;   # small kh\n    C9  D0B8 ;   # small i\n    CA  D0B9 ;   # small j\n    CB  D0BA ;   # small k\n    CC  D0BB ;   # small l\n    CD  D0BC ;   # small m\n    CE  D0BD ;   # small n\n    CF  D0BE ;   # small o\n\n    D0  D0BF ;   # small p\n    D1  D18F ;   # small ya\n    D2  D180 ;   # small r\n    D3  D181 ;   # small s\n    D4  D182 ;   # small t\n    D5  D183 ;   # small u\n    D6  D0B6 ;   # small zh\n    D7  D0B2 ;   # small v\n    D8  D18C ;   # small soft sign\n    D9  D18B ;   # small y\n    DA  D0B7 ;   # small z\n    DB  D188 ;   # small sh\n    DC  D18D ;   # small e\n    DD  D189 ;   # small shch\n    DE  D187 ;   # small ch\n    DF  D18A ;   # small hard sign\n\n    E0  D0AE ;   # capital YU\n    E1  D090 ;   # capital A\n    E2  D091 ;   # capital B\n    E3  D0A6 ;   # capital TS\n    E4  D094 ;   # capital D\n    E5  D095 ;   # capital YE\n    E6  D0A4 ;   # capital F\n    E7  D093 ;   # capital G\n    E8  D0A5 ;   # capital KH\n    E9  D098 ;   # capital I\n    EA  D099 ;   # capital J\n    EB  D09A ;   # capital K\n    EC  D09B ;   # capital L\n    ED  D09C ;   # capital M\n    EE  D09D ;   # capital N\n    EF  D09E ;   # capital O\n\n    F0  D09F ;   # capital P\n    F1  D0AF ;   # capital YA\n    F2  D0A0 ;   # capital R\n    F3  D0A1 ;   # capital S\n    F4  D0A2 ;   # capital T\n    F5  D0A3 ;   # capital U\n    F6  D096 ;   # capital ZH\n    F7  D092 ;   # capital V\n    F8  D0AC ;   # capital soft sign\n    F9  D0AB ;   # capital Y\n    FA  D097 ;   # capital Z\n    FB  D0A8 ;   # capital SH\n    FC  D0AD ;   # capital E\n    FD  D0A9 ;   # capital SHCH\n    FE  D0A7 ;   # capital CH\n    FF  D0AA ;   # capital hard sign\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/koi-win",
    "content": "\ncharset_map  koi8-r  windows-1251 {\n\n    80  88 ; # euro\n\n    95  95 ; # bullet\n\n    9A  A0 ; # &nbsp;\n\n    9E  B7 ; # &middot;\n\n    A3  B8 ; # small yo\n    A4  BA ; # small Ukrainian ye\n\n    A6  B3 ; # small Ukrainian i\n    A7  BF ; # small Ukrainian yi\n\n    AD  B4 ; # small Ukrainian soft g\n    AE  A2 ; # small Byelorussian short u\n\n    B0  B0 ; # &deg;\n\n    B3  A8 ; # capital YO\n    B4  AA ; # capital Ukrainian YE\n\n    B6  B2 ; # capital Ukrainian I\n    B7  AF ; # capital Ukrainian YI\n\n    B9  B9 ; # numero sign\n\n    BD  A5 ; # capital Ukrainian soft G\n    BE  A1 ; # capital Byelorussian short U\n\n    BF  A9 ; # (C)\n\n    C0  FE ; # small yu\n    C1  E0 ; # small a\n    C2  E1 ; # small b\n    C3  F6 ; # small ts\n    C4  E4 ; # small d\n    C5  E5 ; # small ye\n    C6  F4 ; # small f\n    C7  E3 ; # small g\n    C8  F5 ; # small kh\n    C9  E8 ; # small i\n    CA  E9 ; # small j\n    CB  EA ; # small k\n    CC  EB ; # small l\n    CD  EC ; # small m\n    CE  ED ; # small n\n    CF  EE ; # small o\n\n    D0  EF ; # small p\n    D1  FF ; # small ya\n    D2  F0 ; # small r\n    D3  F1 ; # small s\n    D4  F2 ; # small t\n    D5  F3 ; # small u\n    D6  E6 ; # small zh\n    D7  E2 ; # small v\n    D8  FC ; # small soft sign\n    D9  FB ; # small y\n    DA  E7 ; # small z\n    DB  F8 ; # small sh\n    DC  FD ; # small e\n    DD  F9 ; # small shch\n    DE  F7 ; # small ch\n    DF  FA ; # small hard sign\n\n    E0  DE ; # capital YU\n    E1  C0 ; # capital A\n    E2  C1 ; # capital B\n    E3  D6 ; # capital TS\n    E4  C4 ; # capital D\n    E5  C5 ; # capital YE\n    E6  D4 ; # capital F\n    E7  C3 ; # capital G\n    E8  D5 ; # capital KH\n    E9  C8 ; # capital I\n    EA  C9 ; # capital J\n    EB  CA ; # capital K\n    EC  CB ; # capital L\n    ED  CC ; # capital M\n    EE  CD ; # capital N\n    EF  CE ; # capital O\n\n    F0  CF ; # capital P\n    F1  DF ; # capital YA\n    F2  D0 ; # capital R\n    F3  D1 ; # capital S\n    F4  D2 ; # capital T\n    F5  D3 ; # capital U\n    F6  C6 ; # capital ZH\n    F7  C2 ; # capital V\n    F8  DC ; # capital soft sign\n    F9  DB ; # capital Y\n    FA  C7 ; # capital Z\n    FB  D8 ; # capital SH\n    FC  DD ; # capital E\n    FD  D9 ; # capital SHCH\n    FE  D7 ; # capital CH\n    FF  DA ; # capital hard sign\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/map_https_fcgi.conf",
    "content": "# -*- mode: conf; mode: flyspell-prog;  ispell-local-dictionary: \"american\" -*-\n### Implement the $https_if_not_empty variable for Nginx versions below 1.1.11.\n\nmap $scheme $https {\n    default '';\n    https on;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/mime.types",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-current-dictionary: american -*-\ntypes {\n    text/html                             html htm shtml;\n    text/css                              css;\n    text/xml                              xml rss;\n    image/gif                             gif;\n    image/jpeg                            jpeg jpg;\n    application/x-javascript              js;\n    application/atom+xml                  atom;\n\n    text/mathml                           mml;\n    text/plain                            txt;\n    text/vnd.sun.j2me.app-descriptor      jad;\n    text/vnd.wap.wml                      wml;\n    text/x-component                      htc;\n\n    image/png                             png;\n    image/tiff                            tif tiff;\n    image/vnd.wap.wbmp                    wbmp;\n    image/x-icon                          ico;\n    image/x-jng                           jng;\n    image/x-ms-bmp                        bmp;\n    image/svg+xml                         svg svgz;\n\n    application/java-archive              jar war ear;\n    application/mac-binhex40              hqx;\n    application/msword                    doc;\n    application/pdf                       pdf;\n    application/postscript                ps eps ai;\n    application/rtf                       rtf;\n    application/vnd.ms-excel              xls;\n    application/vnd.ms-powerpoint         ppt;\n    application/vnd.wap.wmlc              wmlc;\n    application/vnd.wap.xhtml+xml         xhtml;\n    application/x-7z-compressed           7z;\n    application/x-cocoa                   cco;\n    application/x-java-archive-diff       jardiff;\n    application/x-java-jnlp-file          jnlp;\n    application/x-makeself                run;\n    application/x-perl                    pl pm;\n    application/x-pilot                   prc pdb;\n    application/x-rar-compressed          rar;\n    application/x-redhat-package-manager  rpm;\n    application/x-sea                     sea;\n    application/x-shockwave-flash         swf;\n    application/x-stuffit                 sit;\n    application/x-tcl                     tcl tk;\n    application/x-x509-ca-cert            der pem crt;\n    application/x-xpinstall               xpi;\n    application/zip                       zip;\n\n    # Mime types for web fonts. Stolen from here:\n    # http://seconddrawer.com.au/blog/ in part.\n    application/x-font-ttf                ttf;\n    font/opentype                         otf;\n    application/vnd.ms-fontobject         eot;\n    application/x-woff                    woff;\n\n    application/octet-stream              bin exe dll;\n    application/octet-stream              deb;\n    application/octet-stream              dmg;\n    application/octet-stream              iso img;\n    application/octet-stream              msi msp msm;\n\n    audio/midi                            mid midi kar;\n    audio/mpeg                            mp3;\n    audio/x-realaudio                     ra;\n\n    video/3gpp                            3gpp 3gp;\n    video/mpeg                            mpeg mpg;\n    video/quicktime                       mov;\n    video/x-flv                           flv;\n    video/x-mng                           mng;\n    video/x-ms-asf                        asx asf;\n    video/x-ms-wmv                        wmv;\n    video/x-msvideo                       avi;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/nginx.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\nuser www-data;\nworker_processes  4;\n\nerror_log  /var/log/nginx/error.log;\npid        /var/run/nginx.pid;\n\nworker_rlimit_nofile 8192;\n\nevents {\n    worker_connections 4096;\n    ## epoll is preferred on 2.6 Linux\n    ## kernels. Cf. http://www.kegel.com/c10k.html#nb.epoll\n    use epoll;\n    ## Accept as many connections as possible.\n    multi_accept on;\n}\n\nhttp {\n    ## MIME types.\n    include /etc/nginx/mime.types;\n    default_type application/octet-stream;\n\n    ## FastCGI.\n    include /etc/nginx/fastcgi.conf;\n\n    ## Default log and error files.\n    access_log /var/log/nginx/access.log;\n    error_log /var/log/nginx/error.log;\n\n    ## Use sendfile() syscall to speed up I/O operations and speed up\n    ## static file serving.\n    sendfile on;\n    ## Handling of IPs in proxied and load balancing situations.\n    set_real_ip_from 0.0.0.0/32; # all addresses get a real IP.\n    real_ip_header X-Forwarded-For; # the ip is forwarded from the load balancer/proxy\n\n    ## Define a zone for limiting the number of simultaneous\n    ## connections nginx accepts. 1m means 32000 simultaneous\n    ## sessions. We need to define for each server the limit_conn\n    ## value referring to this or other zones.\n    ## ** This syntax requires nginx version >=\n    ## ** 1.1.8. Cf. http://nginx.org/en/CHANGES. If using an older\n    ## ** version then use the limit_zone directive below\n    ## ** instead. Comment out this\n    ## ** one if not using nginx version >= 1.1.8.\n    limit_conn_zone $binary_remote_addr zone=arbeit:10m;\n\n    ## Timeouts.\n    client_body_timeout             60;\n    client_header_timeout           60;\n    keepalive_timeout            10 10;\n    send_timeout                    60;\n\n    ## Reset lingering timed out connections. Deflect DDoS.\n    reset_timedout_connection on;\n\n    ## Body size.\n    client_max_body_size 10m;\n\n    ## TCP options.\n    tcp_nodelay on;\n    tcp_nopush on;\n\n    ## Compression.\n    gzip              on;\n    gzip_buffers      16 8k;\n    gzip_comp_level   1;\n    gzip_http_version 1.1;\n    gzip_min_length   10;\n    gzip_types        text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon application/vnd.ms-fontobject font/opentype application/x-font-ttf;\n    gzip_vary         on;\n    gzip_proxied      any; # Compression for all requests.\n    ## No need for regexps. See\n    ## http://wiki.nginx.org/NginxHttpGzipModule#gzip_disable\n    gzip_disable \"msie6\";\n\n    ## Serve already compressed files directly, bypassing on-the-fly\n    ## compression.\n    gzip_static on;\n\n    ## Hide the Nginx version number.\n    server_tokens off;\n\n    ## Use a SSL/TLS cache for SSL session resume. This needs to be\n    ## here (in this context, for session resumption to work. See this\n    ## thread on the Nginx mailing list:\n    ## http://nginx.org/pipermail/nginx/2010-November/023736.html.\n    ssl_session_cache shared:SSL:10m;\n    ssl_session_timeout 10m;\n\n    ## For the filefield_nginx_progress module to work. From the\n    ## README. Reserve 1MB under the name 'uploads' to track uploads.\n    upload_progress uploads 1m;\n\n    ## Enable clickjacking protection in modern browsers. Available in\n    ## IE8 also. See\n    ## https://developer.mozilla.org/en/The_X-FRAME-OPTIONS_response_header\n    add_header X-Frame-Options sameorigin;\n\n    ## Include the upstream servers for PHP FastCGI handling config.\n    include upstream_phpcgi.conf;\n\n    ## If using Nginx version >= 1.1.11 then there's a $https variable\n    ## that has the value 'on' if the used scheme is https and '' if not.\n    ## See: http://trac.nginx.org/nginx/changeset/4380/nginx\n    ## http://trac.nginx.org/nginx/changeset/4333/nginx and\n    ## http://trac.nginx.org/nginx/changeset/4334/nginx. If using a\n    ## previous version then uncomment out the line below.\n    #include map_https_fcgi.conf;\n\n    ## Include the upstream servers for Apache handling the PHP\n    ## processes. In this case Nginx functions as a reverse proxy.\n    #include reverse_proxy.conf;\n    #include upstream_phpapache.conf;\n\n    ## Include all vhosts.\n    include /etc/nginx/sites-enabled/*;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/reverse_proxy.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n\n### Configuration for reverse proxy. Passing the necessary headers to\n### the backend. Nginx doesn't tunnel the connection, it opens a new\n### one. Hence we need to send these headers to the backend so that\n### the client(s) IP is available to them. The host is also sent.\n\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header Host $http_host; \n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/000-default",
    "content": "# -*-mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n### Block all illegal host headers. Taken from a discussion on nginx\n### forums. Cf. http://forum.nginx.org/read.php?2,3482,3518 following\n### a suggestion by Maxim Dounin. Also suggested in\n### http://nginx.org/en/docs/http/request_processing.html.\nserver {\n    listen [::]:80 default_server;\n    # Uncomment the line below and comment the above if you're\n    # running a Nginx version less than 0.8.20.\n    # listen [::]:80 default;\n\n    # Accept redirects based on the value of the Host header. If\n    # there's no valid vhost configuration file with a\n    # corresponding server_name directive then signal an error and\n    # fail silently. See:\n    # http://wiki.nginx.org/NginxHttpCoreModule#server_name_in_redirect\n    server_name_in_redirect off;\n    return 444;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/chive.example.com.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n### Nginx configuration for Chive.\n\nserver {\n    ## This is to avoid the spurious if for sub-domain name\n    ## rewriting. See http://wiki.nginx.org/Pitfalls#Server_Name.\n    listen 80; # IPv4\n\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on;\n\n    server_name www.chive.example.com;\n\n    return 301 $scheme://chive.example.com$request_uri;\n\n} # server domain rewrite.\n\nserver {\n    listen 80; # IPv4\n\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on;\n\n    limit_conn arbeit 32;\n    server_name chive.example.com;\n\n    ## Parameterization using hostname of access and log filenames.\n    access_log /var/log/nginx/chive.example.com_access.log;\n    error_log /var/log/nginx/chive.example.com_error.log;\n\n    root /var/www/sites/chive.example.com;\n    index index.php index.html;\n\n    ## Support for favicon. Return a 204 (No Content) if the favicon\n    ## doesn't exist.\n    location = /favicon.ico {\n        try_files /favicon.ico =204;\n    }\n\n    ## The main location is accessed using Basic Auth.\n    location / {\n        ## Access is restricted.\n        auth_basic \"Restricted Access\"; # auth realm\n        auth_basic_user_file .htpasswd-users; # htpasswd file\n\n        ## Use PATH_INFO for translating the requests to the\n        ## FastCGI. This config follows Igor's suggestion here:\n        ## http://forum.nginx.org/read.php?2,124378,124582.\n        ## This is preferable to using:\n        ## fastcgi_split_path_info ^(.+\\.php)(.*)$\n        ## It saves one regex in the location. Hence it's faster.\n        location ~ ^(?<script>.+\\.php)(?<path_info>.*)$ {\n            include fastcgi.conf;\n            ## The fastcgi_params must be redefined from the ones\n            ## given in fastcgi.conf. No longer standard names\n            ## but arbitrary: named patterns in regex.\n            fastcgi_param SCRIPT_FILENAME $document_root$script;\n            fastcgi_param SCRIPT_NAME $script;\n            fastcgi_param PATH_INFO $path_info;\n            ## Passing the request upstream to the FastCGI\n            ## listener.\n            fastcgi_pass phpcgi;\n        }\n\n        ## Protect these locations. Replicating the .htaccess\n        ## rules throughout the chive distro.\n        location /protected {\n            internal;\n        }\n\n        location /yii {\n            internal;\n        }\n\n        ## Static file handling.\n        location ~* .+\\.(?:css|gif|htc|js|jpe?g|png|swf)$ {\n            expires max;\n            ## No need to bleed constant updates. Send the all shebang in one\n            ## fell swoop.\n            tcp_nodelay off;\n            ## Set the OS file cache.\n            open_file_cache max=100 inactive=120s;\n            open_file_cache_valid 45s;\n            open_file_cache_min_uses 2;\n            open_file_cache_errors off;\n        }\n    }\n\n    ## We need to capture the case where the index.php is missing,\n    ## hence we drop out of the path info thingie.\n    location ~* /([^\\.])$ {\n        return 302 /index.php/$1;\n    }\n\n    ## Close up git repo access.\n    location ^~ /.git {\n        return 404;\n    }\n\n} # server\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/sites-available/secure.chive.example.com.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n### Nginx configuration for Chive with HTTPS.\n\nserver {\n    ## This is to avoid the spurious if for sub-domain name\n    ## rewriting. See http://wiki.nginx.org/Pitfalls#Server_Name.\n    listen 80; # IPv4\n\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on;\n\n    server_name chive.example.com;\n    return 301 https://chive.example.com$request_uri;\n\n} # server domain rewrite.\n\nserver {\n    ## This is to avoid the spurious if for sub-domain name\n    ## rewriting. See http://wiki.nginx.org/Pitfalls#Server_Name.\n    listen 80; # IPv4\n\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:80 ipv6only=on;\n\n    listen 443 ssl; # IPv4\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:443 ssl ipv6only=on;\n\n    server_name www.chive.example.com;\n\n    ## Server certificate and key.\n    ssl_certificate /etc/ssl/certs/chive.example.com-cert.pem;\n    ssl_certificate_key /etc/ssl/private/chive.example.com-key.pem;\n\n    ## Use only HTTPS.\n    return 301 https://chive.example.com$request_uri;\n\n} # server domain rewrite.\n\nserver {\n    listen 443 ssl; # IPv4\n    ## Replace the IPv6 address by your own address. The address below\n    ## was stolen from the wikipedia page on IPv6.\n    listen [fe80::202:b3ff:fe1e:8329]:443 ssl ipv6only=on;\n\n    limit_conn arbeit 32;\n    server_name chive.example.com;\n\n    ## Keep alive timeout set to a greater value for SSL/TLS.\n    keepalive_timeout 75 75;\n\n    ## Parameterization using hostname of access and log filenames.\n    access_log /var/log/nginx/chive.example.com_access.log;\n    error_log /var/log/nginx/chive.example.com_error.log;\n\n    ## Server certificate and key.\n    ssl_certificate /etc/ssl/certs/chive.example.com-cert.pem;\n    ssl_certificate_key /etc/ssl/private/chive.example.com-key.pem;\n\n    ## Strict Transport Security header for enhanced security. See\n    ## http://www.chromium.org/sts.\n    add_header Strict-Transport-Security \"max-age=12960000\";\n\n    root /var/www/sites/chive.example.com/;\n    index index.php index.html;\n\n    ## Support for favicon. Return a 204 (No Content) if the favicon\n    ## doesn't exist.\n    location = /favicon.ico {\n        try_files /favicon.ico =204;\n    }\n\n    ## The main location is accessed using Basic Auth.\n    location / {\n        ## Access is restricted.\n        auth_basic \"Restricted Access\"; # auth realm\n        auth_basic_user_file .htpasswd-users; # htpasswd file\n\n        ## Use PATH_INFO for translating the requests to the\n        ## FastCGI. This config follows Igor's suggestion here:\n        ## http://forum.nginx.org/read.php?2,124378,124582.\n        ## This is preferable to using:\n        ## fastcgi_split_path_info ^(.+\\.php)(.*)$\n        ## It saves one regex in the location. Hence it's faster.\n        location ~ ^(?<script>.+\\.php)(?<path_info>.*)$ {\n            include fastcgi.conf;\n            ## The fastcgi_params must be redefined from the ones\n            ## given in fastcgi.conf. No longer standard names\n            ## but arbitrary: named patterns in regex.\n            fastcgi_param SCRIPT_FILENAME $document_root$script;\n            fastcgi_param SCRIPT_NAME $script;\n            fastcgi_param PATH_INFO $path_info;\n            ## Passing the request upstream to the FastCGI\n            ## listener.\n            fastcgi_pass php-cgi;\n        }\n\n        ## Protect these locations. Replicating the .htaccess\n        ## rules throughout the chive distro.\n        location /protected {\n            internal;\n        }\n        location /yii {\n            internal;\n        }\n\n        ## Static file handling.\n        location ~* .+\\.(?:css|gif|htc|js|jpe?g|png)$ {\n            expires max;\n            ## No need to bleed constant updates. Send the all shebang in one\n            ## fell swoop.\n            tcp_nodelay off;\n            ## Set the OS file cache.\n            open_file_cache max=100 inactive=120s;\n            open_file_cache_valid 45s;\n            open_file_cache_min_uses 2;\n            open_file_cache_errors off;\n        }\n    }\n\n    ## We need to capture the case where the index.php is missing,\n    ## hence we drop out of the path info thingie.\n    location ~* /([^\\.])$ {\n        return 302 /index.php/$1;\n    }\n\n    ## Close up git repo access.\n    location ^~ /.git {\n        return 404;\n    }\n\n} # server\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/upstream_phpapache.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n\n### Upstream configuration for Apache functioning has a PHP handler.\n\n## Add as many servers as needed. Cf. http://wiki.nginx.org/HttpUpstreamModule.\nupstream phpapache {\n    server 127.0.0.1:8080;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/upstream_phpcgi.conf",
    "content": "# -*- mode: nginx; mode: flyspell-prog; mode: autopair; ispell-local-dictionary: \"american\" -*-\n\n### Upstream configuration for PHP FastCGI.\n\n## Add as many servers as needed. Cf. http://wiki.nginx.org/HttpUpstreamModule.\nupstream phpcgi {\n    server unix:/var/run/php-fpm.sock;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/chive/chive-nginx-master/win-utf",
    "content": "\n# This map is not a full windows-1251 <> utf8 map: it does not\n# contain Serbian and Macedonian letters.  If you need a full map,\n# use contrib/unicode2nginx/win-utf map instead.\n\ncharset_map  windows-1251  utf-8 {\n\n    82  E2809A ; # single low-9 quotation mark\n\n    84  E2809E ; # double low-9 quotation mark\n    85  E280A6 ; # ellipsis\n    86  E280A0 ; # dagger\n    87  E280A1 ; # double dagger\n    88  E282AC ; # euro\n    89  E280B0 ; # per mille\n\n    91  E28098 ; # left single quotation mark\n    92  E28099 ; # right single quotation mark\n    93  E2809C ; # left double quotation mark\n    94  E2809D ; # right double quotation mark\n    95  E280A2 ; # bullet\n    96  E28093 ; # en dash\n    97  E28094 ; # em dash\n\n    99  E284A2 ; # trade mark sign\n\n    A0  C2A0 ;   # &nbsp;\n    A1  D18E ;   # capital Byelorussian short U\n    A2  D19E ;   # small Byelorussian short u\n\n    A4  C2A4 ;   # currency sign\n    A5  D290 ;   # capital Ukrainian soft G\n    A6  C2A6 ;   # broken bar\n    A7  C2A7 ;   # section sign\n    A8  D081 ;   # capital YO\n    A9  C2A9 ;   # (C)\n    AA  D084 ;   # capital Ukrainian YE\n    AB  C2AB ;   # left-pointing double angle quotation mark\n    AC  C2AC ;   # not sign\n    AD  C2AD ;   # soft hyphen\n    AE  C2AE ;   # (R)\n    AF  D087 ;   # capital Ukrainian YI\n\n    B0  C2B0 ;   # &deg;\n    B1  C2B1 ;   # plus-minus sign\n    B2  D086 ;   # capital Ukrainian I\n    B3  D196 ;   # small Ukrainian i\n    B4  D291 ;   # small Ukrainian soft g\n    B5  C2B5 ;   # micro sign\n    B6  C2B6 ;   # pilcrow sign\n    B7  C2B7 ;   # &middot;\n    B8  D191 ;   # small yo\n    B9  E28496 ; # numero sign\n    BA  D194 ;   # small Ukrainian ye\n    BB  C2BB ;   # right-pointing double angle quotation mark\n\n    BF  D197 ;   # small Ukrainian yi\n\n    C0  D090 ;   # capital A\n    C1  D091 ;   # capital B\n    C2  D092 ;   # capital V\n    C3  D093 ;   # capital G\n    C4  D094 ;   # capital D\n    C5  D095 ;   # capital YE\n    C6  D096 ;   # capital ZH\n    C7  D097 ;   # capital Z\n    C8  D098 ;   # capital I\n    C9  D099 ;   # capital J\n    CA  D09A ;   # capital K\n    CB  D09B ;   # capital L\n    CC  D09C ;   # capital M\n    CD  D09D ;   # capital N\n    CE  D09E ;   # capital O\n    CF  D09F ;   # capital P\n\n    D0  D0A0 ;   # capital R\n    D1  D0A1 ;   # capital S\n    D2  D0A2 ;   # capital T\n    D3  D0A3 ;   # capital U\n    D4  D0A4 ;   # capital F\n    D5  D0A5 ;   # capital KH\n    D6  D0A6 ;   # capital TS\n    D7  D0A7 ;   # capital CH\n    D8  D0A8 ;   # capital SH\n    D9  D0A9 ;   # capital SHCH\n    DA  D0AA ;   # capital hard sign\n    DB  D0AB ;   # capital Y\n    DC  D0AC ;   # capital soft sign\n    DD  D0AD ;   # capital E\n    DE  D0AE ;   # capital YU\n    DF  D0AF ;   # capital YA\n\n    E0  D0B0 ;   # small a\n    E1  D0B1 ;   # small b\n    E2  D0B2 ;   # small v\n    E3  D0B3 ;   # small g\n    E4  D0B4 ;   # small d\n    E5  D0B5 ;   # small ye\n    E6  D0B6 ;   # small zh\n    E7  D0B7 ;   # small z\n    E8  D0B8 ;   # small i\n    E9  D0B9 ;   # small j\n    EA  D0BA ;   # small k\n    EB  D0BB ;   # small l\n    EC  D0BC ;   # small m\n    ED  D0BD ;   # small n\n    EE  D0BE ;   # small o\n    EF  D0BF ;   # small p\n\n    F0  D180 ;   # small r\n    F1  D181 ;   # small s\n    F2  D182 ;   # small t\n    F3  D183 ;   # small u\n    F4  D184 ;   # small f\n    F5  D185 ;   # small kh\n    F6  D186 ;   # small ts\n    F7  D187 ;   # small ch\n    F8  D188 ;   # small sh\n    F9  D189 ;   # small shch\n    FA  D18A ;   # small hard sign\n    FB  D18B ;   # small y\n    FC  D18C ;   # small soft sign\n    FD  D18D ;   # small e\n    FE  D18E ;   # small yu\n    FF  D18F ;   # small ya\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/cms-made-simple/nginx.conf",
    "content": "server {\n        listen 80;\n        server_name .domain.tld;\n        root /var/www/cms;  # Directory root of your CMS.\n        index index.php index.html index.htm;\n\n        location / {\n                try_files $uri $uri/ /index.php?page=$request_uri;\n        }\n\n        location ~ \\.php$ {\n                include fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/codeigniter/nginx-alt.conf",
    "content": "server {\n        listen       80;\n        server_name  localhost;\n        root   /var/www/html/ci;\n        autoindex on;\n        index index.php;\n\n        location / {\n\n            try_files $uri $uri/ /index.php;\n\n            location = /index.php {\n\n                fastcgi_pass   127.0.0.1:6969;\n                fastcgi_param  SCRIPT_FILENAME /var/www/html/ci$fastcgi_script_name;\n                include        fastcgi_params;\n            }\n        }\n\n        location ~ \\.php$ {\n            return 444;\n        }\n\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/codeigniter/nginx.conf",
    "content": "server {\n        server_name domain.tld;\n\n        root /var/www/codeignitor;\n        index index.html index.php;\n\n        # set expiration of assets to MAX for caching\n        location ~* \\.(ico|css|js|gif|jpe?g|png)(\\?[0-9]+)?$ {\n                expires max;\n                log_not_found off;\n        }\n\n        location / {\n                # Check if a file or directory index file exists, else route it to index.php.\n                try_files $uri $uri/ /index.php;\n        }\n\n        location ~* \\.php$ {\n                fastcgi_pass 127.0.0.1:9000;\n                include fastcgi.conf;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/contao/sites-available/example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       # Make sure files with the following extensions do not get loaded by nginx because nginx would display the source code, and these files can contain PASSWORDS!\n        location ~* \\.(tpl|html5|xhtml)$ {\n                deny all;\n        }\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n       location / {\n                try_files $uri $uri/ /index.php?$args;\n       }\n       location ~*  \\.(jpg|jpeg|png|gif|css|js|ico)$ {\n                expires max;\n                log_not_found off;\n       }\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/cs-cart/sites-available/example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       client_max_body_size 100M;\n\n       rewrite /api/(.*)$ /api.php?_d=$1&ajax_custom=1 last;\n\n       location ~ \\.(png|gif|ico|swf|jpe?g|js|css|ttf|svg|eot|woff)$ {\n                if (!-e $request_filename){\n                    rewrite ^/(.*?)\\/(.*)$ /$2 last;\n                }\n                expires 1w;\n       }\n\t   \n       location ~ store_closed.html$ {\n                if (!-e $request_filename){\n                    rewrite ^/(.*?)\\/(.*)$ /$2 last;\n                }\n       }\n\n       location / {\n                index index.php;\n                try_files $uri $uri/ /index.php?sef_rewrite=1&$args;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass unix:/var/run/php5-fpm.sock;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n                fastcgi_temp_file_write_size 10m;\n                fastcgi_busy_buffers_size    512k;\n                fastcgi_buffer_size          512k;\n                fastcgi_buffers           16 512k;\n                fastcgi_read_timeout 1200;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/djangofastcgi/large.conf",
    "content": "pid /project_location/log/nginx.pid;\nworker_processes  2;\nerror_log /project_location/log/error_log;\n\nevents {\n  worker_connections  1024;\n  use epoll;\n}\n\nhttp {\n  # default nginx location\n  include        /usr/local/nginx/mime.types;\n  default_type    application/octet-stream;\n  log_format main\n      '$remote_addr - $remote_user [$time_local] '\n      '\"$request\" $status $bytes_sent '\n      '\"$http_referer\" \"$http_user_agent\" '\n      '\"$gzip_ratio\"';\n\n  client_header_timeout  3m;\n  client_body_timeout    3m;\n  send_timeout           3m;\n  connection_pool_size        256;\n  client_header_buffer_size    1k;\n  large_client_header_buffers    4 2k;\n  request_pool_size        4k;\n  output_buffers   4 32k;\n  postpone_output  1460;\n  sendfile        on;\n  tcp_nopush             on;\n  keepalive_timeout      75 20;\n  tcp_nodelay            on;\n\n  client_max_body_size       10m;\n  client_body_buffer_size    256k;\n  proxy_connect_timeout      90;\n  proxy_send_timeout         90;\n  proxy_read_timeout         90;\n  client_body_temp_path      /project_location/log/client_body_temp;\n  proxy_temp_path            /project_location/log/proxy_temp;\n  fastcgi_temp_path            /project_location/log/fastcgi_temp;\n\n  gzip on;\n  gzip_min_length  1100;\n  gzip_buffers     4 32k;\n  gzip_types       text/plain text/html application/x-javascript text/xml text/css;\n\n  ignore_invalid_headers    on;\n\n  server {\n    listen 8000;\n    server_name localhost;\n    index index.html;\n    root   project_location/public;\n    # static resources\n\n    location ~* ^.+\\.(html|jpg|jpeg|gif|png|ico|css|zip|tgz|gz|rar|bz2|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js)$\n    {\n      expires 30d;\n      break;\n    }\n\n    location / {\n      # host and port to fastcgi server\n      fastcgi_pass unix:/project_location/log/django.sock;\n      fastcgi_param PATH_INFO $fastcgi_script_name;\n      fastcgi_param REQUEST_METHOD $request_method;\n      fastcgi_param QUERY_STRING $query_string;\n      fastcgi_param CONTENT_TYPE $content_type;\n      fastcgi_param CONTENT_LENGTH $content_length;\n      fastcgi_pass_header Authorization;\n      fastcgi_intercept_errors off;\n    }\n\n    location /403.html {\n      root   /usr/local/nginx;\n      access_log   off;\n    }\n\n    location /401.html {\n      root   /usr/local/nginx;\n      access_log   off;\n    }\n\n    location /404.html {\n      root   /usr/local/nginx;\n      access_log   off;\n    }\n\n    location = /_.gif {\n      empty_gif;\n      access_log   off;\n    }\n\n    access_log    /project_location/log/localhost.access_log main;\n    error_log    /project_location/log/localhost.error_log;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/djangofastcgi/nginx.conf",
    "content": "# inside a http section\n# replace the several paths and names\nserver {\n    listen   80;\n    server_name  servername;\n\n    access_log  /path/to/log/file;\n\n    location /media {\n        root   /path/to/sites/siteYY/;\n        # I use a symbolic link called \"admin\" in the media/ folder\n        # (pointing to /usr/local/lib/python2.6/dist-packages/django/contrib/admin/media/ in my case)\n        # as suggested in http://docs.djangoproject.com/en/dev/howto/deployment/modpython/#serving-the-admin-files\n        # so that nginx serves the django admin media files with the parameter\n        # ADMIN_MEDIA_PREFIX set to '/media/admin/' in settings.py\n    }\n\n    location / {\n        fastcgi_pass unix:RUNFILES_PATH/siteYY.socket;\n        # for a TCP host/port:\n        # fastcgi_pass   {hostname}:{port};\n\n        # necessary parameter\n        fastcgi_param PATH_INFO $fastcgi_script_name;\n\n        # to deal with POST requests\n        fastcgi_param REQUEST_METHOD $request_method;\n        fastcgi_param CONTENT_TYPE $content_type;\n        fastcgi_param CONTENT_LENGTH $content_length;\n\n        # http://stackoverflow.com/questions/605173/how-to-nginx-virtual-servers-fcgi-for-django uses many other parameters,\n        # some may be necessary in some situations\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/dokuwiki.conf",
    "content": "include drop.conf;\n\nclient_max_body_size 15M;\nclient_body_buffer_size 128k;\nlocation / {\n    try_files $uri $uri/ @dw;\n}\n\nlocation @dw {\n    rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;\n    rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;\n    rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;\n    rewrite ^/(.*) /doku.php?id=$1 last;\n}\n\nlocation ~ \\.php$ {\n    include fastcgi_params;\n    fastcgi_param HTTPS $php_https;#DW checks $_SERVER['HTTPS']\n    fastcgi_pass unix:/tmp/php5-fpm.sock;\n}\n\n# Block access to data folders\nlocation ~ /(data|conf|bin|inc)/ {\ndeny all;\n}\n\n# Block access to .htaccess files\nlocation ~ /\\.ht {\ndeny  all;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/drop.conf",
    "content": "location = /robots.txt  { access_log off; log_not_found off; }\nlocation = /favicon.ico { access_log off; log_not_found off; }\nlocation ~ /\\.          { access_log off; log_not_found off; deny all; }\nlocation ~ ~$           { access_log off; log_not_found off; deny all; }\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/full.conf",
    "content": "server {\n  server_name wiki.ulyaoth.net;\n  listen 80;\n  autoindex off;\n  client_max_body_size 15M;\n  client_body_buffer_size 128k;\n  index index.html index.htm index.php doku.php;\n  access_log  /var/log/nginx/wiki.ulyaoth.net/access.log;\n  error_log  /var/log/nginx/wiki.ulyaoth.net/error.log;\n  root /usr/share/nginx/dokuwiki;\n\n  location / {\n    try_files $uri $uri/ @dokuwiki;\n  }\n\n  location ~ ^/lib.*\\.(gif|png|ico|jpg)$ {\n    expires 30d;\n  }\n\n  location = /robots.txt  { access_log off; log_not_found off; }\n  location = /favicon.ico { access_log off; log_not_found off; }\n  location ~ /\\.          { access_log off; log_not_found off; deny all; }\n  location ~ ~$           { access_log off; log_not_found off; deny all; }\n\n  location @dokuwiki {\n    rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;\n    rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;\n    rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;\n    rewrite ^/(.*) /doku.php?id=$1 last;\n  }\n\n  location ~ \\.php$ {\n    try_files $uri =404;\n    fastcgi_pass   unix:/var/run/php-fpm/wiki.ulyaoth.net.sock;\n    fastcgi_index  index.php;\n    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\n    include /etc/nginx/fastcgi_params;\n    fastcgi_param  QUERY_STRING     $query_string;\n    fastcgi_param  REQUEST_METHOD   $request_method;\n    fastcgi_param  CONTENT_TYPE     $content_type;\n    fastcgi_param  CONTENT_LENGTH   $content_length;\n    fastcgi_intercept_errors        on;\n    fastcgi_ignore_client_abort     off;\n    fastcgi_connect_timeout 60;\n    fastcgi_send_timeout 180;\n    fastcgi_read_timeout 180;\n    fastcgi_buffer_size 128k;\n    fastcgi_buffers 4 256k;\n    fastcgi_busy_buffers_size 256k;\n    fastcgi_temp_file_write_size 256k;\n  }\n\n  location ~ /(data|conf|bin|inc)/ {\n    deny all;\n  }\n\n  location ~ /\\.ht {\n    deny  all;\n  }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/nginx-no-ssl.conf",
    "content": "server {\n  server_name wiki.domain.tld;\n  root /var/www/dokuwiki;\n\n  location / {\n    index doku.php;\n    try_files $uri $uri/ @dokuwiki;\n  }\n\n  location ~ ^/lib.*\\.(gif|png|ico|jpg)$ {\n    expires 30d;\n  }\n\n  location ^~ /conf/ { return 403; }\n  location ^~ /data/ { return 403; }\n\n  location @dokuwiki {\n    rewrite ^/_media/(.*) /lib/exe/fetch.php?media=$1 last;\n    rewrite ^/_detail/(.*) /lib/exe/detail.php?media=$1 last;\n    rewrite ^/_export/([^/]+)/(.*) /doku.php?do=export_$1&id=$2 last;\n    rewrite ^/(.*) /doku.php?id=$1 last;\n  }\n\n  location ~ \\.php$ {\n    include fastcgi_params;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n    fastcgi_pass unix:/tmp/phpcgi.socket;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dokuwiki/nginx.conf",
    "content": "map $scheme $php_https { default off; https on; }\n\n    server {\n      server_name wiki.host.org\n      root /path/to/dokuwiki;\n      index doku.php;\n      listen 80;\n      #Enforce https for logins, admin\n      if ($args ~* do=(log|admin|profile)) {\n        rewrite ^ https://$host$request_uri? redirect;\n      }\n      include dokuwiki.conf;\n    }\n\n    server {\n      server_name wiki.host.org;\n      root /path/to/dokuwiki;\n      index doku.php;\n      listen 443 ssl;\n      keepalive_requests    10;\n      keepalive_timeout     60 60;\n      ssl_certificate      /etc/ssl/certs/ssl-cert-snakeoil.pem;\n      ssl_certificate_key  /etc/ssl/private/ssl-cert-snakeoil.key;\n      #switch back to plain http for normal view\n\n      if ($args ~* (do=show|^$)){\n        rewrite ^ http://$host$request_uri? redirect;\n      }\n      include dokuwiki.conf;\n    }\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/drupal/nginx.conf",
    "content": "server {\n    server_name example.com;\n    root /var/www/drupal8; ## <-- Your only path reference.\n\n    location = /favicon.ico {\n        log_not_found off;\n        access_log off;\n    }\n\n    location = /robots.txt {\n        allow all;\n        log_not_found off;\n        access_log off;\n    }\n\n    # Very rarely should these ever be accessed outside of your lan\n    location ~* \\.(txt|log)$ {\n        allow 192.168.0.0/16;\n        deny all;\n    }\n\n    location ~ \\..*/.*\\.php$ {\n        return 403;\n    }\n\n    location ~ ^/sites/.*/private/ {\n        return 403;\n    }\n\n    # Allow \"Well-Known URIs\" as per RFC 5785\n    location ~* ^/.well-known/ {\n        allow all;\n    }\n\n    # Block access to \"hidden\" files and directories whose names begin with a\n    # period. This includes directories used by version control systems such\n    # as Subversion or Git to store control files.\n    location ~ (^|/)\\. {\n        return 403;\n    }\n\n    location / {\n        # try_files $uri @rewrite; # For Drupal <= 6\n        try_files $uri /index.php?$query_string; # For Drupal >= 7\n    }\n\n    location @rewrite {\n        rewrite ^/(.*)$ /index.php?q=$1;\n    }\n\n    # Don't allow direct access to PHP files in the vendor directory.\n    location ~ /vendor/.*\\.php$ {\n        deny all;\n        return 404;\n    }\n\n    # In Drupal 8, we must also match new paths where the '.php' appears in\n    # the middle, such as update.php/selection. The rule we use is strict,\n    # and only allows this pattern with the update.php front controller.\n    # This allows legacy path aliases in the form of\n    # blog/index.php/legacy-path to continue to route to Drupal nodes. If\n    # you do not have any paths like that, then you might prefer to use a\n    # laxer rule, such as:\n    #   location ~ \\.php(/|$) {\n    # The laxer rule will continue to work if Drupal uses this new URL\n    # pattern with front controllers other than update.php in a future\n    # release.\n    location ~ '\\.php$|^/update.php' {\n        fastcgi_split_path_info ^(.+?\\.php)(|/.*)$;\n        # Security note: If you're running a version of PHP older than the\n        # latest 5.3, you should have \"cgi.fix_pathinfo = 0;\" in php.ini.\n        # See http://serverfault.com/q/627903/94922 for details.\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_param PATH_INFO $fastcgi_path_info;\n        fastcgi_intercept_errors on;\n        fastcgi_pass unix:/var/run/php5-fpm.sock;\n    }\n\n    # Fighting with Styles? This little gem is amazing.\n    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6\n    location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7\n        try_files $uri @rewrite;\n    }\n\n    # Handle private files through Drupal.\n    location ~ ^/system/files/ { # For Drupal >= 7\n        try_files $uri /index.php?$query_string;\n    }\n\n    location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n        expires max;\n        log_not_found off;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/dynamic_ssi/nginx.conf",
    "content": "user nginx;\nworker_processes  1;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include       mime.types;\n    default_type  application/octet-stream;\n\n    sendfile      on;\n    tcp_nopush    on;\n    keepalive_timeout 10;\n    gzip          on;\n\n    server {\n        server_name  localhost;\n        charset      utf-8;\n        access_log   /var/log/nginx/access.log;\n\n        root    /var/www;\n\n        location = / {\n            rewrite ^ /home redirect;\n        }\n\n        location / {\n            ssi on;\n            set $inc $request_uri;\n            if (!-f $request_filename) {\n                rewrite ^ /index.html last;\n            }\n            if (!-f $document_root$inc.html) {\n                return 404;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/elgg/nginx.conf",
    "content": "server {\n  server_name domain.com;\n  rewrite ^/(.*) http://www.domain.com/$1 permanent;\n}\n\nserver {\n  server_name  www.domain.com;\n\n  client_max_body_size 8M;\n  client_body_buffer_size 256k;\n\n  location / {\n    if ($request_method = POST) {\n      proxy_pass http://localhost:8000;\n      break;\n    }\n\n    default_type  \"text/html; charset=utf-8\";\n    set $memcached_key \"/budokin-$uri\";\n    memcached_pass 127.0.0.1:11211;\n    error_page 404 502 = /fallback;\n  }\n\n  location = /fallback {\n    proxy_pass http://127.0.0.1:8000;\n    break;\n  }\n\n  access_log off;\n  #access_log /home/kam/www/budokin.com/log/access.log;\n  error_log /home/kam/www/budokin.com/log/error.log;\n}\n\nserver {\n  listen  8000;\n  server_name  www.domain.com;\n  root  /home/user/domain.com;\n  index  index.php;\n\n  client_max_body_size 8M;\n  client_body_buffer_size 256k;\n\n  location / {\n    if (!-e $request_filename) {\n      rewrite ^/action/([A-Za-z\\_\\-\\/] +) /engine/handlers/action_handler.php?action=$1 last;\n      rewrite ^/actions/([A-Za-z\\_\\-\\/] +) /engine/handlers/action_handler.php?action=$1 last;\n      rewrite ^/export/([A-Za-z] +)/([0-9] +) /services/export/handler.php?view=$1&guid=$2 last;\n      rewrite ^/export/([A-Za-z] +)/([0-9] +)/([A-Za-z] +)/([A-Za-z0-9\\_] +) /services/export/handler.php?view=$1&guid=$2&type=$3&idname=$4 last;\n      rewrite ^/_css/css.css /_css/css.php last;\n      rewrite ^/pg/([A-Za-z\\_\\-] +)/(.*) /engine/handlers/pagehandler.php?handler=$1&page=$2 last;\n      rewrite ^/pg/([A-Za-z\\_\\-] +) /engine/handlers/pagehandler.php?handler=$1 last;\n      rewrite ^/xml-rpc.php /engine/handlers/xml-rpc_handler.php last;\n      rewrite ^/mt/mt-xmlrpc.cgi /engine/handlers/xml-rpc_handler.php last;\n    }\n  }\n\n  location ~ \\.php$ {\n    fastcgi_connect_timeout 60;\n    fastcgi_send_timeout 180;\n    fastcgi_read_timeout 180;\n    fastcgi_buffer_size 128k;\n    fastcgi_buffers 4 256k;\n    fastcgi_busy_buffers_size 256k;\n    fastcgi_temp_file_write_size 256k;\n    fastcgi_intercept_errors on;\n\n    fastcgi_pass  127.0.0.1:9000;\n    fastcgi_index  index.php;\n    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n    fastcgi_param  QUERY_STRING     $query_string;\n    fastcgi_param  REQUEST_METHOD   $request_method;\n    fastcgi_param  CONTENT_TYPE     $content_type;\n    fastcgi_param  CONTENT_LENGTH   $content_length;\n\n    expires  max;\n  }\n\n  access_log off;\n  #access_log /home/kam/www/budokin.com/log/access.log;\n  error_log /home/kam/www/budokin.com/log/error.log;\n\n  #error_page  500 502 503 504  /50x.html;\n  #location =  /500.html { root  /home/kam/www/nginx-default; }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/embeddedperlminifyjs/nginx.conf",
    "content": "http {\n  perl_modules perl;\n\n  # Get this module from the CPAN and put the file in this directory.\n  # or install it systemwide\n  perl_require Javascript/Minifier.pm;\n  perl_require Minify.pm;\n\n  root /var/www;\n  server {\n    location / {\n      index  index.html index.htm;\n    }\n\n    location ~ \\.js$  {\n      perl Minify::handler;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/embeddedperlsitemapsproxy/nginx.conf",
    "content": "http {\n  include       mime.types;\n  default_type  application/octet-stream;\n\n  perl_modules lib;\n  perl_require Sitemap.pm;\n\n  keepalive_timeout  65;\n\n  server {\n    listen       8090;\n    server_name  sitemaps.worldsoft-cms.info;\n\n    location / {\n      root   html;\n      index  index.html index.htm;\n      if (!-f $request_filename) {\n        rewrite ^/(.*)-sitemap.xml$ /sitemap/$1 last;\n        # If a file matches somethingsomething-sitemap.xml\n        # then redirect it to /sitemap/somethingsomething\n        # here somethingsomething will match a domain\n      }\n    }\n\n    location /sitemap {\n      perl Sitemap::handler;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/bad.conf",
    "content": "server {\n  listen 80;\n  server_name www.mydomain.com;\n  root /var/www/EECore1.6.7;\n\n  access_log /var/log/nginx/www.mydomain.com-access.log;\n  error_log  /var/log/nginx/www.mydomain.com-error.log info;\n\n  location / {\n    index index.php;\n    error_page 404 = @ee;\n  }\n\n  location @ee {\n    rewrite ^(.*) /index.php?$1 last;\n  }\n\n  location ~ \\.php$ {\n    include /etc/nginx/fastcgi_params;\n    fastcgi_pass  127.0.0.1:8888;\n    fastcgi_index index.php5;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/better.conf",
    "content": "server {\n  listen 80;\n  server_name www.mydomain.com;\n  root /var/www/EECore1.6.7;\n\n  access_log /var/log/nginx/www.mydomain.com-access.log;\n  error_log  /var/log/nginx/www.mydomain.com-error.log info;\n\n  location / {\n    index index.php;\n    try_files $uri $uri/ @ee;\n  }\n\n  location @ee {\n    rewrite ^(.*) /index.php?$1 last;\n  }\n\n  location ~ \\.php$ {\n    include /etc/nginx/fastcgi_params;\n    fastcgi_pass  127.0.0.1:8888;\n    fastcgi_index index.php5;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/expressionengine/yourpath.conf",
    "content": "server {\n  listen 80;\n  server_name example.com;\n  root /PATH_TO_ROOT;\n  index index.php;\n\n  location / {\n    index index.php;\n    try_files $uri $uri/ @ee;\n  }\n\n  location @ee {\n    rewrite ^(.*) /index.php$1 last;\n  }\n\n  location ~ \\.php$ {\n    include fastcgi_params;\n    fastcgi_pass unix:/tmp/php-fastcgi.socket;\n    fastcgi_index index.php;\n    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n  }\n\n    # This location is for our EE index.php gateway\n    location /index.php {\n      include /usr/local/nginx/conf/fastcgi_params;\n      set $script     $uri;\n      set $path_info  $uri;\n      # this will set the path_info when it exists as query string: /index.php?/something/here\n      if ($args ~* \"^(/.+)$\") {\n        set $path_info  $1;\n      }\n      fastcgi_pass 127.0.0.1:9000;\n      fastcgi_index index.php;\n      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n      fastcgi_param PATH_INFO $path_info;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fastcgiexample/fastcgi.conf",
    "content": "#fastcgi.conf\nfastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fastcgiexample/nginx.conf",
    "content": "location ~ \\.php$ {\n    include /etc/nginx/fastcgi_params;\n    if ($uri !~ \"^/images/\") {\n        fastcgi_pass 127.0.0.1:9000;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fengoffice/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n                expires max;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n       client_max_body_size 8M;\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_index index.php;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/fastcgi.conf",
    "content": "fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;\nfastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\nfastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n\nfastcgi_index  index.php;\n\nfastcgi_param  REDIRECT_STATUS    200;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/mime.types",
    "content": "types {\n  text/html                             html htm shtml;\n  text/css                              css;\n  text/xml                              xml rss;\n  image/gif                             gif;\n  image/jpeg                            jpeg jpg;\n  application/x-javascript              js;\n  text/plain                            txt;\n  text/x-component                      htc;\n  text/mathml                           mml;\n  image/png                             png;\n  image/x-icon                          ico;\n  image/x-jng                           jng;\n  image/vnd.wap.wbmp                    wbmp;\n  application/java-archive              jar war ear;\n  application/mac-binhex40              hqx;\n  application/pdf                       pdf;\n  application/x-cocoa                   cco;\n  application/x-java-archive-diff       jardiff;\n  application/x-java-jnlp-file          jnlp;\n  application/x-makeself                run;\n  application/x-perl                    pl pm;\n  application/x-pilot                   prc pdb;\n  application/x-rar-compressed          rar;\n  application/x-redhat-package-manager  rpm;\n  application/x-sea                     sea;\n  application/x-shockwave-flash         swf;\n  application/x-stuffit                 sit;\n  application/x-tcl                     tcl tk;\n  application/x-x509-ca-cert            der pem crt;\n  application/x-xpinstall               xpi;\n  application/zip                       zip;\n  application/octet-stream              deb;\n  application/octet-stream              bin exe dll;\n  application/octet-stream              dmg;\n  application/octet-stream              eot;\n  application/octet-stream              iso img;\n  application/octet-stream              msi msp msm;\n  audio/mpeg                            mp3;\n  audio/x-realaudio                     ra;\n  video/mpeg                            mpeg mpg;\n  video/quicktime                       mov;\n  video/x-flv                           flv;\n  video/x-msvideo                       avi;\n  video/x-ms-wmv                        wmv;\n  video/x-ms-asf                        asx asf;\n  video/x-mng                           mng;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/nginx.conf",
    "content": "user       www www;  ## Default: nobody\nworker_processes  5;  ## Default: 1\nerror_log  logs/error.log;\npid        logs/nginx.pid;\nworker_rlimit_nofile 8192;\n\nevents {\n  worker_connections  4096;  ## Default: 1024\n}\n\nhttp {\n  include    conf/mime.types;\n  include    /etc/nginx/proxy.conf;\n  include    /etc/nginx/fastcgi.conf;\n  index    index.html index.htm index.php;\n\n  default_type application/octet-stream;\n  log_format   main '$remote_addr - $remote_user [$time_local]  $status '\n    '\"$request\" $body_bytes_sent \"$http_referer\" '\n    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n  access_log   logs/access.log  main;\n  sendfile     on;\n  tcp_nopush   on;\n  server_names_hash_bucket_size 128; # this seems to be required for some vhosts\n\n  server { # php/fastcgi\n    listen       80;\n    server_name  domain1.com www.domain1.com;\n    access_log   logs/domain1.access.log  main;\n    root         html;\n\n    location ~ \\.php$ {\n      fastcgi_pass   127.0.0.1:1025;\n    }\n  }\n\n  server { # simple reverse-proxy\n    listen       80;\n    server_name  domain2.com www.domain2.com;\n    access_log   logs/domain2.access.log  main;\n\n    # serve static files\n    location ~ ^/(images|javascript|js|css|flash|media|static)/  {\n      root    /var/www/virtual/big.server.com/htdocs;\n      expires 30d;\n    }\n\n    # pass requests for dynamic content to rails/turbogears/zope, et al\n    location / {\n      proxy_pass      http://127.0.0.1:8080;\n    }\n  }\n\n  upstream big_server_com {\n    server 127.0.0.3:8000 weight=5;\n    server 127.0.0.3:8001 weight=5;\n    server 192.168.0.1:8000;\n    server 192.168.0.1:8001;\n  }\n\n  server { # simple load balancing\n    listen          80;\n    server_name     big.server.com;\n    access_log      logs/big.server.access.log main;\n\n    location / {\n      proxy_pass      http://big_server_com;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/full-example/proxy.conf",
    "content": "proxy_redirect          off;\nproxy_set_header        Host            $host;\nproxy_set_header        X-Real-IP       $remote_addr;\nproxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;\nclient_max_body_size    10m;\nclient_body_buffer_size 128k;\nproxy_connect_timeout   90;\nproxy_send_timeout      90;\nproxy_read_timeout      90;\nproxy_buffers           32 4k;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/fullexample2/nginx.conf",
    "content": "user  www www;\nworker_processes  2;\npid /var/run/nginx.pid;\n\n# [ debug | info | notice | warn | error | crit ]\nerror_log  /var/log/nginx.error_log  info;\n\nevents {\n  worker_connections   2000;\n  # use [ kqueue | rtsig | epoll | /dev/poll | select | poll ] ;\n  use kqueue;\n}\n\nhttp {\n  include       conf/mime.types;\n  default_type  application/octet-stream;\n\n  log_format main      '$remote_addr - $remote_user [$time_local]  '\n    '\"$request\" $status $bytes_sent '\n    '\"$http_referer\" \"$http_user_agent\" '\n    '\"$gzip_ratio\"';\n\n  log_format download  '$remote_addr - $remote_user [$time_local]  '\n    '\"$request\" $status $bytes_sent '\n    '\"$http_referer\" \"$http_user_agent\" '\n    '\"$http_range\" \"$sent_http_content_range\"';\n\n  client_header_timeout  3m;\n  client_body_timeout    3m;\n  send_timeout           3m;\n\n  client_header_buffer_size    1k;\n  large_client_header_buffers  4 4k;\n\n  gzip on;\n  gzip_min_length  1100;\n  gzip_buffers     4 8k;\n  gzip_types       text/plain;\n\n  output_buffers   1 32k;\n  postpone_output  1460;\n\n  sendfile         on;\n  tcp_nopush       on;\n\n  tcp_nodelay      on;\n  send_lowat       12000;\n\n  keepalive_timeout  75 20;\n\n  # lingering_time     30;\n  # lingering_timeout  10;\n  # reset_timedout_connection  on;\n\n\n  server {\n    listen        one.example.com;\n    server_name   one.example.com  www.one.example.com;\n\n    access_log   /var/log/nginx.access_log  main;\n\n    location / {\n      proxy_pass         http://127.0.0.1/;\n      proxy_redirect     off;\n\n      proxy_set_header   Host             $host;\n      proxy_set_header   X-Real-IP        $remote_addr;\n      # proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;\n\n      client_max_body_size       10m;\n      client_body_buffer_size    128k;\n\n      client_body_temp_path      /var/nginx/client_body_temp;\n\n      proxy_connect_timeout      90;\n      proxy_send_timeout         90;\n      proxy_read_timeout         90;\n      proxy_send_lowat           12000;\n\n      proxy_buffer_size          4k;\n      proxy_buffers              4 32k;\n      proxy_busy_buffers_size    64k;\n      proxy_temp_file_write_size 64k;\n\n      proxy_temp_path            /var/nginx/proxy_temp;\n\n      charset  koi8-r;\n    }\n\n    error_page  404  /404.html;\n\n    location /404.html {\n      root  /spool/www;\n\n      charset         on;\n      source_charset  koi8-r;\n    }\n\n    location /old_stuff/ {\n      rewrite   ^/old_stuff/(.*)$  /new_stuff/$1  permanent;\n    }\n\n    location /download/ {\n      valid_referers  none  blocked  server_names  *.example.com;\n\n      if ($invalid_referer) {\n        #rewrite   ^/   http://www.example.com/;\n        return   403;\n      }\n\n      # rewrite_log  on;\n      # rewrite /download/*/mp3/*.any_ext to /download/*/mp3/*.mp3\n      rewrite ^/(download/.*)/mp3/(.*)\\..*$ /$1/mp3/$2.mp3 break;\n\n      root         /spool/www;\n      # autoindex    on;\n      access_log   /var/log/nginx-download.access_log  download;\n    }\n\n    location ~* ^.+\\.(jpg|jpeg|gif)$ {\n      root         /spool/www;\n      access_log   off;\n      expires      30d;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/geoip/nginx.conf",
    "content": "http {\n    geoip_country /usr/share/GeoIP/GeoIP.dat;\n    map $geoip_country_code $allowed_country {\n        default yes;\n        FK no;\n        FM no;\n        EH no;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/guide-to-nginx-ssl-spdy-hsts/nginx.conf",
    "content": "# openssl_version_minimum 1.0.1g;\n\nhttp {\n    ssl_session_cache   shared:SSL:10m;\n    ssl_session_timeout 10m;\n\n    server_tokens       off;\n    keepalive_timeout   60;\n\n    # http-redirects to https; even if using of hsts; \n    # useful if users are typing your server-name w/out https://\n\n\n    # logjam and a good idea anyway\n    ssl_dhparam /etc/nginx/dhparams.pem;\n\n\n    server {\n        listen          80 default_server;\n        server_name     secure.example.com;\n\n        rewrite ^ https://secure.example.com$request_uri? permanent;\n\n        # dummy redirect, if http+https-server-in-one\n        if ($scheme = http) {\n            return 301 https://$server_name$request_uri;\n        }\n\n    }\n\n\n    server {\n        # turn on ssl + spdy\n        listen          443 ssl spdy default_server;\n        server_name     secure.example.com;\n\n        # sending hsts-header / 12 months\n        add_header  Strict-Transport-Security \"max-age=31536000; includeSubDomains\";\n\n        # usually not needed\n        #add_header  Alternate-Protocol  443:npn-spdy/2;\n\n        # ssl-config\n\n        ssl_certificate     /etc/ssl/secure.example.com.crt-combined;\n        ssl_certificate_key /etc/ssl/secure.example.com.key;\n\n        ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;\n        ssl_prefer_server_ciphers on;\n\n        # please use a separate session_cache for each server {} - config\n        ssl_session_cache shared:SSL:10m;\n        ssl_session_timeout 10m;\n\n        # turn off gzip to protect against BREACH / http://breachattack.com/\n        gzip on;\n\n        #\n        # ssl cipher-suites\n        #\n\n        # nginx-default - kind of ok and working in usually any cases\n        ssl_ciphers         HIGH:!aNULL:!MD5\n\n        # nginx-default-for-static\n        #ssl_ciphers RC4:HIGH:!aNULL:!MD5;\n\n        # nginx mailinglist suggestion\n        #ssl_ciphers HIGH:!SSLv2:!MEDIUM:!LOW:!EXP:!RC4:!DSS:!aNULL:@STRENGTH;\n\n\n        # bare minimum for basic + BEAST-mitigation\n        # grade A w/ ssllabs, but no PFS\n        #ssl_ciphers         RC4:HIGH:!aNULL:!MD5;\n\n        # suggestion from sslabs\n        #ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA+RC4:EECDH:EDH+aRSA:RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;\n\n        # suggestion from hasgeek.com\n        #ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-RC4-SHA:ECDHE-RSA-RC4-SHA:ECDH-ECDSA-RC4-SHA:ECDH-RSA-RC4-SHA:ECDHE-RSA-AES256-SHA:RC4-SHA:HIGH:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!CBC:!EDH:!kEDH:!PSK:!SRP:!kECDH;\n\n        # bare minimum for BEAST-mitigation + PFS\n        #ssl_ciphers !aNULL:!LOW:!MD5:!EXP:RC4:AES256:3DES:AES128:SEED:CAMELLIA;\n\n\n\n        # PFS and most secure ciphers (think about using TLS1.2 only)\n        # suggestion from code-bear.com\n        #ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-RC4-SHA:ECDHE-RSA-RC4-SHA:ECDH-ECDSA-RC4-SHA:ECDH-RSA-RC4-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:RC4-SHA;\n\n        # PFS + BEAST-mitigation \n        # suggestion from hynek.me\n        #ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-RC4-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA:RC4-SHA;\n\n\n        # fast and secure, BEAST-mitigation but no PFS?\n        # suggestion from http://unhandledexpression.com\n        #ssl_ciphers ALL:!ADH:!EXP:!LOW:!RC2:!3DES:!SEED:!RC4:+HIGH:+MEDIUM;\n\n        #\n        # suggestions by mozilla-server-team - good compatibility, pfs, preferable ciphers \n        #\n        # modern ciphers \n        ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK';\n\n        # intermediate ciphers\n        #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';\n\n        # old ciphers (would need SSLv3, but is not recommended as of oct 2014\n        #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';\n\n        # logjam / cipher suggested from weakdh.org\n        #ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';\n\n\n\n    }\n\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/hardwarelberrors/nginx.conf",
    "content": "http {\n  geo  $lb  {\n    default      0;\n    10.1.1.1/32  1;   # LB IPs\n    10.1.1.2/32  1;\n  }\n\n  # ...\n\n  server {\n    # ...\n    access_log   /path/to/log;\n    error_page 400 /400;\n\n    location = /400 {\n      if ($lb) {\n        access_log  off;\n      }\n      return 400;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/icinga/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html index.htm;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n                expires max;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n       location / {\n                root   /usr/share/icinga/htdocs;\n                index  index.html;\n                auth_basic              \"Restricted\";\n                auth_basic_user_file    /etc/icinga/htpasswd.users;\n       }\n       location /icinga/stylesheets {\n                alias /etc/icinga/stylesheets;\n       }\n       location /stylesheets {\n                alias /etc/icinga/stylesheets;\n       }\n       location /icinga/images {\n                alias /usr/share/icinga/htdocs/images;\n       }\n       location ~ \\.cgi$ {\n                # define root directory for CGIs\n                root /usr/lib/cgi-bin/icinga;\n                rewrite ^/icinga/cgi-bin/(.*)\\.cgi /$1.cgi break;\n                rewrite ^/cgi-bin/icinga/(.*)\\.cgi /$1.cgi break;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass  unix:/var/run/fcgiwrap.socket;\n                fastcgi_index index.php;\n                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n                auth_basic              \"Restricted\";\n                auth_basic_user_file    /etc/icinga/htpasswd.users;\n                fastcgi_param  AUTH_USER          $remote_user;\n                fastcgi_param  REMOTE_USER        $remote_user;\n       }\n       location ~ ^/icinga-api/(.+\\.php)$ {\n                root   /usr/share/icinga/htdocs;\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_index index.php;\n                auth_basic              \"Restricted\";\n                auth_basic_user_file    /etc/icinga/htpasswd.users;\n                fastcgi_param  AUTH_USER          $remote_user;\n                fastcgi_param  REMOTE_USER        $remote_user;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapauthenticatewithapacheperlscript/nginx.conf",
    "content": "user  nobody;\nworker_processes  1;\nerror_log  logs/error.log  info;\npid        logs/nginx.pid;\n\nevents {\n  worker_connections  1024;\n  multi_accept on;\n}\n\nhttp {\n  perl_modules  perl/lib;\n  perl_require  mailauth.pm;\n\n  server {\n    location /auth {\n      perl  mailauth::handler;\n    }\n  }\n}\n\nmail {\n  auth_http  127.0.0.1:80/auth;\n\n  pop3_capabilities  \"TOP\"  \"USER\";\n  imap_capabilities  \"IMAP4rev1\"  \"UIDPLUS\";\n\n  server {\n    listen     110;\n    protocol   pop3;\n    proxy      on;\n  }\n\n  server {\n    listen     143;\n    protocol   imap;\n    proxy      on;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapauthenticatewithapachephpscript/nginx.conf",
    "content": "user  nobody;\nworker_processes  1;\nerror_log  logs/error.log  info;\npid  logs/nginx.pid;\n\nevents {\n  worker_connections  1024;\n  multi_accept on;\n}\n\nmail {\n  auth_http  192.168.1.44:80/mail/auth.php;\n  pop3_capabilities  \"TOP\"  \"USER\";\n  imap_capabilities  \"IMAP4rev1\"  \"UIDPLUS\";\n\n  server {\n    listen     110;\n    protocol   pop3;\n    proxy      on;\n  }\n\n  server {\n    listen     143;\n    protocol   imap;\n    proxy      on;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapproxyexample/nginx.conf",
    "content": "mail {\n  #auth_http  unix:/path/socket:/cgi-bin/auth;\n  auth_http  localhost:9000/cgi-bin/auth;\n\n  proxy     on;\n  starttls  on; ## enable STARTTLS for all mail servers\n\n  # The SSL part can be put in a separate configuration file,\n  # e.g., in the case of an SSL offloader / caching proxy.\n  # In that case, only the ssl_certificate* needs to be set here (or in server block.)\n  # The config assumes certificates in /etc/nginx/ssl/ and\n  # private keys in /etc/nginx/ssl/private/\n  ssl                        on;\n  ssl_prefer_server_ciphers  on;\n  ssl_protocols              TLSv1 SSLv3;\n  ssl_ciphers                HIGH:!ADH:!MD5:@STRENGTH;\n  ssl_session_cache          shared:TLSSL:16m;\n  ssl_session_timeout        10m;\n  ## default SSL cert. Each host should have its own.\n  ssl_certificate            ssl/wildcard.crt;\n  ssl_certificate_key        ssl/private/wildcard.key;\n\n  ## default, STARTTLS is appended because of starttls directive above\n  imap_capabilities  \"IMAP4rev1\"  \"UIDPLUS\";\n  server {\n    listen       143;\n    protocol     imap;\n    server_name  mx.example.org;\n  }\n\n## uncomment to enable POP3 proxy\n#  pop3_capabilities  \"TOP\"  \"USER\";\n#  server {\n#    listen     110;\n#    protocol   pop3;\n#  }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/imapproxyexample/proxy-example.conf",
    "content": "mail {\n  #auth_http  unix:/path/socket:/cgi-bin/auth;\n  auth_http  localhost:9000/cgi-bin/auth;\n\n  proxy  on;\n\n  imap_capabilities  \"IMAP4rev1\"  \"UIDPLUS\"; ## default\n  server {\n    listen     143;\n    protocol   imap;\n  }\n\n## uncomment to enable POP3 proxy\n#  pop3_capabilities  \"TOP\"  \"USER\";\n#  server {\n#    listen     110;\n#    protocol   pop3;\n#  }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iphone-website-with-nginx/mobile.conf",
    "content": "upstream m_app_server {\n  server 0.0.0.0:3001;\n}\n\nserver {\n  listen 80;\n  server_name m.mysite.com;\n\n  root /path/to/mobile_site;\n  # ...\n\n  location / {\n    proxy_set_header X-Real-IP $remote_addr;\n    # ...\n\n    if ($http_user_agent ~* '(iPhone|iPod)') {\n      set $iphone_request '1';\n      set $iphone_path_prefix '/iphone';\n    }\n    if ($uri ~ ^/iphone.*$) {\n      set $iphone_path_prefix '';\n    }\n    if ($uri ~ '(images|stylesheets|javascripts|\\.css|\\.js|\\.ico|\\.gif|\\.jpg|\\.png)') {\n      set $iphone_path_prefix '';\n    }\n    if ($iphone_request = '1') {\n      rewrite (.*) $iphone_path_prefix$1;\n    }\n\n    # serve cached pages ...\n\n    if (!-f $request_filename) {\n      proxy_pass http://m_app_server;\n      break;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iphone-website-with-nginx/nginx.conf",
    "content": "upstream app_server {\n  server 0.0.0.0:3000;\n}\n\nserver {\n  listen 80;\n  server_name www.mysite.com;\n\n  root /path/to/main_site;\n  # ...\n\n  location / {\n    proxy_set_header X-Real-IP $remote_addr;\n    # ...\n\n    if ($http_user_agent ~* '(iPhone|iPod)') {\n      set $iphone_request '1';\n    }\n    if ($http_cookie ~ 'iphone_mode=full') {\n      set $iphone_request '';\n    }\n    if ($iphone_request = '1') {\n      rewrite ^.+ http://m.mysite.com$uri;\n    }\n\n    # serve cached pages ...\n\n    if (!-f $request_filename) {\n      proxy_pass http://app_server;\n      break;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iredmail/iredadmin.conf",
    "content": "server {\n    listen 443 ssl; ## listen for ipv4; this line is default and implied\n    access_log /var/log/nginx/iredadmin.access.log;\n    error_log /var/log/nginx/iredadmin.error.log;\n\n    ssl_certificate /etc/nginx/ssl/star.crt;\n    ssl_certificate_key /etc/nginx/ssl/server.key;\n    ssl_session_timeout 5m;\n    ssl_protocols SSLv3 TLSv1;\n    ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;\n    ssl_prefer_server_ciphers on;\n\n    server_name mail.elegbara.com;\n\n    location / {\n        root /var/www/iredadmin/;\n        uwsgi_pass unix:///var/run/uwsgi/app/iredadmin/iredadmin.socket;\n        uwsgi_param UWSGI_PYHOME /var/www/iredadmin/python-home;\n        uwsgi_param UWSGI_CHDIR /var/www/iredadmin;\n        uwsgi_param UWSGI_SCRIPT iredadmin;\n        include uwsgi_params;\n    }\n\n    location /static {\n        alias /var/www/iredadmin/static/;\n    }\n\n    location ~ /\\.ht {\n        deny all;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/iredmail/nginx.conf",
    "content": "server {\n    listen          80;\n    server_name mail.elegbara.net;\n\n    location / {\n        rewrite ^ https://mail.elegbara.net/webmail permanent;\n    }\n\n    location ~ \\.php$ {\n        fastcgi_pass   127.0.0.1:9000;\n        fastcgi_index  index.php;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME /usr/share/apache2$fastcgi_script_name;\n    }\n}\n\nserver {\n    listen       443;\n    server_name  mail.elegbara.net;\n\n    location / {\n        root   /usr/share/apache2/;\n        index  index.php index.html;\n    }\n\n     location ~ \\.php$ {\n        root            /usr/share/apache2/;\n        include         fastcgi_params;\n        fastcgi_pass    127.0.0.1:9000;\n        fastcgi_index   index.php;\n        fastcgi_param   SCRIPT_FILENAME /usr/share/apache2$fastcgi_script_name;\n        fastcgi_param SERVER_NAME $http_host;\n        fastcgi_ignore_client_abort on;\n    }\n\n    ssl                  on;\n    ssl_certificate      /etc/ssl/certs/iRedMail_CA.pem;\n    ssl_certificate_key  /etc/ssl/private/iRedMail.key;\n    ssl_session_timeout  5m;\n    ssl_protocols  SSLv2 SSLv3 TLSv1;\n    ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;\n    ssl_prefer_server_ciphers   on;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/javaservers/nginx.conf",
    "content": "root  /PATH/TO/YOUR/WEB/APPLICATION;\n\nproxy_pass  http://localhost:8080;\n\nlocation ~ \\.do$ {\n  proxy_pass        http://localhost:8080;\n  proxy_set_header  X-Real-IP $remote_addr;\n  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n  proxy_set_header  Host $http_host;\n}\nlocation ~ \\.jsp$ {\n  proxy_pass        http://localhost:8080;\n  proxy_set_header  X-Real-IP $remote_addr;\n  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n  proxy_set_header  Host $http_host;\n}\nlocation ^~/servlets/* {\n  proxy_pass        http://localhost:8080;\n  proxy_set_header  X-Real-IP $remote_addr;\n  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n  proxy_set_header  Host $http_host;\n}\n\nserver {\n  listen          80;\n  server_name     YOUR_DOMAIN;\n  root            /PATH/TO/YOUR/WEB/APPLICATION;\n  location / {\n    index index.jsp;\n  }\n  location ~ \\.do$ {\n    proxy_pass        http://localhost:8080;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header  Host $http_host;\n  }\n  location ~ \\.jsp$ {\n    proxy_pass        http://localhost:8080;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header  Host $http_host;\n  }\n  location ^~/servlets/* {\n    proxy_pass        http://localhost:8080;\n    proxy_set_header  X-Real-IP $remote_addr;\n    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header  Host $http_host;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/joomla/nginx.conf",
    "content": "server {\n        listen 80;\n        server_name YOUR_DOMAIN;\n        server_name_in_redirect off;\n\n        access_log /var/log/nginx/localhost.access_log;\n        error_log /var/log/nginx/localhost.error_log info;\n\n        root PATH_ON_SERVER;\n        index index.php index.html index.htm default.html default.htm;\n        # Support Clean (aka Search Engine Friendly) URLs\n        location / {\n                try_files $uri $uri/ /index.php?$args;\n        }\n\n        # deny running scripts inside writable directories\n        location ~* /(images|cache|media|logs|tmp)/.*\\.(php|pl|py|jsp|asp|sh|cgi)$ {\n                return 403;\n                error_page 403 /403_error.html;\n        }\n\n        location ~ \\.php$ {\n            fastcgi_pass  127.0.0.1:9000;\n            fastcgi_index index.php;\n            include fastcgi_params;\n            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n            include /etc/nginx/fastcgi.conf;\n        }\n\n        # caching of files \n        location ~* \\.(ico|pdf|flv)$ {\n                expires 1y;\n        }\n\n        location ~* \\.(js|css|png|jpg|jpeg|gif|swf|xml|txt)$ {\n                expires 14d;\n        }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/likeapache/nginx.conf",
    "content": "server {\n    listen myhost:80;\n    server_name  myhost;\n    location / {\n        root /path/to/myapp/public;\n        proxy_set_header X-Forwarded-Host $host:$server_port;\n        proxy_set_header X-Forwarded-Server $host;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_pass http://myapp:8080;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/loadbalanceexample/nginx.conf",
    "content": "http {\n  upstream myproject {\n    server 127.0.0.1:8000 weight=3;\n    server 127.0.0.1:8001;\n    server 127.0.0.1:8002;\n    server 127.0.0.1:8003;\n  }\n\n  server {\n    listen 80;\n    server_name www.domain.com;\n    location / {\n      proxy_pass http://myproject;\n    }\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mailman/nginx.conf",
    "content": "server {\n    listen 1.2.3.4:80;\n    server_name lists.DOMAIN.TLD;\n    root /usr/lib;\n\n    location = / {\n        rewrite ^ /mailman/listinfo permanent;\n    }\n\n    location / {\n        rewrite ^ /mailman$uri?$args;\n    }\n\n    location = /mailman/ {\n        rewrite ^ /mailman/listinfo permanent;\n    }\n\n    location /mailman/ {\n        include proxy_params;\n        proxy_pass http://127.0.0.1/;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n    }\n\n    location /cgi-bin {\n        rewrite ^/cgi-bin(.*)$ $1 permanent;\n    }\n\n    location /images/mailman {\n        alias /var/lib/mailman/icons;\n    }\n\n    location /pipermail {\n        alias /var/lib/mailman/archives/public;\n        autoindex on;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mediawiki/nginx.conf",
    "content": "server {\n    server_name wiki.nginx.org;\n    root /var/www/mediawiki;\n\n    client_max_body_size 5m;\n    client_body_timeout 60;\n\n    location / {\n        try_files $uri $uri/ @rewrite;\n    }\n\n    location @rewrite {\n        rewrite ^/(.*)$ /index.php?title=$1&$args;\n    }\n\n    location ^~ /maintenance/ {\n        return 403;\n    }\n\n    location ~ \\.php$ {\n        include fastcgi_params;\n        fastcgi_pass unix:/tmp/phpfpm.sock;\n    }\n\n    location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n        try_files $uri /index.php;\n        expires max;\n        log_not_found off;\n    }\n\n    location = /_.gif {\n        expires max;\n        empty_gif;\n    }\n\n    location ^~ /cache/ {\n        deny all;\n    }\n\n    location /dumps {\n        root /var/www/mediawiki/local;\n        autoindex on;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/memcachepreload/sites-available/default",
    "content": "server {\n        listen          80;\n        server_name     <webserver>;\n        root            /var/www/;\n\n        location / {\n                index                   index.html;\n                default_type            text/plain;\n                set $memcached_key      memfis://<hostname>$uri;\n                memcached_pass          127.0.0.1:11211;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/minio/sites-enabled/nginx.conf",
    "content": "server {\n listen 80;\n server_name example.com;\n location / {\n   proxy_set_header Host $host;\n   proxy_set_header X-Real-IP $remote_addr;\n   proxy_set_header X-Forwarded-Proto $scheme;\n   proxy_pass http://localhost:9000;\n }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mono/nginx.conf",
    "content": "server {\n        server_name profarius.com;\n        root /var/www/webapp;\n        index index.html index.htm index.aspx default.aspx;\n\n        location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n        }\n\n        location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n        }\n\n        location / {\n                try_files $uri $uri/ /index.aspx;\n        }\n\n        # Fighting with ImageCache? This little gem is amazing.\n        location ~ ^/sites/.*/files/imagecache/ {\n                try_files $uri $uri/ @rewrite;\n        }\n\n        location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n                expires max;\n                log_not_found off;\n        }\n\n        location ~ \\.(aspx|asmx|ashx|asax|ascx|soap|rem|axd|cs|config|dll)$ {\n            fastcgi_pass   127.0.0.1:9000;\n            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n            include        fastcgi_params;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/mybb/nginx.conf",
    "content": "server {\n    server_name quantifiedselfforum.com;\n\n    access_log logs/qsforum.access;\n    error_log logs/qsforum.error error;\n\n    root /var/www/qsforum;\n\n    location / {\n        index index.php;\n    }\n\n    # Deny access to internal files.\n    location ~ /(inc|uploads/avatars) {\n        deny all;\n    }\n\n    # Pass the php scripts to fastcgi server\n    location ~ \\.php$ {\n        fastcgi_pass unix:/tmp/php.socket;\n        # Necessary for php.\n        fastcgi_param SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n        # Unmodified fastcgi_params from nginx distribution.\n        include fastcgi_params;\n    }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/nonrootwebpath/nginx.conf",
    "content": "location ~ /thisapp(?<path_info>/.*|$) {\n  fastcgi_pass    unix:/path/to/thisappfcgi.sock;\n  include /etc/nginx/fastcgi_params;\n\n  fastcgi_param   PATH_INFO   $path_info;\n  fastcgi_param   SCRIPT_NAME \"/thisapp\";\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/omeka/nginx.conf",
    "content": "server {\n        server_name omeka.corp.good-sam.com;\n        root /var/www/omeka;\n\n        location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n        }\n\n        location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n        }\n\n        location ~ \\..*/.*\\.php$ {\n                return 403;\n        }\n\n        location / {\n                try_files $uri /index.php?$args;\n        }\n\n        location /admin {\n                try_files $uri /admin/index.php?$args;\n        }\n\n        location ~ \\.php$ {\n                fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n                #NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n                include fastcgi_params;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n                fastcgi_pass unix:/tmp/phpfpm.sock;\n        }\n\n        location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n                expires max;\n                log_not_found off;\n        }\n\n        location /install {\n                try_files $uri /install/index.php?$args;\n\n                # This is an odd way to check that rewrites work...\n                location ~* /install/check-mod-rewrite([^/]*)\\.html$ {\n                        rewrite ^ /install/mod-rewrite.php?enabled=true;\n                }\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/oscommerce/nginx.conf",
    "content": "server {\n    server_name www.domain.com;\n    server_name_in_redirect off;\n\n    root /var/www/www.domain.com/catalog;\n    access_log /var/www/www.domain.com/logs/access.log;\n    error_log /var/www/www.domain.com/logs/error.log;\n\n    # expires max on static content\n    location / { expires max; }\n\n    # Inaccessible locations\n    location ~ ^/includes/.*\\.php$ { return 403; }\n    location ~ ^/admin/includes/.*\\.php$ { return 403; }\n    location ^~ /admin/backups { return 403; }\n    location ^~ /download { return 403; }\n\n    # osCommerce rewrites\n    location ~ -p-(?<id>[0-9]+)\\.html$ { rewrite ^ /product_info.php?products_id=$id; }\n    location ~ -c-(?<id>[0-9_]+)\\.html$ { rewrite ^ /index.php?cPath=$id; }\n    location ~ -m-(?<id>[0-9]+)\\.html$ { rewrite ^ /index.php?manufacturers_id=$id; }\n    location ~ -pi-(?<id>[0-9]+)\\.html$ { rewrite ^ /popup_image.php?pID=$id; }\n    location ~ -pr-(?<id>[0-9]+)\\.html$ { rewrite ^ /product_reviews.php?products_id=$id; }\n    location ~ -pri-(?<id>[0-9]+)\\.html$ { rewrite ^ /product_reviews_info.php?products_id=$id; }\n\n    # Articles contribution\n    location ~ -t-(?<id>[0-9_]+)\\.html$ { rewrite ^ /articles.php?tPath=$id; }\n    location ~ -a-(?<id>[0-9]+)\\.html$ { rewrite ^ /article_info.php?articles_id=$id; }\n\n    # Information pages\n    location ~ -i-(?<id>[0-9]+)\\.html$ { rewrite ^ /information.php?info_id=$id; }\n\n    # Links contribution\n    location ~ -links-(?<id>[0-9_]+)\\.html$ { rewrite ^ /links.php?lPath=$id; }\n\n    # Newsdesk contribution\n    location ~ -n-(?<id>[0-9]+)\\.html$ { rewrite ^ /newsdesk_info.php?newsdesk_id=$id; }\n    location ~ -nc-(?<id>[0-9]+)\\.html$ { rewrite ^ /newsdesk_index.php?newsPath=$id; }\n    location ~ -nri-(?<id>[0-9]+)\\.html$ { rewrite ^ /newsdesk_reviews_info.php?newsdesk_id=$id; }\n    location ~ -nra-(?<id>[0-9]+)\\.html$ { rewrite ^ /newsdesk_reviews_article.php?newsdesk_id=$id; }\n\n    # Pass to php\n    location ~ \\.php$ {\n        if (!-f $request_filename) { return 404; }\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_param REDIRECT_STATUS 200;\n        fastcgi_pass 127.0.0.1:9000;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/osticket/nginx.conf",
    "content": "user  nginx;\nworker_processes 1;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include         mime.types;\n    default_type    application/octet-stream;\n    sendfile        on;\n    charset         utf-8;\n    gzip            on;\n    gzip_types      text/plain application/xml text/javascript;\n    gzip_min_length 1000;\n\n    index index.php index.html index.htm;\n\n    # Rewrite all requests from HTTP to HTTPS\n    server {\n        listen 80;\n        server_name tickets.mydomain.com;\n        rewrite ^ https://tickets.mydomain.com permanent;\n    }\n\n    server {\n        listen 443;\n        server_name tickets.mydomain.com;\n        ssl on;\n        ssl_certificate /etc/nginx/certs/cert.pem;\n        ssl_certificate_key /etc/nginx/certs/cert.key;\n\n        keepalive_timeout 70;\n\n        root /var/www/osticket;\n\n        set $path_info \"\";\n\n        location ~ /include {\n            deny all;\n            return 403;\n        }\n\n        if ($request_uri ~ \"^/api(/[^\\?]+)\") {\n        set $path_info $1;\n    }\n\n    location ~ ^/api/(?:tickets|tasks).*$ {\n        try_files $uri $uri/ /api/http.php?$query_string;\n    }\n\n    if ($request_uri ~ \"^/scp/.*\\.php(/[^\\?]+)\") {\n        set $path_info $1;\n    }\n\n    location ~ ^/scp/ajax.php/.*$ {\n        try_files $uri $uri/ /scp/ajax.php?$query_string;\n    }\n\n        location / {\n            try_files $uri $uri/ index.php;\n        }\n\n        location ~ \\.php$ {\n            fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n            include        fastcgi_params;\n            fastcgi_param  PATH_INFO        $path_info;\n            fastcgi_pass   127.0.0.1:8888;\n        }\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/owncloud/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n        client_max_body_size 10G; # set max upload size\n\n        rewrite ^/caldav(.*)$ /remote.php/caldav$1 redirect;\n        rewrite ^/carddav(.*)$ /remote.php/carddav$1 redirect;\n        rewrite ^/webdav(.*)$ /remote.php/webdav$1 redirect;\n        rewrite ^/apps/calendar/caldav.php /remote.php/caldav/ last;\n        rewrite ^/apps/contacts/carddav.php /remote.php/carddav/ last;\n        rewrite ^/apps/([^/]*)/(.*\\.(css|php))$ /index.php?app=$1&getfile=$2 last;\n        rewrite ^/remote/(.*) /remote.php last;\n\n        error_page 403 = /core/templates/403.php;\n        error_page 404 = /core/templates/404.php;\n\n        location ~ ^/(data|config|\\.ht|db_structure\\.xml|README) {\n                        deny all;\n        }\n\n        location / {\n                        rewrite ^/.well-known/host-meta /public.php?service=host-meta last;\n                        rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;\n\n                        rewrite ^/.well-known/carddav /remote.php/carddav/ redirect;\n                        rewrite ^/.well-known/caldav /remote.php/caldav/ redirect;\n\n                        rewrite ^(/core/doc/[^\\/]+/)$ $1/index.html;\n\n                        try_files $uri $uri/ /index.php$is_args$args;\n        }\n\n        location ~ ^(.+?\\.php)(/.*)?$ {\n                        try_files $1 =404;\n                        include fastcgi_params;\n                        fastcgi_param SCRIPT_FILENAME $document_root$1;\n                        fastcgi_param PATH_INFO $2;\n                        fastcgi_param HTTPS $https;\n                        fastcgi_pass unix:/var/run/php5-fpm.sock;\n                        fastcgi_intercept_errors on;\n                        fastcgi_index index.php;\n                        fastcgi_buffers 64 4K;\n        }\n\n        location ~* ^.+\\.(jpg|jpeg|gif|bmp|ico|png|css|js|swf)$ {\n                        expires 30d;\n                        access_log off;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/oxid-eshop/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n\n       ## SSL directives might go here\n       ## see http://www.howtoforge.com/how_to_set_up_ssl_vhosts_under_nginx_plus_sni_support_ubuntu_11.04_debian_squeeze\n       ## if you want to enable SSL for this vhost\n\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n\n       if ($request_method ~ ^(TRACE|TRACK)$ ) {\n                return 403;\n       }\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       location ~ (/\\.|EXCEPTION_LOG\\.txt|\\.log$|\\.tpl$|pkg.rev) {\n                deny all;\n       }\n\n       location ~ /out/pictures/.*(\\.jpg|\\.gif|\\.png)$ {\n                try_files $uri /core/utils/getimg.php;\n       }\n\n       location ~ ^/(admin|setup)/?$ {\n       }\n\n       location ~ /(core|export|modules|out|tmp|views)/ {\n       }\n\n       location / {\n                try_files $uri $uri/ /oxseo.php;\n       }\n\n       location = /oxseo.php {\n                if ($args ~ \"mod_rewrite_module_is=off\") {\n                       rewrite /oxseo.php /oxseo.php?mod_rewrite_module_is=on? break;\n                }\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_param  HTTPS $fastcgi_https;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_param  HTTPS $fastcgi_https;\n       }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/php-fpm/default.conf",
    "content": "# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n#\nlocation ~ \\.php$ {\n\troot           /usr/share/nginx/html;\n\tfastcgi_pass   127.0.0.1:9000;\n\tfastcgi_index  index.php;\n\tfastcgi_param  SCRIPT_FILENAME   $document_root$fastcgi_script_name;\n\tinclude        fastcgi_params;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpbb/nginx.sample.conf",
    "content": "# Sample nginx configuration file for phpBB.\n# Global settings have been removed, copy them\n# from your system's nginx.conf.\n# Tested with nginx 0.8.35.\n\n# If you want to use the X-Accel-Redirect feature,\n# add the following to your config.php.\n#\n#  define('PHPBB_ENABLE_X_ACCEL_REDIRECT', true);\n#\n# See http://wiki.nginx.org/XSendfile for the details\n# on X-Accel-Redirect.\n\nhttp {\n    # Compression - requires gzip and gzip static modules.\n    gzip on;\n    gzip_static on;\n    gzip_vary on;\n    gzip_http_version 1.1;\n    gzip_min_length 700;\n    \n    # Compression levels over 6 do not give an appreciable improvement\n    # in compression ratio, but take more resources.\n    gzip_comp_level 6;\n    \n    # IE 6 and lower do not support gzip with Vary correctly.\n    gzip_disable \"msie6\";\n    # Before nginx 0.7.63:\n    #gzip_disable \"MSIE [1-6]\\.\";\n\n    # Catch-all server for requests to invalid hosts.\n    # Also catches vulnerability scanners probing IP addresses.\n    server {\n        # default specifies that this block is to be used when\n        # no other block matches.\n        listen 80 default;\n\n        server_name bogus;\n        return 444;\n        root /var/empty;\n    }\n\n    # If you have domains with and without www prefix,\n    # redirect one to the other.\n    server {\n        # Default port is 80.\n        #listen 80;\n\n        server_name myforums.com;\n\n        # A trick from http://wiki.nginx.org/Pitfalls#Taxing_Rewrites:\n        rewrite ^ http://www.myforums.com$request_uri permanent;\n        # Equivalent to:\n        #rewrite ^(.*)$ http://www.myforums.com$1 permanent;\n    }\n\n    # The actual board domain.\n    server {\n        #listen 80;\n        server_name www.myforums.com;\n\n        root /path/to/phpbb;\n\n        location / {\n            # phpBB uses index.htm\n            index index.php index.html index.htm;\n            try_files $uri $uri/ @rewriteapp;\n        }\n\n        location @rewriteapp {\n            rewrite ^(.*)$ /app.php/$1 last;\n        }\n\n        # Deny access to internal phpbb files.\n        location ~ /(config\\.php|common\\.php|includes|cache|files|store|images/avatars/upload) {\n            deny all;\n            # deny was ignored before 0.8.40 for connections over IPv6.\n            # Use internal directive to prohibit access on older versions.\n            internal;\n        }\n\n        # Pass the php scripts to fastcgi server specified in upstream declaration.\n        location ~ \\.php(/|$) {\n            # Unmodified fastcgi_params from nginx distribution.\n            include fastcgi_params;\n            # Necessary for php.\n            fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n            fastcgi_param PATH_INFO $fastcgi_path_info;\n            fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n            fastcgi_param DOCUMENT_ROOT $realpath_root;\n            try_files $uri $uri/ /app.php$is_args$args;\n            fastcgi_pass php;\n        }\n\n        # Correctly pass scripts for installer\n        location /install/ {\n            # phpBB uses index.htm\n            try_files $uri $uri/ @rewrite_installapp;\n\n            # Pass the php scripts to fastcgi server specified in upstream declaration.\n            location ~ \\.php(/|$) {\n                # Unmodified fastcgi_params from nginx distribution.\n                include fastcgi_params;\n                # Necessary for php.\n                fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n                fastcgi_param PATH_INFO $fastcgi_path_info;\n                fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n                fastcgi_param DOCUMENT_ROOT $realpath_root;\n                try_files $uri $uri/ /install/app.php$is_args$args;\n                fastcgi_pass php;\n            }\n        }\n\n        location @rewrite_installapp {\n            rewrite ^(.*)$ /install/app.php/$1 last;\n        }\n\n        # Deny access to version control system directories.\n        location ~ /\\.svn|/\\.git {\n            deny all;\n            internal;\n        }\n    }\n\n    # If running php as fastcgi, specify php upstream.\n    upstream php {\n        server unix:/tmp/php.sock;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfastcgionwindows/nginx.conf",
    "content": "root c:/www;\n\nlocation ~ \\.php$ {\n    fastcgi_pass   127.0.0.1:9123;\n    fastcgi_index  index.php;\n    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n    include        fastcgi_params;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfcgi/fastcgi_params",
    "content": "fastcgi_param   QUERY_STRING            $query_string;\nfastcgi_param   REQUEST_METHOD          $request_method;\nfastcgi_param   CONTENT_TYPE            $content_type;\nfastcgi_param   CONTENT_LENGTH          $content_length;\n\nfastcgi_param   SCRIPT_FILENAME         $document_root$fastcgi_script_name;\nfastcgi_param   SCRIPT_NAME             $fastcgi_script_name;\nfastcgi_param   PATH_INFO               $fastcgi_path_info;\nfastcgi_param       PATH_TRANSLATED         $document_root$fastcgi_path_info;\nfastcgi_param   REQUEST_URI             $request_uri;\nfastcgi_param   DOCUMENT_URI            $document_uri;\nfastcgi_param   DOCUMENT_ROOT           $document_root;\nfastcgi_param   SERVER_PROTOCOL         $server_protocol;\n\nfastcgi_param   GATEWAY_INTERFACE       CGI/1.1;\nfastcgi_param   SERVER_SOFTWARE         nginx/$nginx_version;\n\nfastcgi_param   REMOTE_ADDR             $remote_addr;\nfastcgi_param   REMOTE_PORT             $remote_port;\nfastcgi_param   SERVER_ADDR             $server_addr;\nfastcgi_param   SERVER_PORT             $server_port;\nfastcgi_param   SERVER_NAME             $server_name;\n\nfastcgi_param   HTTPS                   $https;\n\n# PHP only, required if PHP was built with --enable-force-cgi-redirect\nfastcgi_param   REDIRECT_STATUS         200;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phpfcgi/nginx.conf",
    "content": "location ~ [^/]\\.php(/|$) {\n    fastcgi_split_path_info ^(.+?\\.php)(/.*)$;\n    if (!-f $document_root$fastcgi_script_name) {\n        return 404;\n    }\n\n    fastcgi_pass 127.0.0.1:9000;\n    fastcgi_index index.php;\n    include fastcgi_params;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/phplist/nginx.conf",
    "content": "server {\n            listen 80;\n            server_name example.com;\n\n            root /var/www/phplist/public_html/lists;\n            index index.php;\n\n            access_log <<log file>>;\n            error_log <<log file>>;\n\n            charset utf-8;\n\n            location ~* \\.(txt|log|inc)$ {\n                allow 127.0.0.1;\n                deny all;\n            }\n\n            location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n              expires max;\n              log_not_found off;\n           }\n\n            #block phplist config directory\n            location /config {\n                deny all;\n            }\n\n            #per the phplist .htaccess these are the only public allowed php files\n            location ~* (index\\.php|upload\\.php|connector\\.php|dl\\.php|ut\\.php|lt\\.php|download\\.php)$ {\n                   fastcgi_split_path_info ^(.|\\.php)(/.+)$;\n\n                   include /etc/nginx/fastcgi_params.conf; #standard fastcgi config file\n                   fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                   fastcgi_intercept_errors on;\n                   fastcgi_pass 127.0.0.1:9000;\n            }\n\n\n            #block all other php file access from public\n            location ~ \\.php$ {\n               deny all;\n            }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/piwik/nginx.conf",
    "content": "server {\n   ## This is to avoid the spurious if for sub-domain name rewriting.\n   listen [::]:80;\n   server_name www.stats.example.com;\n   rewrite ^ $scheme://stats.example.com$request_uri? permanent;\n}\n\nserver {\n    listen [::]:80;\n    limit_conn arbeit 10;\n    server_name stats.example.com;\n\n    # Parameterization using hostname of access and log filenames.\n    access_log  /var/log/nginx/stats.example.com_access.log;\n    error_log   /var/log/nginx/stats.example.com_error.log;\n\n    # Disable all methods besides HEAD, GET and POST.\n    if ($request_method !~ ^(GET|HEAD|POST)$ ) {\n        return 444;\n    }\n\n    root  /var/www/sites/stats.example.com/;\n    index  index.php index.html;\n\n    # Disallow any usage of piwik assets if referer is non valid.\n    location ~* ^.+\\.(?:jpg|png|css|gif|jpeg|js|swf)$ {\n             # Defining the valid referrers.\n             valid_referers none blocked *.mysite.com othersite.com;\n             if ($invalid_referer)  {\n                return 444;\n             }\n             expires max;\n             break;\n    }\n\n    # Support for favicon. Return a 204 (No Content) if the favicon\n    # doesn't exist.\n    location = /favicon.ico {\n             try_files /favicon.ico =204;\n    }\n\n    # Try all locations and relay to index.php as a fallback.\n    location / {\n             try_files $uri /index.php;\n    }\n\n    # Relay all index.php requests to fastcgi.\n    location ~* ^/(?:index|piwik)\\.php$ {\n            fastcgi_pass unix:/tmp/php-cgi/php-cgi.socket;\n    }\n\n    # Any other attempt to access PHP files returns a 404.\n    location ~* ^.+\\.php$ {\n              return 404;\n    }\n\n    # Return a 404 for all text files.\n    location ~* ^/(?:README|LICENSE[^.]*|LEGALNOTICE)(?:\\.txt)*$ {\n             return 404;\n    }\n\n    # # The 404 is signaled through a static page.\n    # error_page  404  /404.html;\n\n    # ## All server error pages go to 50x.html at the document root.\n    # error_page 500 502 503 504  /50x.html;\n    # location = /50x.html {\n    #       root   /var/www/nginx-default;\n    # }\n} # server\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pmwiki/nginx.conf",
    "content": "server {\n    server_name wiki.example.com;\n    root /srv/www/pmwiki/example.com/wiki/public;\n\n    index pmwiki.php;\n\n    location ~ ^/(cookbook|local|scripts|wiki.d|wikilib.d) {\n        deny all;\n    }\n\n    location / {\n        try_files $uri $uri/ @pmwiki;\n    }\n\n    location @pmwiki {\n        rewrite ^/(.*) /pmwiki.php?n=$1;\n    }\n\n    ## php configuration using unix sockets.\n    location ~ \\.php$ {\n        include /etc/nginx/fastcgi_params;\n        fastcgi_pass unix:/run/php-fpm/php-fpm.sock;\n    }\n\n\n    # cache configuration for most common files\n    location ~* \\.(?:ico|css|js|gif|jpe?g|png)$ {\n        # Some basic cache-control for static files to be sent to the browser\n        expires max;\n        add_header Pragma public;\n        add_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n    }\n\n    # drop common log errors\n    location = /robots.txt { access_log off; log_not_found off; }\n    location = /favicon.ico { access_log off; log_not_found off; }\n    location ~ /\\. { access_log off; log_not_found off; deny all; }\n    location ~ ~$ { access_log off; log_not_found off; deny all; }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/prestashop/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       rewrite ^/api/?(.*)$ /webservice/dispatcher.php?url=$1 last;\n       rewrite ^/([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$1$2$3.jpg last;\n       rewrite ^/([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$1$2$3$4.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$1$2$3$4$5.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$4/$1$2$3$4$5$6.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$4/$5/$1$2$3$4$5$6$7.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$4/$5/$6/$1$2$3$4$5$6$7$8.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$4/$5/$6/$7/$1$2$3$4$5$6$7$8$9.jpg last;\n       rewrite ^/([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])([0-9])(\\-[_a-zA-Z0-9-]*)?(-[0-9]+)?/.+\\.jpg$ /img/p/$1/$2/$3/$4/$5/$6/$7/$8/$1$2$3$4$5$6$7$8$9$10.jpg last;\n       rewrite ^/c/([0-9]+)(\\-[\\.*_a-zA-Z0-9-]*)(-[0-9]+)?/.+\\.jpg$ /img/c/$1$2$3.jpg last;\n       rewrite ^/c/([a-zA-Z_-]+)(-[0-9]+)?/.+\\.jpg$ /img/c/$1$2.jpg last;\n       rewrite ^/images_ie/?([^/]+)\\.(jpe?g|png|gif)$ /js/jquery/plugins/fancybox/images/$1.$2 last;\n       try_files $uri $uri/ /index.php$is_args$args;\n       error_page 404 /index.php?controller=404;\n\t   \n       location ~* \\.(gif)$ {\n          expires 2592000s;\n       }\n       location ~* \\.(jpeg|jpg)$ {\n          expires 2592000s;\n       }\n       location ~* \\.(png)$ {\n          expires 2592000s;\n       }\n       location ~* \\.(css)$ {\n          expires 604800s;\n       }\n       location ~* \\.(js|jsonp)$ {\n          expires 604800s;\n       }\n       location ~* \\.(js)$ {\n          expires 604800s;\n       }\n       location ~* \\.(ico)$ {\n          expires 31536000s;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass unix:/var/run/php5-fpm.sock;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/processwire/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       client_max_body_size 100M;\n\n       location ~ /(COPYRIGHT|LICENSE|README|htaccess)\\.txt {\n                deny  all;\n       }\n       location ~ ^/site(-[^/]+)?/assets/(.*\\.php|backups|cache|config|install|logs|sessions) {\n                deny  all;\n       }\n       location ~ ^/site(-[^/]+)?/install {\n                deny  all;\n       }\n       location ~ ^/(site(-[^/]+)?|wire)/(config(-dev)?|index\\.config)\\.php {\n                deny  all;\n       }\n       location ~ ^/((site(-[^/]+)?|wire)/modules|wire/core)/.*\\.(inc|module|php|tpl) {\n                deny  all;\n       }\n       location ~ ^/(site(-[^/]+)?|wire)/templates(-admin)?/.*\\.(inc|html?|php|tpl) {\n                deny  all;\n       }\n\n       ### GLOBAL REWRITE\n       location / {\n                try_files  $uri  $uri/  /index.php?it=$uri&$args;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass unix:/var/run/php5-fpm.sock;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pylons/nginx.vhost.conf",
    "content": "server {\n        server_name domain.tld;\n\n        location / {\n                # host and port to fastcgi server\n                fastcgi_pass 127.0.0.1:8080;\n                include fastcgi_params;\n                fastcgi_pass_header Authorization;\n                fastcgi_intercept_errors off;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/drop.conf",
    "content": "location = /robots.txt  { access_log off; log_not_found off; }\nlocation = /favicon.ico { access_log off; log_not_found off; }\nlocation ~ /\\.          { access_log off; log_not_found off; deny all; }\nlocation ~ ~$           { access_log off; log_not_found off; deny all; }\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/fastcgi_params",
    "content": "fastcgi_param  QUERY_STRING       $query_string;\nfastcgi_param  REQUEST_METHOD     $request_method;\nfastcgi_param  CONTENT_TYPE       $content_type;\nfastcgi_param  CONTENT_LENGTH     $content_length;\n\nfastcgi_param  SCRIPT_NAME        $fastcgi_script_name;\nfastcgi_param  REQUEST_URI        $request_uri;\nfastcgi_param  DOCUMENT_URI       $document_uri;\nfastcgi_param  DOCUMENT_ROOT      $document_root;\nfastcgi_param  SERVER_PROTOCOL    $server_protocol;\n\nfastcgi_param  GATEWAY_INTERFACE  CGI/1.1;\nfastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;\n\nfastcgi_param  REMOTE_ADDR        $remote_addr;\nfastcgi_param  REMOTE_PORT        $remote_port;\nfastcgi_param  SERVER_ADDR        $server_addr;\nfastcgi_param  SERVER_PORT        $server_port;\nfastcgi_param  SERVER_NAME        $server_name;\n\n# PHP only, required if PHP was built with --enable-force-cgi-redirect\nfastcgi_param  REDIRECT_STATUS    200;\n\nfastcgi_connect_timeout 60;\nfastcgi_send_timeout 180;\nfastcgi_read_timeout 180;\nfastcgi_buffer_size 128k;\nfastcgi_buffers 4 256k;\nfastcgi_busy_buffers_size 256k;\nfastcgi_temp_file_write_size 256k;\nfastcgi_intercept_errors off;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/pyrocms/nginx.conf",
    "content": "server {\n        listen 80;\n        server_name domain.com;\n        root /path/to/webroot;\n        index index.php;\n\n        access_log  /path/to/logs/access.log  main;\n        error_log  /path/to/logs/error.log;\n\n        client_max_body_size 200M;\n\n        gzip  on;\n        gzip_static on;\n        gzip_http_version 1.0;\n        gzip_disable \"MSIE [1-6].\";\n        gzip_vary on;\n\n        gzip_comp_level 9;\n        gzip_proxied any;\n        gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;\n\n        fastcgi_buffers 8 16k;\n        fastcgi_buffer_size 32k;\n        fastcgi_read_timeout 180;\n\n        location / {\n                try_files $uri $uri/ /index.php;\n        }\n\n         location /installer {\n                try_files $uri $uri/ /installer/index.php;\n        }\n\n        fastcgi_intercept_errors off;\n\n        location ~* \\.(?:ico|css|js|gif|jpe?g|png)$ {\n                expires max;\n                add_header Pragma public;\n                add_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n        }\n\n        location ~ \\.php {\n                fastcgi_pass   unix:/tmp/domain.sock;\n                fastcgi_split_path_info ^(.+\\.php)(.*)$;\n                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n                include        fastcgi_params;\n        }\n\n        include drop.conf;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/qwebric/redirect.conf",
    "content": "server {\n    listen 127.0.0.1:80;\n    listen [::1]:80;\n    server_name webchat.domain.tld;\n    return 301 $scheme://webchat.domain.tld:8080$request_uri;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/qwebric/reverse-proxy.conf",
    "content": "server {\n    listen 127.0.0.1:80;\n    listen [::1]:80;\n\n    server_name webchat.domain.tld;\n\n    access_log /home/web/log/qwebirc.access.log;\n    error_log /home/web/log/qwebirc.error.log;\n\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_buffering off;\n\n    location / {\n        proxy_pass http://127.0.0.1:9090;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/redaxo/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\t   \n       location ^~ /redaxo/include {\n                deny all;\n       }\n\n       location / {\n                try_files $uri $uri/ /index.php?$args;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass unix:/var/run/php5-fpm.sock;\n                fastcgi_index index.php;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_intercept_errors on;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/redmine/nginx.conf",
    "content": "upstream redmine {\n        server 127.0.0.1:8000;\n        server 127.0.0.1:8001;\n        server 127.0.0.1:8002;\n}\n\nserver {\n        server_name redmine.DOMAIN.TLD;\n        root /var/www/redmine/public;\n\n        location / {\n                try_files $uri @redmine;\n        }\n\n        location @redmine {\n                proxy_set_header  X-Forwarded-For $remote_addr;\n                proxy_pass http://redmine;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/reverseproxycachingexample/nginx.conf",
    "content": "http {\n    proxy_cache_path  /data/nginx/cache  levels=1:2    keys_zone=STATIC:10m\n    inactive=24h  max_size=1g;\n    server {\n        location / {\n            proxy_pass             http://1.2.3.4;\n            proxy_set_header       Host $host;\n            proxy_cache            STATIC;\n            proxy_cache_valid      200  1d;\n            proxy_cache_use_stale  error timeout invalid_header updating\n                                   http_500 http_502 http_503 http_504;\n        }\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/roundcube/sites-available/example.com.vhost.conf",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location ~ ^/favicon.ico$ {\n                root /var/www/www.example.com/web/skins/default/images;\n                log_not_found off;\n                access_log off;\n                expires max;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       location ~ ^/(README|INSTALL|LICENSE|CHANGELOG|UPGRADING)$ {\n                deny all;\n       }\n       location ~ ^/(bin|SQL)/ {\n                deny all;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_index index.php;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/separateerrorloggingpervirtualhost/nginx.conf",
    "content": "error_log logs/main_error.log;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    error_log logs/http_error.log error;\n    server {\n        server_name one.org;\n        access_log logs/one.access;\n        error_log logs/one.error error;\n    }\n\n    server {\n        server_name two.org;\n        access_log logs/two.access;\n        error_log logs/two.error error;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/catchall.conf",
    "content": "http {\n  index index.html;\n\n  server {\n    listen 80 default_server;\n    server_name _; # This is just an invalid value which will never trigger on a real hostname.\n    access_log logs/default.access.log main;\n\n    server_name_in_redirect off;\n\n    root  /var/www/default/htdocs;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/two.conf",
    "content": "http {\n  index index.html;\n\n  server {\n    server_name www.domain1.com;\n    access_log logs/domain1.access.log main;\n\n    root /var/www/domain1.com/htdocs;\n  }\n\n  server {\n    server_name www.domain2.com;\n    access_log  logs/domain2.access.log main;\n\n    root /var/www/domain2.com/htdocs;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/server_blocks/wildcard-subdomains.conf",
    "content": "server {\n  # Replace this port with the right one for your requirements\n  listen 80 default_server;  #could also be 1.2.3.4:80\n\n  # Multiple hostnames separated by spaces.  Replace these as well.\n  server_name star.yourdomain.com *.yourdomain.com; # Alternately: _\n\n  root /PATH/TO/WEBROOT;\n\n  error_page 404 errors/404.html;\n  access_log logs/star.yourdomain.com.access.log;\n\n  index index.php index.html index.htm;\n\n  # static file 404's aren't logged and expires header is set to maximum age\n  location ~* \\.(jpg|jpeg|gif|css|png|js|ico|html)$ {\n    access_log off;\n    expires max;\n  }\n\n  location ~ \\.php$ {\n    include fastcgi_params;\n    fastcgi_intercept_errors on;\n    # By all means use a different server for the fcgi processes if you need to\n    fastcgi_pass   127.0.0.1:YOURFCGIPORTHERE;\n  }\n\n  location ~ /\\.ht {\n    deny  all;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/shopware/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n\n       ## SSL directives might go here\n       ## see http://www.howtoforge.com/how_to_set_up_ssl_vhosts_under_nginx_plus_sni_support_ubuntu_11.04_debian_squeeze\n       ## if you want to enable SSL for this vhost\n\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       location ~ /\\. {\n                deny all;\n       }\n\n       location / {\n                # Set DirectoryIndex\n                index  shopware.php index.php;\n\n                # Deny direct access to all smarty templates\n                location ~ ^/templates/.*/*.tpl {\n                         deny all;\n                }\n\n                # Deny access to php files in web readable directories\n                location ~ ^/cache/.*/.*\\.(php|cgi|php5|php3|php4|phtml|pl|py) {\n                         deny all;\n                }\n                location ~ ^/images/.*/.*\\.(php|cgi|php5|php3|php4|phtml|pl|py) {\n                         deny all;\n                }\n                location ~ ^/files/.*/.*\\.(php|cgi|php5|php3|php4|phtml|pl|py) {\n                         deny all;\n                }\n                location ~ ^/upload/.*/.*\\.(php|cgi|php5|php3|php4|phtml|pl|py) {\n                         deny all;\n                }\n\n                # Defining rewrite rules\n                rewrite /images/ayww/(.*) /images/banner/$1 last;\n                rewrite /files/documents/.* /engine last;\n                rewrite /templates/(.*(css|js))$ /engine/backend/php/sCacheTemplate.php?file=/templates/$1 last;\n                rewrite /sitemap.xml(.*) /shopware.php?controller=SitemapXml;\n                rewrite /application.yaml /engine last;\n                rewrite /engine/core/php/sAjaxSearch.php$ /engine/backend/php/sAjaxSearch.php last;\n                rewrite /engine/core/php/campaigns.php$ /engine/backend/php/campaigns.php last;\n\n                # Defining controller based route processing behaviour\n                if (!-e $request_filename) {\n                         rewrite . /shopware.php last;\n                }\n       }\n\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_param  HTTPS $fastcgi_https;\n       }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/shopware4/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       ## SSL directives might go here\n       ## see http://www.howtoforge.com/how_to_set_up_ssl_vhosts_under_nginx_plus_sni_support_ubuntu_11.04_debian_squeeze\n       ## if you want to enable SSL for this vhost\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       location ~ /\\. {\n                deny all;\n       }\n       location ~ /(engine|images/[a-z]+|files|templates)/ {\n       }\n       location / {\n                index index.html index.php shopware.php\n                rewrite shopware.dll /shopware.php;\n                rewrite files/documents/.* /engine last;\n                rewrite images/ayww/(.*) /images/banner/$1 last;\n                rewrite backend/media/(.*) /media/$1 last;\n                if (!-e $request_filename){\n                    rewrite . /shopware.php last;\n                }\n       }\n       location ~ \\.(tpl|yml|ini)$ {\n                deny all;\n       }\n       location /install {\n                location /install/assets {\n                }\n                if (!-e $request_filename){\n                    rewrite . /install/index.php last;\n                }\n       }\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_param  HTTPS $fastcgi_https;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/silverstripe/nginx.conf",
    "content": "server {\n    listen 80;\n    root /path/to/ss/folder;\n\n    server_name site.com www.site.com;\n\n    location / {\n        try_files $uri /framework/main.php?url=$uri&$query_string;\n    }\n\n    error_page 404 /assets/error-404.html;\n    error_page 500 /assets/error-500.html;\n\n    location ^~ /assets/ {\n        sendfile on;\n        try_files $uri =404;\n    }\n\n    location ~ /framework/.*(main|rpc|tiny_mce_gzip)\\.php$ {\n        fastcgi_keep_conn on;\n        fastcgi_pass   127.0.0.1:9000;\n        fastcgi_index  index.php;\n        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        include        fastcgi_params;\n    }\n\n    location ~ /(mysite|framework|cms)/.*\\.(php|php3|php4|php5|phtml|inc)$ {\n        deny all;\n    }\n\n    location ~ /\\.. {\n        deny all;\n    }\n\n    location ~ \\.ss$ {\n        satisfy any;\n        allow 127.0.0.1;\n        deny all;\n    }\n\n    location ~ web\\.config$ {\n        deny all;\n    }\n\n    location ~ \\.ya?ml$ {\n        deny all;\n    }\n\n    location ^~ /vendor/ {\n        deny all;\n    }\n\n    location ~* /silverstripe-cache/ {\n        deny all;\n    }\n\n    location ~* composer\\.(json|lock)$ {\n        deny all;\n    }\n\n    location ~* /(cms|framework)/silverstripe_version$ {\n        deny all;\n    }\n\n    location ~ \\.php$ {\n                fastcgi_keep_conn on;\n                fastcgi_pass   127.0.0.1:9000;\n                fastcgi_index  index.php;\n                fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                include        fastcgi_params;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplecgi/nginx.conf",
    "content": "http {\n  root  /var/www/htdocs;\n  index index.html;\n  location ~ ^/cgi-bin/.*\\.cgi$ {\n    gzip off; #gzip makes scripts feel slower since they have to complete before getting gzipped\n    fastcgi_pass  unix:/var/run/nginx/cgiwrap-dispatch.sock;\n    fastcgi_index index.cgi;\n    fastcgi_param SCRIPT_FILENAME /var/www/cgi-bin$fastcgi_script_name;\n    fastcgi_param QUERY_STRING     $query_string;\n    fastcgi_param REQUEST_METHOD   $request_method;\n    fastcgi_param CONTENT_TYPE     $content_type;\n    fastcgi_param CONTENT_LENGTH   $content_length;\n    fastcgi_param GATEWAY_INTERFACE  CGI/1.1;\n    fastcgi_param SERVER_SOFTWARE    nginx;\n    fastcgi_param SCRIPT_NAME        $fastcgi_script_name;\n    fastcgi_param REQUEST_URI        $request_uri;\n    fastcgi_param DOCUMENT_URI       $document_uri;\n    fastcgi_param DOCUMENT_ROOT      $document_root;\n    fastcgi_param SERVER_PROTOCOL    $server_protocol;\n    fastcgi_param REMOTE_ADDR        $remote_addr;\n    fastcgi_param REMOTE_PORT        $remote_port;\n    fastcgi_param SERVER_ADDR        $server_addr;\n    fastcgi_param SERVER_PORT        $server_port;\n    fastcgi_param SERVER_NAME        $server_name;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplegroupware/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n                expires max;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n       client_max_body_size 1000M;\n       dav_methods PUT DELETE MKCOL COPY MOVE;\n       create_full_put_path on;\n       dav_access user:rw group:rw all:r;\n\n       # WebDAV server\n       location ~ ^/sgdav {\n                rewrite . /bin/webdav.php;\n       }\n       # CMS real URLs\n       location ~ ^/cms/ {\n                rewrite ^/cms/ext/(.*)$ /bin/ext/cms/$1 last;\n                rewrite ^/cms/thumbs/(.*)$ /bin/preview.php?filename=$1 last;\n                rewrite ^/cms/(.*?)/file/(.*)$ /bin/cms.php?page=$1&file=$2 last;\n                rewrite ^/cms/(.*)$ /bin/cms.php?page=$1 last;\n       }\n       # Root\n       location = / {\n                if ($request_method = \"OPTIONS\") {\n                   rewrite . /bin/webdav.php last;\n                }\n                try_files /bin/index.php /src/index.php /sgs_installer.php =404;\n\n                include /etc/nginx/fastcgi_params;\n                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n                fastcgi_pass   127.0.0.1:9000; # use spawn-fcgi (!!)\n       }\n       # Root PHP /*.php\n       location ~ ^/([^/]+\\.php)$ {\n                try_files /bin/$1 /src/$1 $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n                fastcgi_pass   127.0.0.1:9000; # use spawn-fcgi (!!)\n       }\n       # sgs src/*.php bin/*.php\n       location ~ ^/(src|bin)/([^/]+\\.php|ext/.+\\.php)$ {\n                include /etc/nginx/fastcgi_params;\n                fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;\n                fastcgi_pass   127.0.0.1:9000; # use spawn-fcgi (!!)\n       }\n       # Redirect static files\n       location ~ ^/(src/|bin/)?(ext/.*|docs/.*)$ {\n                try_files /custom/$2 /ext/$2 /bin/$2 /src/$2 $uri =404;\n       }\n       # Drop all other stuff\n       location / {\n                if (!-f $request_filename) { return 404; }\n                #return 403;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplepythonfcgi/fastcgi.conf",
    "content": "#fastcgi.conf\nfastcgi_param GATEWAY_INTERFACE CGI/1.1;\nfastcgi_param SERVER_SOFTWARE nginx;\n\nfastcgi_param QUERY_STRING $query_string;\nfastcgi_param REQUEST_METHOD $request_method;\nfastcgi_param CONTENT_TYPE $content_type;\nfastcgi_param CONTENT_LENGTH $content_length;\n\nfastcgi_param SCRIPT_NAME $fastcgi_script_name;\nfastcgi_param REQUEST_URI $request_uri;\nfastcgi_param DOCUMENT_URI $document_uri;\nfastcgi_param DOCUMENT_ROOT $document_root;\nfastcgi_param SERVER_PROTOCOL $server_protocol;\n\nfastcgi_param REMOTE_ADDR $remote_addr;\nfastcgi_param REMOTE_PORT $remote_port;\nfastcgi_param SERVER_ADDR $server_addr;\nfastcgi_param SERVER_PORT $server_port;\nfastcgi_param SERVER_NAME $server_name;\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplepythonfcgi/nginx.conf",
    "content": "# static files\nlocation ~ ^/(images|javascript|js|css|flash|media|static)/ {\n  root /PROJECTBASE/PROJECTNAME/static;\n}\n\nlocation = /favicon.ico {\n  root /PROJECTBASE/PROJECTNAME/static/images;\n}\n\n# pass all requests to FastCGI TG server listening on ${HOST}:${PORT}\n#\nlocation / {\n   fastcgi_pass 127.0.0.1:9000;\n   fastcgi_index index;\n   fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;\n   include conf/fastcgi_params;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplepythonfcgi/weird-spacing.conf",
    "content": " # static files\n location ~ ^/(images|javascript|js|css|flash|media|static)/ {\n   root \"${PROJECTBASE}/${PROJECTNAME}/static\";\n }\n\n location = /favicon.ico {\n   root \"${PROJECTBASE}/${PROJECTNAME}/static/images\";\n }\n\n # pass all requests to FastCGI TG server listening on ${HOST}:${PORT}\n #\n location / {\n   fastcgi_pass \"${HOST}:${PORT}\";\n   fastcgi_index index;\n   fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;\n   include conf/fastcgi_params;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/simplerubyfcgi/nginx.conf",
    "content": "user http;\nworker_processes  3;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n    include            mime.types;\n    default_type       application/octet-stream;\n    sendfile           on;\n    tcp_nopush         on;\n    keepalive_timeout  65;\n    gzip               on;\n    gzip_types         text/plain text/css text/javascript\n                       application/javascript application/json\n                       application/xml;\n    index              index.html index.htm;\n\n    server {\n        listen       80;\n        server_name  .example.com;\n        root         /srv/http/my_app/public;\n        location / {\n            proxy_set_header  X-Real-IP        $remote_addr;\n            proxy_set_header  X-Forwarded-For  $proxy_add_x_forwarded_for;\n            proxy_set_header  Host             $http_host;\n            proxy_redirect    off;\n            proxy_pass        http://unix:/var/run/my_app.sock:;\n        }\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/spip/nginx.conf",
    "content": "server {\n    server_name emeagwali.net www.emeagwali.net;\n    client_max_body_size 10m;\n    root /var/www/spip;\n    index index.php;\n\n    location / {\n        try_files $uri $uri/ /spip.php?q=$uri&$args;\n    }\n\n    location ~^/(tmp|config)/{\n        return 403;\n    }\n\n    location ~ \\.php$ {\n        fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n        fastcgi_pass   127.0.0.1:9000;\n        fastcgi_index  index.php ;\n        fastcgi_buffers 16 16k;\n        fastcgi_buffer_size 32k;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/sugarcrm/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n       location / {\n                try_files $uri $uri/ /index.php?$args;\n       }\n       # Add trailing slash to */wp-admin requests.\n       rewrite /wp-admin$ $scheme://$host$uri/ permanent;\n       location ~*  \\.(jpg|jpeg|png|gif|css|js|ico)$ {\n                expires max;\n                log_not_found off;\n       }\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/nginx.conf",
    "content": "server {\n    server_name domain.tld www.domain.tld;\n    root /var/www/project/web;\n\n    location / {\n        # try to serve file directly, fallback to app.php\n        try_files $uri /app.php$is_args$args;\n    }\n    # DEV\n    # This rule should only be placed on your development environment\n    # In production, don't include this and don't deploy app_dev.php or config.php\n    location ~ ^/(app_dev|config)\\.php(/|$) {\n        fastcgi_pass unix:/var/run/php5-fpm.sock;\n        fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n        include fastcgi_params;\n        # When you are using symlinks to link the document root to the\n        # current version of your application, you should pass the real\n        # application path instead of the path to the symlink to PHP\n        # FPM.\n        # Otherwise, PHP's OPcache may not properly detect changes to\n        # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126\n        # for more information).\n        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n        fastcgi_param DOCUMENT_ROOT $realpath_root;\n    }\n    # PROD\n    location ~ ^/app\\.php(/|$) {\n        fastcgi_pass unix:/var/run/php5-fpm.sock;\n        fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n        include fastcgi_params;\n       # When you are using symlinks to link the document root to the\n       # current version of your application, you should pass the real\n       # application path instead of the path to the symlink to PHP\n       # FPM.\n       # Otherwise, PHP's OPcache may not properly detect changes to\n       # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126\n       # for more information).\n       fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;\n       fastcgi_param DOCUMENT_ROOT $realpath_root;\n       # Prevents URIs that include the front controller. This will 404:\n       # http://domain.tld/app.php/some-path\n       # Remove the internal directive to allow URIs like this\n       internal;\n   }\n\n   # return 404 for all other php files not matching the front controller\n   # this prevents access to other php files you don't want to be accessible.\n   location ~ \\.php$ {\n     return 404;\n   }\n\n   error_log /var/log/nginx/project_error.log;\n   access_log /var/log/nginx/project_access.log;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/old.conf",
    "content": "upstream phpfcgi {\n    server 127.0.0.1:9000;\n    # server unix:/var/run/php5-fpm.sock; #for PHP-FPM running on UNIX socket\n}\nserver {\n    listen 80;\n\n    server_name symfony2;\n    root /var/www/symfony2/web;\n\n    error_log /var/log/nginx/symfony2.error.log;\n    access_log /var/log/nginx/symfony2.access.log;\n\n    # strip app.php/ prefix if it is present\n    rewrite ^/app\\.php/?(.*)$ /$1 permanent;\n\n    location / {\n        index app.php;\n        try_files $uri @rewriteapp;\n    }\n\n    location @rewriteapp {\n        rewrite ^(.*)$ /app.php/$1 last;\n    }\n\n    # pass the PHP scripts to FastCGI server from upstream phpfcgi\n    location ~ ^/(app|app_dev|config)\\.php(/|$) {\n        fastcgi_pass phpfcgi;\n        fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n        include fastcgi_params;\n        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_param  HTTPS off;\n    }\n}\n\n\nserver {\n    listen 443;\n\n    server_name symfony2;\n    root /var/www/symfony2/web;\n\n    ssl on;\n    ssl_certificate /etc/ssl/certs/symfony2.crt;\n    ssl_certificate_key /etc/ssl/private/symfony2.key;\n\n    error_log /var/log/nginx/symfony2.error.log;\n    access_log /var/log/nginx/symfony2.access.log;\n\n    # strip app.php/ prefix if it is present\n    rewrite ^/app\\.php/?(.*)$ /$1 permanent;\n\n    location / {\n        index app.php;\n        try_files $uri @rewriteapp;\n    }\n\n    location @rewriteapp {\n        rewrite ^(.*)$ /app.php/$1 last;\n    }\n\n    # pass the PHP scripts to FastCGI server from upstream phpfcgi\n    location ~ ^/(app|app_dev|config)\\.php(/|$) {\n        fastcgi_pass phpfcgi;\n        fastcgi_split_path_info ^(.+\\.php)(/.*)$;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_param HTTPS on;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/symfony/oldold.conf",
    "content": "server {\n  listen 80;\n\n  server_name mysite.com;\n\n  root /var/www/mysite.com/web;\n  access_log /var/log/nginx/mysite.com.access.log;\n  error_log /var/log/nginx/mysite.com.error.log;\n\n  location ~ ^/(index|frontend|frontend_dev|backend|backend_dev)\\.php$ {\n    include fastcgi_params;\n    fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n    fastcgi_param PATH_INFO $fastcgi_path_info;\n    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;\n    fastcgi_param HTTPS off;\n    fastcgi_pass   127.0.0.1:9000;\n  }\n\n  location / {\n    index index.php;\n    try_files $uri /index.php?$args;\n  }\n}\n\nserver {\n  listen 443;\n\n  ssl on;\n  ssl_certificate      /etc/ssl/certs/mysite.com.crt;\n  ssl_certificate_key  /etc/ssl/private/mysite.com.key;\n\n  server_name mysite.com;\n\n  root /var/www/mysite.com/web;\n  access_log /var/log/nginx/mysite.com.access.log;\n  error_log /var/log/nginx/mysite.com.error.log;\n  location ~ ^/(index|frontend|frontend_dev|backend|backend_dev)\\.php$ {\n    include fastcgi_params;\n    fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n    fastcgi_param PATH_INFO $fastcgi_path_info;\n    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;\n    fastcgi_param HTTPS on;\n    fastcgi_pass   127.0.0.1:9000;\n  }\n\n  location / {\n    index index.php;\n    try_files $uri /index.php?$args;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/typo3-4.6/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n       index index.php index.html;\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n                expires max;\n       }\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n       location ~*  \\.(jpg|jpeg|png|gif|css|js|ico)$ {\n                expires max;\n                log_not_found off;\n       }\n       location ~* \\.(cur|ico|gif|png|jpe?g|css|js|swf|woff)((\\?\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d)|(\\?s=\\d\\d\\d\\d\\d\\d\\d\\d\\d\\d))$ {\n                expires max;\n                log_not_found off;\n       }\n       location ~* \\.(cur|ico|gif|png|jpe?g|css|js|swf|woff)(\\?v\\d\\d?\\.\\d\\d?\\.\\d\\d?)$ {\n                expires max;\n                log_not_found off;\n       }\n       location ~* ^(/typo3/sysext|/typo3conf/ext).*\\.(cur|ico|gif|png|jpe?g|css|js|swf|woff) {\n                expires max;\n                log_not_found off;\n       }\n       location = /clear.gif {\n                empty_gif;\n                expires max;\n       }\n       location ^~ /typo3/gfx {\n                expires max;\n       }\n       location ^~ /typo3temp/compressor {\n                expires max;\n       }\n       location ~* \\.(sql|htaccess|htpasswd|tpl|html5|xhtml) {\n                deny all;\n       }\n       location / {\n                if ($query_string ~ \".+\") {\n                        return 405;\n                }\n                # pass requests from logged-in users to PHP\n                if ($http_cookie ~ 'nc_staticfilecache|be_typo_user' ) {\n                        return 405;\n                } # pass POST requests to PHP\n                if ($request_method !~ ^(GET|HEAD)$ ) {\n                        return 405;\n                }\n                if ($http_pragma = 'no-cache') {\n                        return 405;\n                }\n                if ($http_cache_control = 'no-cache') {\n                        return 405;\n                }\n                error_page 405 = @nocache;\n                # serve requested content from the cache if available, otherwise pass the request to PHP\n                try_files /typo3temp/tx_ncstaticfilecache/$host${request_uri}index.html @nocache;\n       }\n       location @nocache {\n                try_files $uri $uri/ /index.php?$args;\n       }\n       location ^~ /typo3temp/tx_ncstaticfilecache {\n                expires 43200;\n                charset utf-8;\n       }\n       location ~ \\.php$ {\n                try_files $uri =404;\n                include /etc/nginx/fastcgi_params;\n                fastcgi_pass 127.0.0.1:9000;\n                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                fastcgi_index index.php;\n       }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/typo3-6.2/sites-available/www.example.com.vhost",
    "content": "server {\n       listen 80;\n       server_name www.example.com example.com;\n       root /var/www/www.example.com/web;\n\n       if ($http_host != \"www.example.com\") {\n                 rewrite ^ http://www.example.com$request_uri permanent;\n       }\n\n       index index.php index.html;\n\n       location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n       }\n\n       location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n       }\n\n       # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).\n       location ~ /\\. {\n                deny all;\n                access_log off;\n                log_not_found off;\n       }\n\n        location ~ \\.php$ {\n                        try_files $uri =404;\n                        include /etc/nginx/fastcgi_params;\n                        fastcgi_pass unix:/var/run/php5-fpm.sock;\n                        fastcgi_index index.php;\n                        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n                        fastcgi_intercept_errors on;\n                        fastcgi_buffer_size 128k;\n                        fastcgi_buffers 256 16k;\n                        fastcgi_busy_buffers_size 256k;\n                        fastcgi_temp_file_write_size 256k;\n                        fastcgi_read_timeout 1200;\n        }\n\t\t\n        client_max_body_size 100M;\n\n        location ~ /\\.(js|css)$ {\n                expires 604800s;\n        }\n\n        if (!-e $request_filename){\n                rewrite ^/(.+)\\.(\\d+)\\.(php|js|css|png|jpg|gif|gzip)$ /$1.$3 last;\n        }\n\n        location ~* ^/fileadmin/(.*/)?_recycler_/ {\n                deny all;\n        }\n        location ~* ^/fileadmin/templates/.*(\\.txt|\\.ts)$ {\n                deny all;\n        }\n        location ~* ^/typo3conf/ext/[^/]+/Resources/Private/ {\n                deny all;\n        }\n        location ~* ^/(typo3/|fileadmin/|typo3conf/|typo3temp/|uploads/|favicon\\.ico) {\n        }\n\n        location / {\n                        if ($query_string ~ \".+\") {\n                                return 405;\n                        }\n                        if ($http_cookie ~ 'nc_staticfilecache|be_typo_user|fe_typo_user' ) {\n                                return 405;\n                        } # pass POST requests to PHP\n                        if ($request_method !~ ^(GET|HEAD)$ ) {\n                                return 405;\n                        }\n                        if ($http_pragma = 'no-cache') {\n                                return 405;\n                        }\n                        if ($http_cache_control = 'no-cache') {\n                                return 405;\n                        }\n                        error_page 405 = @nocache;\n\n                        try_files /typo3temp/tx_ncstaticfilecache/$host${request_uri}index.html @nocache;\n        }\n\n        location @nocache {\n                        try_files $uri $uri/ /index.php$is_args$args;\n        }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/upstream-issue-17/nginx.conf",
    "content": "# from https://github.com/fatiherikli/nginxparser/issues/17\n\nserver {\n  listen 443 ssl;\n  server_name prod.example.com example.com stage.example.com;\n\n  ssl_certificate ssl.crt/example.com.crt;\n  ssl_certificate_key ssl.key/example.com.key;\n\n  keepalive_timeout 5;\n\n  access_log /var/log/nginx/example.access.log;\n  error_log /var/log/nginx/example.error.log;\n\n  root /var/www/example/cs/current;\n\n  client_max_body_size 20M;\n\n  location / {\n    try_files $uri $uri/index.html @cs_shared;\n  }\n\n  location /images {\n    expires 30d;\n  }\n\n  location /scripts {\n    expires 30d;\n  }\n\n  location /styles {\n    expires 30d;\n  }\n\n  location @cs_shared {\n    root /var/www/example/cs/shared/public;\n    try_files $uri $uri/index.html @ss;\n  }\n\n  location @ss {\n    root /var/www/example/ss/current/public;\n    try_files $uri @index;\n  }\n\n  location @index {\n    root /var/www/example/cs/current;\n    rewrite ^ /index.html break;\n  }\n\n  location @rails {\n    proxy_pass http://example-ror;\n\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Real-IP $remote_addr;\n    proxy_set_header Host $host;\n    proxy_set_header X-Request-Start \"t=${msec}\";\n    proxy_redirect off;\n\n    proxy_read_timeout 300;\n\n    proxy_buffer_size 16k;\n    proxy_buffers 32 16k;\n\n  }\n\n  location ~* ^/(api|assets|system|rich|admin) {\n    root /var/www/example/ss/current/public;\n    try_files $uri @rails;\n  }\n\n  error_page 500 502 503 504 /500.html;\n  location = /500.html {\n    root /var/www/example/cs/current;\n  }\n\n  error_page 405 =200 $uri;\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/upstream-issue-19/nginx.conf",
    "content": "# from https://github.com/fatiherikli/nginxparser/issues/19\n\nmap $http_user_agent $mobile {\n    default       0;\n    \"~Opera Mini\" 1;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/multisite-subdir.conf",
    "content": "map $uri $blogname{\n    ~^(?P<blogpath>/[^/]+/)files/(.*)       $blogpath ;\n}\n\nmap $blogname $blogid{\n    default -999;\n\n    #Ref: http://wordpress.org/extend/plugins/nginx-helper/\n    #include /var/www/wordpress/wp-content/plugins/nginx-helper/map.conf ;\n}\n\nserver {\n    server_name example.com ;\n\n    root /var/www/example.com/htdocs;\n    index index.php;\n\n    location ~ ^(/[^/]+/)?files/(.+) {\n        try_files /wp-content/blogs.dir/$blogid/files/$2 /wp-includes/ms-files.php?file=$2 ;\n        access_log off;     log_not_found off; expires max;\n    }\n\n    #avoid php readfile()\n    location ^~ /blogs.dir {\n        internal;\n        alias /var/www/example.com/htdocs/wp-content/blogs.dir ;\n        access_log off;     log_not_found off; expires max;\n    }\n\n    if (!-e $request_filename) {\n        rewrite /wp-admin$ $scheme://$host$uri/ permanent;\n        rewrite ^(/[^/]+)?(/wp-.*) $2 last;\n        rewrite ^(/[^/]+)?(/.*\\.php) $2 last;\n    }\n\n    location / {\n        try_files $uri $uri/ /index.php?$args ;\n    }\n\n    location ~ \\.php$ {\n        try_files $uri =404;\n        include fastcgi_params;\n        fastcgi_pass php;\n    }\n\n    #add some rules for static content expiry-headers here\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/multisite-subdomain.conf",
    "content": "map $http_host $blogid {\n    default       -999;\n\n    #Ref: http://wordpress.org/extend/plugins/nginx-helper/\n    #include /var/www/wordpress/wp-content/plugins/nginx-helper/map.conf ;\n\n}\n\nserver {\n    server_name example.com *.example.com ;\n\n    root /var/www/example.com/htdocs;\n    index index.php;\n\n    location / {\n        try_files $uri $uri/ /index.php?$args ;\n    }\n\n    location ~ \\.php$ {\n        try_files $uri =404;\n        include fastcgi_params;\n        fastcgi_pass php;\n    }\n\n    #WPMU Files\n        location ~ ^/files/(.*)$ {\n                try_files /wp-content/blogs.dir/$blogid/$uri /wp-includes/ms-files.php?file=$1 ;\n                access_log off; log_not_found off;      expires max;\n        }\n\n    #WPMU x-sendfile to avoid php readfile()\n    location ^~ /blogs.dir {\n        internal;\n        alias /var/www/example.com/htdocs/wp-content/blogs.dir;\n        access_log off;     log_not_found off;      expires max;\n    }\n\n    #add some rules for static content expiry-headers here\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress/nginx.conf",
    "content": "# Upstream to abstract backend connection(s) for php\nupstream php {\n        server unix:/tmp/php-cgi.socket;\n        server 127.0.0.1:9000;\n}\n\nserver {\n        ## Your website name goes here.\n        server_name domain.tld;\n        ## Your only path reference.\n        root /var/www/wordpress;\n        ## This should be in your http block and if it is, it's not needed here.\n        index index.php;\n\n        location = /favicon.ico {\n                log_not_found off;\n                access_log off;\n        }\n\n        location = /robots.txt {\n                allow all;\n                log_not_found off;\n                access_log off;\n        }\n\n        location / {\n                # This is cool because no php is touched for static content.\n                # include the \"?$args\" part so non-default permalinks doesn't break when using query string\n                try_files $uri $uri/ /index.php?$args;\n        }\n\n        location ~ \\.php$ {\n                #NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n                include fastcgi.conf;\n                fastcgi_intercept_errors on;\n                fastcgi_pass php;\n        }\n\n        location ~* \\.(js|css|png|jpg|jpeg|gif|ico)$ {\n                expires max;\n                log_not_found off;\n        }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/no-cache.conf",
    "content": "server {\n\tlisten 80;\n \n\tserver_name kbeezie.com www.kbeezie.com;\n\troot html/kbeezie.com;\n \n\taccess_log logs/kbeezie.access.log;\n\terror_log logs/kbeezie.error.log;\n \n\t# Simply using try_files, tests the request uri against a file, then folder\n\t# then if neither can be found, the request is sent to index.php\n\t# this is a lot simpler than the .htaccess method of rewriting permalinks\n \n\tlocation / { \n\t\ttry_files $uri $uri/ /index.php; \n\t}\n \n\t# Normally you do not need this if you are not using any error_page directive\n\t# but having it off allows Wordpress to return it's own error page\n\t# rather than the plain Nginx screen\n \n\tfastcgi_intercept_errors off;\n \n\t# Caching the typical static files such as css, js, jpg, png and so forth\n\t# helps in telling the browser they can cache the content\n\tlocation ~* \\.(ico|css|js|gif|jpe?g|png)$ {\n\t\texpires max;\n\t\tadd_header Pragma public;\n\t\tadd_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n\t}\n \n\t# I like to place my php stuff into it's own file\n\t# see http://kbeezie.com/view/nginx/ for more information\n\tinclude php.conf;\n \n\t# We don't really need to log favicon requests\n\tlocation = /favicon.ico { access_log off; log_not_found off; }\t\n \n\t# We don't want to allow the browsers to see .hidden linux/unix files\n\tlocation ~ /\\. { deny  all; access_log off; log_not_found off; }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/supercache.conf",
    "content": "server {\n\tlisten 80;\n \n\tserver_name kbeezie.com www.kbeezie.com;\n\troot html/kbeezie.com;\n \n\taccess_log logs/kbeezie.access.log;\n\terror_log logs/kbeezie.error.log;\n \n\tlocation / { \n\t\t# This line when enabled will use Nginx's gzip static module\n\t\tgzip_static on;\n \n\t\t# Disables serving gzip content to IE 6 or below\n\t\tgzip_disable        \"MSIE [1-6]\\.\";\n \n\t\t# Sets the default type to text/html so that gzipped content is served\n\t\t# as html, instead of raw uninterpreted data.\n\t\tdefault_type text/html;\n \n\t\t# does the requested file exist exactly as it is? if yes, serve it and stop here\n\t\tif (-f $request_filename) { break; }\n \n\t\t# sets some variables to help test for the existence of a cached copy of the request\n\t\tset $supercache_file '';\n\t\tset $supercache_uri $request_uri;\n \n\t\t# IF the request is a post, has a query attached, or a cookie\n\t\t# then don't serve the cache (ie: users logged in, or posting comments)\n\t\tif ($request_method = POST) { set $supercache_uri ''; }\n\t\tif ($query_string) { set $supercache_uri ''; }\n\t\tif ($http_cookie ~* \"comment_author_|wordpress|wp-postpass_\" ) { \n\t\t\tset $supercache_uri ''; \n\t\t}\n \n\t\t# if the supercache_uri variable hasn't been blanked by this point, attempt\n\t\t# to set the name of the destination to the possible cache file\n\t\tif ($supercache_uri ~ ^(.+)$) { \n\t\t\tset $supercache_file /wp-content/cache/supercache/$http_host/$1index.html; \n\t\t}\n \n\t\t# If a cache file of that name exists, serve it directly\n\t\tif (-f $document_root$supercache_file) { rewrite ^ $supercache_file break; }\n \n\t\t# Otherwise send the request back to index.php for further processing\n\t\tif (!-e $request_filename) { rewrite . /index.php last; }\n\t}\n \n\t# Normally you do not need this if you are not using any error_page directive\n\t# but having it off allows Wordpress to return it's own error page\n\t# rather than the plain Nginx screen\n \n\tfastcgi_intercept_errors off;\n \n\t# Caching the typical static files such as css, js, jpg, png and so forth\n\t# helps in telling the browser they can cache the content\n\tlocation ~* \\.(ico|css|js|gif|jpe?g|png)$ {\n\t\texpires max;\n\t\tadd_header Pragma public;\n\t\tadd_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n\t}\n \n\t# I like to place my php stuff into it's own file\n\t# see http://kbeezie.com/view/nginx/ for more information\n\tinclude php.conf;\n \n\t# We don't really need to log favicon requests\n\tlocation = /favicon.ico { access_log off; log_not_found off; }\t\n \n\t# We don't want to allow the browsers to see .hidden linux/unix files\n\tlocation ~ /\\. { deny  all; access_log off; log_not_found off; }\n}\n\n\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/total-cache.conf",
    "content": "server {\n\tlisten 80;\n \n\tserver_name kbeezie.com www.kbeezie.com;\n\troot html/kbeezie.com;\n \n\taccess_log logs/kbeezie.access.log;\n\terror_log logs/kbeezie.error.log;\n \n\t# Simply using try_files, tests the request uri against a file, then folder\n\t# then if neither can be found, the request is sent to index.php\n\t# this is a lot simpler than the .htaccess method of rewriting permalinks\n \n\tlocation / { \n\t\ttry_files $uri $uri/ /index.php; \n\t}\n \n\t# Normally you do not need this if you are not using any error_page directive\n\t# but having it off allows Wordpress to return it's own error page\n\t# rather than the plain Nginx screen\n \n\tfastcgi_intercept_errors off;\n \n\t# Caching the typical static files such as css, js, jpg, png and so forth\n\t# helps in telling the browser they can cache the content\n\tlocation ~* \\.(ico|css|js|gif|jpe?g|png)$ {\n\t\texpires max;\n\t\tadd_header Pragma public;\n\t\tadd_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n\t}\n \n\t# I like to place my php stuff into it's own file\n\t# see http://kbeezie.com/view/nginx/ for more information\n\tinclude php.conf;\n \n\t# We don't really need to log favicon requests\n\tlocation = /favicon.ico { access_log off; log_not_found off; }\t\n \n\t# We don't want to allow the browsers to see .hidden linux/unix files\n\tlocation ~ /\\. { deny  all; access_log off; log_not_found off; }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/wordpress-caching/totalcache-enhanced.conf",
    "content": "server {\n\tlisten 80;\n \n\tserver_name kbeezie.com www.kbeezie.com;\n \n\troot /usr/local/www/kbeezie.com;\n \n\taccess_log /var/log/nginx/kbeezie.access.log;\n\terror_log /var/log/nginx/kbeezie.error.log;\n \n\tlocation / { \n\t\tif (-f $request_filename) {\n\t\t        break;\n\t\t}\n \n\t\tset $w3tc_rewrite 1;\n\t\tif ($request_method = POST) { set $w3tc_rewrite 0; }\n\t\tif ($query_string != \"\") { set $w3tc_rewrite 0; }\n \n\t\tset $w3tc_rewrite2 1;\n\t\tif ($request_uri !~ \\/$) { set $w3tc_rewrite2 0; }\n\t\tif ($request_uri ~* \"(sitemap(_index)?\\.xml(\\.gz)?|[a-z0-9_\\-]+-sitemap([0-9]+)?\\.xml(\\.gz)?)\") { set $w3tc_rewrite2 1; }\n\t\tif ($w3tc_rewrite2 != 1) { set $w3tc_rewrite 0; }\n \n\t\tif ($http_cookie ~* \"(comment_author|wp\\-postpass|wordpress_\\[a\\-f0\\-9\\]\\+|wordpress_logged_in)\") { set $w3tc_rewrite 0; }\n\t\tif ($http_user_agent ~* \"(W3\\ Total\\ Cache/0\\.9\\.2\\.4)\") { set $w3tc_rewrite 0; }\n \n\t\tset $w3tc_ua \"\";\n\t\tset $w3tc_ref \"\";\n\t\tset $w3tc_ssl \"\";\n\t\tset $w3tc_enc \"\";\n \n\t\tif ($http_accept_encoding ~ gzip) { set $w3tc_enc _gzip; }\n \n\t\tset $w3tc_ext \"\";\n\t\tif (-f \"$document_root/wp-content/cache/page_enhanced/$host/$request_uri/_index$w3tc_ua$w3tc_ref$w3tc_ssl.html$w3tc_enc\") {\n\t\t    set $w3tc_ext .html;\n\t\t}\n\t\tif ($w3tc_ext = \"\") { set $w3tc_rewrite 0; }\n \n\t\tif ($w3tc_rewrite = 1) {\n\t\t    rewrite ^ \"/wp-content/cache/page_enhanced/$host/$request_uri/_index$w3tc_ua$w3tc_ref$w3tc_ssl$w3tc_ext$w3tc_enc\" last;\n\t\t}\n \n\t\tif (!-e $request_filename) { \n\t\t\trewrite ^ /index.php last; \n\t\t}\n\t}\n \n\tlocation /search { limit_req zone=kbeezieone burst=3 nodelay; rewrite ^ /index.php; }\n \n\tfastcgi_intercept_errors off;\n \n\tlocation ~* \\.(?:ico|css|js|gif|jpe?g|png)$ {\n\t\texpires max;\n\t\tadd_header Pragma public;\n\t\tadd_header Cache-Control \"public, must-revalidate, proxy-revalidate\";\n\t}\n \n\t# see http://kbeezie.com/view/nginx/ for more information\n\tinclude php.conf;\n\tlocation = /favicon.ico { access_log off; log_not_found off; }\t\n\tlocation ~ /\\. { deny  all; access_log off; log_not_found off; }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/xenforo/nginx.conf",
    "content": "server {\n\n    server_name     localhost;\n\n    root    html/xenforo;\n    index   index.php index.html;\n\n    location / {\n        try_files $uri $uri/ /index.php?$uri&$args;\n    }\n\n    location ~ \\.php$ {\n        fastcgi_pass    127.0.0.1:9000;\n        fastcgi_param   SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        include         fastcgi_params;\n    }\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/yii/nginx.conf",
    "content": "server {\n        server_name domain.tld;\n\n        root /usr/share/nginx/html;\n        index index.html index.php;\n\n        #Yii Specific location configurations.\n\n        #SEF URLs for sampleapp.\n        location /sampleapp {\n         rewrite ^/sampleapp/(.*)$ /sampleapp/index.php?r=$1;\n        }\n\n        location ~ /(protected|framework|nbproject) {\n            deny all;\n            access_log off;\n            log_not_found off;\n        }\n\n        location ~ /themes/\\w+/views {\n            deny all;\n            access_log off;\n            log_not_found off;\n        }\n\n        location ~ \\.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {\n                try_files $uri =404;\n        }\n\n        #End Yii Specific specific location configurations.\n\n        location ~ \\.php$ {\n                root            /usr/share/nginx/html;\n                fastcgi_pass    127.0.0.1:9000;\n                fastcgi_index   index.php;\n                fastcgi_param   SCRIPT_FILENAME /usr/share/nginx/html/$fastcgi_script_name;\n                fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n                include         fastcgi_params;\n        }\n\n\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zend/nginx.conf",
    "content": "server {\n  listen      80;\n  server_name www.example.com;\n  root        /var/www/www.example.com/myapplication;\n  index       index.html index.htm index.php;\n\n  location / {\n    try_files $uri $uri/ /index.php$is_args$args;\n  }\n\n  location ~ \\.php$ {\n    fastcgi_pass   unix:/usr/local/zend/tmp/php-fastcgi.socket;\n    fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;\n    include        fastcgi_params;\n  }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zenphoto/nginx.conf",
    "content": "server {\n    server_name domain.tld;\n\n    root   /var/www/zenphoto;\n    index    index.php;\n\n    # pass the PHP scripts to php-fpm server\n    location ~ \\.php$ {\n        try_files $uri =404;\n        fastcgi_split_path_info ^(.+\\.php)(/.+)$;\n        include fastcgi_params;\n        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;\n        fastcgi_index index.php;\n        fastcgi_pass php;\n    }\n\n    location @zenphoto {\n\n        # experimental rss rules\n        rewrite index\\.php\\?^rss-(.*)&(.*)    /index.php?rss=$1 last;\n        rewrite index\\.php\\?^rss-(.*)$        /index.php?rss=$1 last;\n\n        rewrite index\\.php$ /index.php last;\n        rewrite ^/(.*)/page/([A-Za-z0-9_\\-]+)/?$ /index.php?album=$1&page=$2 last;\n\n        # Images and stuff\n        rewrite \"^/(.*)/image/(thumb|[0-9]{1,4})/([^/\\\\\\]+)$\"  /zp-core/i.php?a=$1&i=$3&s=$2  last;\n        rewrite ^/(.*)/image/([^/\\\\\\]+)$  /zp-core/i.php?a=$1&i=$2  last;\n        rewrite \"^/(.*)/album/(thumb|[0-9]{1,4})/([^/\\\\\\]+)$\"  /zp-core/i.php?a=$1&i=$3&s=$2&album=true  last;\n\n        # Catch all for unknown stuff\n        rewrite ^/(.*)/?$ /index.php?album=$1 last;\n    }\n\n    location @albums {\n         rewrite ^/albums/?(.+/?)?$  /$1  redirect;\n    }\n\n    # Admin pages\n    location /admin {\n        rewrite ^/admin/?$ /zp-core/admin.php redirect;\n    }\n\n    location /albums {\n        try_files $uri @albums;\n    }\n\n    # Tiny URLs\n    location /tiny {\n        rewrite ^/tiny/([0-9]+)/?$ /index.php?p=$1&t last;\n    }\n\n    # Page\n    location /page {\n        rewrite ^/page/([0-9]+)/?$ /index.php?page=$1 last;\n        rewrite ^/page/([A-Za-z0-9\\-_]+)/?$ /index.php?p=$1 last;\n        rewrite ^/page/([A-Za-z0-9_\\-]+)/([0-9]+)/?$ /index.php?p=$1&page=$2 last;\n    }\n\n    # Pages\n    location /pages {\n        rewrite ^/pages/?$ /index.php?p=pages last;\n        rewrite ^/pages/(.*)/?$ /index.php?p=pages&title=$1 last;\n    }\n\n    # Search\n    location /page/search {\n        rewrite ^/page/search/fields([0-9]+)/(.*)/([0-9]+)/?$ /index.php?p=search&searchfields=$1&words=$2&page=$3 last;\n        rewrite ^/page/search/fields([0-9]+)/(.*)/?$ /index.php?p=search&searchfields=$1&words=$2 last;\n        rewrite ^/page/search/archive/(.*)/([0-9]+)/?$ /index.php?p=search&date=$1&page=$2 last;\n        rewrite ^/page/search/archive/(.*)/?$ /index.php?p=search&date=$1 last;\n        rewrite ^/page/search/tags/(.*)/([0-9]+)/?$ /index.php?p=search&searchfields=tags&words=$1&page=$2 last;\n        rewrite ^/page/search/tags/(.*)/?$ /index.php?p=search&searchfields=tags&words=$1 last;\n        rewrite ^/page/search/(.*)/([0-9]+)/?$ /index.php?p=search&words=$1&page=$2 last;\n        rewrite ^/page/search/(.*)/?$ /index.php?p=search&words=$1 last;\n    }\n\n    # News\n    location /news {\n        rewrite ^/news/?$ /index.php?p=news last;\n        rewrite ^/news/([0-9]+)/?$ /index.php?p=news&page=$1 last;\n        rewrite ^/news/category/(.*)/([0-9]+)/?$ /index.php?p=news&category=$1&page=$2 last;\n        rewrite ^/news/category/(.*)/?$ /index.php?p=news&category=$1 last;\n        rewrite ^/news/archive/(.*)/([0-9]+)/?$ /index.php?p=news&date=$1&page=$2 last;\n        rewrite ^/news/archive/(.*)/?$ /index.php?p=news&date=$1 last;\n        rewrite ^/news/(.*)/?$ /index.php?p=news&title=$1 last;\n    }\n\n    # Root\n    location / {\n        try_files $uri $uri/ @zenphoto;\n    }\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/nginx-roundtrip-testdata/zope/nginx.conf",
    "content": "location ~ /zope(/|$) {\n  fastcgi_pass    unix:/var/run/plone-site.sock;\n  include /etc/nginx/fastcgi_params;\n\n  set $fixed_content_length $content_length;\n  if ( $fixed_content_length = \"\" ) {\n    set $fixed_content_length \"0\";\n  }\n\n  set $path_info \"\";\n  if ( $document_uri ~ \"^/zope/(.*)$\" ) {\n    set $path_info $1;\n  }\n\n  fastcgi_param   CONTENT_LENGTH  $fixed_content_length;\n  fastcgi_param   PATH_INFO   $path_info;\n  fastcgi_param   SCRIPT_NAME /zope;\n}\n"
  },
  {
    "path": "certbot-compatibility-test/nginx/roundtrip.py",
    "content": "#!/usr/bin/env python\n\nimport os\nimport sys\n\nfrom certbot_nginx._internal import nginxparser\n\n\ndef roundtrip(stuff):\n    success = True\n    for t in stuff:\n        print(t)\n        if not os.path.isfile(t):\n            continue\n        with open(t, \"r\") as f:\n            config = f.read()\n            try:\n                if nginxparser.dumps(nginxparser.loads(config)) != config:\n                    print(\"Failed parsing round-trip for {0}\".format(t))\n                    success = False\n            except Exception as e:\n                print(\"Failed parsing {0} ({1})\".format(t, e))\n                success = False\n    return success\n\nif __name__ == \"__main__\":\n    if len(sys.argv) != 2:\n        print(\"usage: %s directory\" % sys.argv[0])\n        sys.exit(1)\n    success = True\n    for where, _, files in os.walk(sys.argv[1]):\n        if files:\n            success &= roundtrip(os.path.join(where, f) for f in files)\n\n    sys.exit(0 if success else 1)\n"
  },
  {
    "path": "certbot-compatibility-test/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-compatibility-test\"\ndynamic = [\"version\"]\ndescription = \"Compatibility tests for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n]\ndependencies = [\n    \"certbot\",\n    \"certbot-apache\",\n    \"requests\",\n]\n\n[project.scripts]\ncertbot-compatibility-test = \"certbot_compatibility_test.test_driver:main\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-compatibility-test/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\nsetup(\n    version=version,\n)\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/__init__.py",
    "content": "\"\"\"Certbot compatibility test\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/__init__.py",
    "content": "\"\"\"Certbot compatibility test configurators\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/apache/__init__.py",
    "content": "\"\"\"Certbot compatibility test Apache configurators\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/apache/common.py",
    "content": "\"\"\"Provides a common base for Apache proxies\"\"\"\nimport argparse\nimport os\nimport shutil\nimport subprocess\nfrom unittest import mock\n\nfrom certbot import configuration\nfrom certbot import errors as le_errors\nfrom certbot import util as certbot_util\nfrom certbot_apache._internal import entrypoint\nfrom certbot_compatibility_test import errors\nfrom certbot_compatibility_test import util\nfrom certbot_compatibility_test.configurators import common as configurators_common\n\n\nclass Proxy(configurators_common.Proxy):\n    \"\"\"A common base for Apache test configurators\"\"\"\n\n    def __init__(self, args: argparse.Namespace) -> None:\n        \"\"\"Initializes the plugin with the given command line args\"\"\"\n        super().__init__(args)\n        self.le_config.apache_le_vhost_ext = \"-le-ssl.conf\"\n\n        self.modules = self.server_root = self.test_conf = self.version = None\n        patch = mock.patch(\n            \"certbot_apache._internal.configurator.display_ops.select_vhost\")\n        mock_display = patch.start()\n        mock_display.side_effect = le_errors.PluginError(\n            \"Unable to determine vhost\")\n\n    def load_config(self) -> str:\n        \"\"\"Loads the next configuration for the plugin to test\"\"\"\n        config = super().load_config()\n        self._all_names, self._test_names = _get_names(config)\n\n        server_root = _get_server_root(config)\n        shutil.rmtree(\"/etc/apache2\")\n        shutil.copytree(server_root, \"/etc/apache2\", symlinks=True)\n\n        self._prepare_configurator()\n\n        try:\n            subprocess.check_call(\"apachectl -k restart\".split())\n        except errors.Error:\n            raise errors.Error(\n                \"Apache failed to load {0} before tests started\".format(\n                    config))\n\n        return config\n\n    def _prepare_configurator(self) -> None:\n        \"\"\"Prepares the Apache plugin for testing\"\"\"\n        for k in entrypoint.ENTRYPOINT.OS_DEFAULTS.__dict__.keys():\n            setattr(self.le_config, \"apache_\" + k,\n                    getattr(entrypoint.ENTRYPOINT.OS_DEFAULTS, k))\n\n        self._configurator = entrypoint.ENTRYPOINT(\n            config=configuration.NamespaceConfig(self.le_config),\n            name=\"apache\")\n        self._configurator.prepare()\n\n    def cleanup_from_tests(self) -> None:\n        \"\"\"Performs any necessary cleanup from running plugin tests\"\"\"\n        super().cleanup_from_tests()\n        mock.patch.stopall()\n\n\ndef _get_server_root(config: str) -> str:\n    \"\"\"Returns the server root directory in config\"\"\"\n    subdirs = [\n        name for name in os.listdir(config)\n        if os.path.isdir(os.path.join(config, name))]\n\n    if len(subdirs) != 1:\n        raise errors.Error(\"Malformed configuration directory {0}\".format(config))\n\n    return os.path.join(config, subdirs[0].rstrip())\n\n\ndef _get_names(config: str) -> tuple[set[str], set[str]]:\n    \"\"\"Returns all and testable domain names in config\"\"\"\n    all_names = set()\n    non_ip_names = set()\n    with open(os.path.join(config, \"vhosts\")) as f:\n        for line in f:\n            # If parsing a specific vhost\n            if line[0].isspace():\n                words = line.split()\n                if words[0] == \"alias\":\n                    all_names.add(words[1])\n                    non_ip_names.add(words[1])\n                # If for port 80 and not IP vhost\n                elif words[1] == \"80\" and not util.IP_REGEX.match(words[3]):\n                    all_names.add(words[3])\n                    non_ip_names.add(words[3])\n            elif \"NameVirtualHost\" not in line:\n                words = line.split()\n                if (words[0].endswith(\"*\") or words[0].endswith(\"80\") and\n                        not util.IP_REGEX.match(words[1]) and\n                        words[1].find(\".\") != -1):\n                    all_names.add(words[1])\n    return (\n        certbot_util.get_filtered_names(all_names),\n        certbot_util.get_filtered_names(non_ip_names)\n    )\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/common.py",
    "content": "\"\"\"Provides a common base for configurator proxies\"\"\"\nfrom abc import abstractmethod\nimport argparse\nimport logging\nimport os\nimport shutil\nimport tempfile\nfrom typing import Iterable\nfrom typing import Optional\nfrom typing import overload\nfrom typing import Union\n\nfrom acme import challenges\nfrom acme.challenges import Challenge\nfrom certbot._internal import constants\nfrom certbot.achallenges import AnnotatedChallenge\nfrom certbot.plugins import common\nfrom certbot_compatibility_test import errors\nfrom certbot_compatibility_test import interfaces\nfrom certbot_compatibility_test import util\n\nlogger = logging.getLogger(__name__)\n\n\nclass Proxy(interfaces.ConfiguratorProxy):\n    \"\"\"A common base for compatibility test configurators\"\"\"\n\n    @classmethod\n    def add_parser_arguments(cls, parser: argparse.ArgumentParser) -> None:\n        \"\"\"Adds command line arguments needed by the plugin\"\"\"\n\n    def __init__(self, args: argparse.Namespace) -> None:\n        \"\"\"Initializes the plugin with the given command line args\"\"\"\n        super().__init__(args)\n        self._temp_dir = tempfile.mkdtemp()\n        # tempfile.mkdtemp() creates folders with too restrictive permissions to be accessible\n        # to an Apache worker, leading to HTTP challenge failures. Let's fix that.\n        os.chmod(self._temp_dir, 0o755)\n        self.le_config = util.create_le_config(self._temp_dir)\n        config_dir = util.extract_configs(args.configs, self._temp_dir)\n        self._configs = [\n            os.path.join(config_dir, config)\n            for config in os.listdir(config_dir)]\n\n        self.args = args\n        self.http_port = 80\n        self.https_port = 443\n        self._configurator: common.Configurator\n        self._all_names: Optional[set[str]] = None\n        self._test_names: Optional[set[str]] = None\n\n    def has_more_configs(self) -> bool:\n        \"\"\"Returns true if there are more configs to test\"\"\"\n        return bool(self._configs)\n\n    @abstractmethod\n    def cleanup_from_tests(self) -> None:\n        \"\"\"Performs any necessary cleanup from running plugin tests\"\"\"\n\n    def load_config(self) -> str:\n        \"\"\"Returns the next config directory to be tested\"\"\"\n        shutil.rmtree(self.le_config.work_dir, ignore_errors=True)\n        backup = os.path.join(self.le_config.work_dir, constants.BACKUP_DIR)\n        os.makedirs(backup)\n        return self._configs.pop()\n\n    @overload\n    def copy_certs_and_keys(self, cert_path: str, key_path: str,\n                            chain_path: str) -> tuple[str, str, str]: ...\n\n    @overload\n    def copy_certs_and_keys(self, cert_path: str, key_path: str,\n                            chain_path: Optional[str]) -> tuple[str, str, Optional[str]]: ...\n\n    def copy_certs_and_keys(self, cert_path: str, key_path: str,\n                            chain_path: Optional[str] = None) -> tuple[str, str, Optional[str]]:\n        \"\"\"Copies certs and keys into the temporary directory\"\"\"\n        cert_and_key_dir = os.path.join(self._temp_dir, \"certs_and_keys\")\n        if not os.path.isdir(cert_and_key_dir):\n            os.mkdir(cert_and_key_dir)\n\n        cert = os.path.join(cert_and_key_dir, \"cert\")\n        shutil.copy(cert_path, cert)\n        key = os.path.join(cert_and_key_dir, \"key\")\n        shutil.copy(key_path, key)\n        chain = None\n        if chain_path:\n            chain = os.path.join(cert_and_key_dir, \"chain\")\n            shutil.copy(chain_path, chain)\n\n        return cert, key, chain\n\n    def get_all_names_answer(self) -> set[str]:\n        \"\"\"Returns the set of domain names that the plugin should find\"\"\"\n        if self._all_names:\n            return self._all_names\n        raise errors.Error(\"No configuration file loaded\")\n\n    def get_testable_domain_names(self) -> set[str]:\n        \"\"\"Returns the set of domain names that can be tested against\"\"\"\n        if self._test_names:\n            return self._test_names\n        return {\"example.com\"}\n\n    def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str,\n                    fullchain_path: str) -> None:\n        \"\"\"Installs cert\"\"\"\n        cert_path, key_path, chain_path = self.copy_certs_and_keys(cert_path, key_path, chain_path)\n        if not self._configurator:\n            raise ValueError(\"Configurator plugin is not set.\")\n        self._configurator.deploy_cert(\n            domain, cert_path, key_path, chain_path, fullchain_path)\n\n    def cleanup(self, achalls: list[AnnotatedChallenge]) -> None:\n        self._configurator.cleanup(achalls)\n\n    def config_test(self) -> None:\n        self._configurator.config_test()\n\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[list[str], str]] = None) -> None:\n        self._configurator.enhance(domain, enhancement, options)\n\n    def get_all_names(self) -> Iterable[str]:\n        return self._configurator.get_all_names()\n\n    def get_chall_pref(self, identifier: str) -> Iterable[type[Challenge]]:\n        return self._configurator.get_chall_pref(identifier)\n\n    @classmethod\n    def inject_parser_options(cls, parser: argparse.ArgumentParser, name: str) -> None:\n        pass\n\n    def more_info(self) -> str:\n        return self._configurator.more_info()\n\n    def perform(self, achalls: list[AnnotatedChallenge]) -> list[challenges.ChallengeResponse]:\n        return self._configurator.perform(achalls)\n\n    def prepare(self) -> None:\n        self._configurator.prepare()\n\n    def recovery_routine(self) -> None:\n        self._configurator.recovery_routine()\n\n    def restart(self) -> None:\n        self._configurator.restart()\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        self._configurator.rollback_checkpoints(rollback)\n\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        self._configurator.save(title, temporary)\n\n    def supported_enhancements(self) -> list[str]:\n        return self._configurator.supported_enhancements()\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/nginx/__init__.py",
    "content": "\"\"\"Certbot compatibility test Nginx configurators\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/configurators/nginx/common.py",
    "content": "\"\"\"Provides a common base for Nginx proxies\"\"\"\nimport os\nimport shutil\nimport subprocess\n\nfrom certbot import configuration\nfrom certbot_compatibility_test import errors\nfrom certbot_compatibility_test import util\nfrom certbot_compatibility_test.configurators import common as configurators_common\nfrom certbot_nginx._internal import configurator\nfrom certbot_nginx._internal import constants\n\n\nclass Proxy(configurators_common.Proxy):\n    \"\"\"A common base for Nginx test configurators\"\"\"\n\n    def load_config(self) -> str:\n        \"\"\"Loads the next configuration for the plugin to test\"\"\"\n        config = super().load_config()\n        self._all_names, self._test_names = _get_names(config)\n\n        server_root = _get_server_root(config)\n\n        # XXX: Deleting all of this is kind of scary unless the test\n        #      instances really each have a complete configuration!\n        shutil.rmtree(\"/etc/nginx\")\n        shutil.copytree(server_root, \"/etc/nginx\", symlinks=True)\n\n        self._prepare_configurator()\n\n        try:\n            subprocess.check_call(\"service nginx reload\".split())\n        except errors.Error:\n            raise errors.Error(\n                \"Nginx failed to load {0} before tests started\".format(\n                    config))\n\n        return config\n\n    def _prepare_configurator(self) -> None:\n        \"\"\"Prepares the Nginx plugin for testing\"\"\"\n        for k in constants.CLI_DEFAULTS:\n            setattr(self.le_config, \"nginx_\" + k, constants.os_constant(k))\n\n        conf = configuration.NamespaceConfig(self.le_config)\n        self._configurator = configurator.NginxConfigurator(config=conf, name=\"nginx\")\n        self._configurator.prepare()\n\n    def cleanup_from_tests(self) -> None:\n        \"\"\"Performs any necessary cleanup from running plugin tests\"\"\"\n\n\ndef _get_server_root(config: str) -> str:\n    \"\"\"Returns the server root directory in config\"\"\"\n    subdirs = [\n        name for name in os.listdir(config)\n        if os.path.isdir(os.path.join(config, name))]\n\n    if len(subdirs) != 1:\n        raise errors.Error(\"Malformed configuration directory {0}\".format(config))\n\n    return os.path.join(config, subdirs[0].rstrip())\n\n\ndef _get_names(config: str) -> tuple[set[str], set[str]]:\n    \"\"\"Returns all and testable domain names in config\"\"\"\n    all_names: set[str] = set()\n    for root, _dirs, files in os.walk(config):\n        for this_file in files:\n            update_names = _get_server_names(root, this_file)\n            all_names.update(update_names)\n    non_ip_names = {n for n in all_names if not util.IP_REGEX.match(n)}\n    return all_names, non_ip_names\n\n\ndef _get_server_names(root: str, filename: str) -> set[str]:\n    \"\"\"Returns all names in a config file path\"\"\"\n    all_names = set()\n    with open(os.path.join(root, filename)) as f:\n        for line in f:\n            if line.strip().startswith(\"server_name\"):\n                names = line.partition(\"server_name\")[2].rpartition(\";\")[0]\n                for n in names.split():\n                    # Filter out wildcards in both all_names and test_names\n                    if not n.startswith(\"*.\"):\n                        all_names.add(n)\n    return all_names\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/errors.py",
    "content": "\"\"\"Certbot compatibility test errors\"\"\"\n\n\nclass Error(Exception):\n    \"\"\"Generic Certbot compatibility test error\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/interfaces.py",
    "content": "\"\"\"Certbot compatibility test interfaces\"\"\"\nfrom abc import ABCMeta\nfrom abc import abstractmethod\nimport argparse\nfrom typing import cast\n\nfrom certbot import interfaces\nfrom certbot.configuration import NamespaceConfig\n\n\nclass PluginProxy(interfaces.Plugin, metaclass=ABCMeta):\n    \"\"\"Wraps a Certbot plugin\"\"\"\n\n    http_port: int = NotImplemented\n    \"\"\"The port to connect to on localhost for HTTP traffic\"\"\"\n\n    https_port: int = NotImplemented\n    \"\"\"The port to connect to on localhost for HTTPS traffic\"\"\"\n\n    @classmethod\n    @abstractmethod\n    def add_parser_arguments(cls, parser: argparse.ArgumentParser) -> None:\n        \"\"\"Adds command line arguments needed by the parser\"\"\"\n\n    @abstractmethod\n    def __init__(self, args: argparse.Namespace) -> None:\n        \"\"\"Initializes the plugin with the given command line args\"\"\"\n        super().__init__(cast(NamespaceConfig, args), 'proxy')\n\n    @abstractmethod\n    def cleanup_from_tests(self) -> None:\n        \"\"\"Performs any necessary cleanup from running plugin tests.\n\n        This is guaranteed to be called before the program exits.\n\n        \"\"\"\n\n    @abstractmethod\n    def has_more_configs(self) -> bool:\n        \"\"\"Returns True if there are more configs to test\"\"\"\n\n    @abstractmethod\n    def load_config(self) -> str:\n        \"\"\"Loads the next config and returns its name\"\"\"\n\n    @abstractmethod\n    def get_testable_domain_names(self) -> set[str]:\n        \"\"\"Returns the domain names that can be used in testing\"\"\"\n\n\nclass AuthenticatorProxy(PluginProxy, interfaces.Authenticator, metaclass=ABCMeta):\n    \"\"\"Wraps a Certbot authenticator\"\"\"\n\n\nclass InstallerProxy(PluginProxy, interfaces.Installer, metaclass=ABCMeta):\n    \"\"\"Wraps a Certbot installer\"\"\"\n\n    @abstractmethod\n    def get_all_names_answer(self) -> set[str]:\n        \"\"\"Returns all names that should be found by the installer\"\"\"\n\n\nclass ConfiguratorProxy(AuthenticatorProxy, InstallerProxy, metaclass=ABCMeta):\n    \"\"\"Wraps a Certbot configurator\"\"\"\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/test_driver.py",
    "content": "\"\"\"Tests Certbot plugins against different server configurations.\"\"\"\nimport argparse\nimport contextlib\nimport filecmp\nimport logging\nimport os\nimport shutil\nimport socket\nimport sys\nimport tempfile\nimport time\nfrom typing import Any\nfrom typing import Generator\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom cryptography.hazmat.primitives import serialization\nfrom urllib3.util import connection\n\nfrom acme import challenges\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot import errors as le_errors\nfrom certbot._internal.display import obj as display_obj\nfrom certbot.tests import acme_util\nfrom certbot_compatibility_test import errors\nfrom certbot_compatibility_test import util\nfrom certbot_compatibility_test import validator\nfrom certbot_compatibility_test.configurators import common\nfrom certbot_compatibility_test.configurators.apache import common as a_common\nfrom certbot_compatibility_test.configurators.nginx import common as n_common\n\nDESCRIPTION = \"\"\"\nTests Certbot plugins against different server configurations. It is\nassumed that Docker is already installed. If no test type is specified, all\ntests that the plugin supports are performed.\n\n\"\"\"\n\nPLUGINS: dict[str, type[common.Proxy]] = {\"apache\": a_common.Proxy, \"nginx\": n_common.Proxy}\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef test_authenticator(plugin: common.Proxy, config: str, temp_dir: str) -> bool:\n    \"\"\"Tests authenticator, returning True if the tests are successful\"\"\"\n    backup = _create_backup(config, temp_dir)\n\n    achalls = _create_achalls(plugin)\n    if not achalls:\n        logger.error(\"The plugin and this program support no common \"\n                     \"challenge types\")\n        return False\n\n    try:\n        responses = plugin.perform(achalls)\n    except le_errors.Error:\n        logger.error(\"Performing challenges on %s caused an error:\", config, exc_info=True)\n        return False\n\n    success = True\n    for i, response in enumerate(responses):\n        achall = achalls[i]\n        if not response:\n            logger.error(\n                \"Plugin failed to complete %s for %s in %s\",\n                type(achall), achall.identifier.value, config)\n            success = False\n        elif isinstance(response, challenges.HTTP01Response):\n            # We fake the DNS resolution to ensure that any domain is resolved\n            # to the local HTTP server setup for the compatibility tests\n            with _fake_dns_resolution(\"127.0.0.1\"):\n                verified = response.simple_verify(\n                    achall.chall, achall.identifier.value,\n                    util.JWK.public_key(), port=plugin.http_port)\n            if verified:\n                logger.info(\n                    \"http-01 verification for %s succeeded\", achall.identifier.value)\n            else:\n                logger.error(\n                    \"**** http-01 verification for %s in %s failed\",\n                    achall.identifier.value, config)\n                success = False\n\n    if success:\n        try:\n            plugin.cleanup(achalls)\n        except le_errors.Error:\n            logger.error(\"Challenge cleanup for %s caused an error:\", config, exc_info=True)\n            success = False\n\n        if _dirs_are_unequal(config, backup):\n            logger.error(\"Challenge cleanup failed for %s\", config)\n            return False\n        logger.info(\"Challenge cleanup succeeded\")\n\n    return success\n\n\ndef _create_achalls(plugin: common.Proxy) -> list[achallenges.AnnotatedChallenge]:\n    \"\"\"Returns a list of annotated challenges to test on plugin\"\"\"\n    achalls: list[achallenges.AnnotatedChallenge] = []\n    names = plugin.get_testable_domain_names()\n    for domain in names:\n        prefs = plugin.get_chall_pref(domain)\n        for chall_type in prefs:\n            if chall_type == challenges.HTTP01:\n                # challenges.HTTP01.TOKEN_SIZE is a float but os.urandom\n                # expects an integer.\n                chall = challenges.HTTP01(\n                    token=os.urandom(int(challenges.HTTP01.TOKEN_SIZE)))\n                challb = acme_util.chall_to_challb(\n                    chall, messages.STATUS_PENDING)\n                achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n                    challb=challb, domain=domain, account_key=util.JWK)\n                achalls.append(achall)\n\n    return achalls\n\n\ndef test_installer(args: argparse.Namespace, plugin: common.Proxy, config: str,\n                   temp_dir: str) -> bool:\n    \"\"\"Tests plugin as an installer\"\"\"\n    backup = _create_backup(config, temp_dir)\n\n    names_match = plugin.get_all_names() == plugin.get_all_names_answer()\n    if names_match:\n        logger.info(\"get_all_names test succeeded\")\n    else:\n        logger.error(\"**** get_all_names test failed for config %s\", config)\n\n    domains = list(plugin.get_testable_domain_names())\n    success = test_deploy_cert(plugin, temp_dir, domains)\n\n    if success and args.enhance:\n        success = test_enhancements(plugin, domains)\n\n    good_rollback = test_rollback(plugin, config, backup)\n    return names_match and success and good_rollback\n\n\ndef test_deploy_cert(plugin: common.Proxy, temp_dir: str, domains: list[str]) -> bool:\n    \"\"\"Tests deploy_cert returning True if the tests are successful\"\"\"\n    cert = util.make_self_signed_cert(util.KEY, domains)\n    cert_path = os.path.join(temp_dir, \"cert.pem\")\n    with open(cert_path, \"wb\") as f:\n        f.write(cert.public_bytes(serialization.Encoding.PEM))\n\n    for domain in domains:\n        try:\n            plugin.deploy_cert(domain, cert_path, util.KEY_PATH, cert_path, cert_path)\n            plugin.save()  # Needed by the Apache plugin\n        except le_errors.Error:\n            logger.error(\"**** Plugin failed to deploy certificate for %s:\", domain, exc_info=True)\n            return False\n\n    if not _save_and_restart(plugin, \"deployed\"):\n        return False\n\n    success = True\n    time.sleep(3)\n    for domain in domains:\n        verified = validator.Validator().certificate(\n            cert, domain, \"127.0.0.1\", plugin.https_port)\n        if not verified:\n            logger.error(\"**** Could not verify certificate for domain %s\", domain)\n            success = False\n\n    if success:\n        logger.info(\"HTTPS validation succeeded\")\n\n    return success\n\n\ndef test_enhancements(plugin: common.Proxy, domains: Iterable[str]) -> bool:\n    \"\"\"Tests supported enhancements returning True if successful\"\"\"\n    supported = plugin.supported_enhancements()\n\n    if \"redirect\" not in supported:\n        logger.error(\"The plugin and this program support no common \"\n                     \"enhancements\")\n        return False\n\n    domains_and_info: list[tuple[str, list[bool]]] = [(domain, []) for domain in domains]\n\n    for domain, info in domains_and_info:\n        try:\n            previous_redirect = validator.Validator().any_redirect(\n                \"localhost\", plugin.http_port, headers={\"Host\": domain})\n            info.append(previous_redirect)\n            plugin.enhance(domain, \"redirect\")\n            plugin.save()  # Needed by the Apache plugin\n        except le_errors.PluginError as error:\n            # Don't immediately fail because a redirect may already be enabled\n            logger.warning(\"*** Plugin failed to enable redirect for %s:\", domain)\n            logger.warning(\"%s\", error)\n        except le_errors.Error:\n            logger.error(\"*** An error occurred while enabling redirect for %s:\",\n                         domain, exc_info=True)\n\n    if not _save_and_restart(plugin, \"enhanced\"):\n        return False\n\n    success = True\n    for domain, info in domains_and_info:\n        previous_redirect = info[0]\n        if not previous_redirect:\n            verified = validator.Validator().redirect(\n                \"localhost\", plugin.http_port, headers={\"Host\": domain})\n            if not verified:\n                logger.error(\"*** Improper redirect for domain %s\", domain)\n                success = False\n\n    if success:\n        logger.info(\"Enhancements test succeeded\")\n\n    return success\n\n\ndef _save_and_restart(plugin: common.Proxy, title: Optional[str] = None) -> bool:\n    \"\"\"Saves and restart the plugin, returning True if no errors occurred\"\"\"\n    try:\n        plugin.save(title)\n        plugin.restart()\n        return True\n    except le_errors.Error:\n        logger.error(\"*** Plugin failed to save and restart server:\", exc_info=True)\n        return False\n\n\ndef test_rollback(plugin: common.Proxy, config: str, backup: str) -> bool:\n    \"\"\"Tests the rollback checkpoints function\"\"\"\n    try:\n        plugin.rollback_checkpoints(1337)\n    except le_errors.Error:\n        logger.error(\"*** Plugin raised an exception during rollback:\", exc_info=True)\n        return False\n\n    if _dirs_are_unequal(config, backup):\n        logger.error(\"*** Rollback failed for config `%s`\", config)\n        return False\n    logger.info(\"Rollback succeeded\")\n    return True\n\n\ndef _create_backup(config: str, temp_dir: str) -> str:\n    \"\"\"Creates a backup of config in temp_dir\"\"\"\n    backup = os.path.join(temp_dir, \"backup\")\n    shutil.rmtree(backup, ignore_errors=True)\n    shutil.copytree(config, backup, symlinks=True)\n\n    return backup\n\n\ndef _dirs_are_unequal(dir1: str, dir2: str) -> bool:\n    \"\"\"Returns True if dir1 and dir2 are unequal\"\"\"\n    dircmps = [filecmp.dircmp(dir1, dir2)]\n    while dircmps:\n        dircmp = dircmps.pop()\n        if dircmp.left_only or dircmp.right_only:\n            logger.error(\"The following files and directories are only \"\n                         \"present in one directory\")\n            if dircmp.left_only:\n                logger.error(str(dircmp.left_only))\n            else:\n                logger.error(str(dircmp.right_only))\n            return True\n        elif dircmp.common_funny or dircmp.funny_files:\n            logger.error(\"The following files and directories could not be \"\n                         \"compared:\")\n            if dircmp.common_funny:\n                logger.error(str(dircmp.common_funny))\n            else:\n                logger.error(str(dircmp.funny_files))\n            return True\n        elif dircmp.diff_files:\n            logger.error(\"The following files differ:\")\n            logger.error(str(dircmp.diff_files))\n            return True\n\n        for subdir in dircmp.subdirs.values():\n            dircmps.append(subdir)\n\n    return False\n\n\ndef get_args() -> argparse.Namespace:\n    \"\"\"Returns parsed command line arguments.\"\"\"\n    parser = argparse.ArgumentParser(\n        description=DESCRIPTION,\n        formatter_class=argparse.ArgumentDefaultsHelpFormatter)\n\n    group = parser.add_argument_group(\"general\")\n    group.add_argument(\n        \"-c\", \"--configs\", default=\"configs.tar.gz\",\n        help=\"a directory or tarball containing server configurations\")\n    group.add_argument(\n        \"-p\", \"--plugin\", default=\"apache\", help=\"the plugin to be tested\")\n    group.add_argument(\n        \"-v\", \"--verbose\", dest=\"verbose_count\", action=\"count\",\n        default=0, help=\"you know how to use this\")\n    group.add_argument(\n        \"-a\", \"--auth\", action=\"store_true\",\n        help=\"tests the challenges the plugin supports\")\n    group.add_argument(\n        \"-i\", \"--install\", action=\"store_true\",\n        help=\"tests the plugin as an installer\")\n    group.add_argument(\n        \"-e\", \"--enhance\", action=\"store_true\", help=\"tests the enhancements \"\n        \"the plugin supports (implicitly includes installer tests)\")\n\n    for plugin in PLUGINS.values():\n        plugin.add_parser_arguments(parser)\n\n    args = parser.parse_args()\n    if args.enhance:\n        args.install = True\n    elif not (args.auth or args.install):\n        args.auth = args.install = args.enhance = True\n\n    return args\n\n\ndef setup_logging(args: argparse.Namespace) -> None:\n    \"\"\"Prepares logging for the program\"\"\"\n    handler = logging.StreamHandler()\n\n    root_logger = logging.getLogger()\n    root_logger.setLevel(logging.ERROR - args.verbose_count * 10)\n    root_logger.addHandler(handler)\n\n\ndef setup_display() -> None:\n    \"\"\"\"Prepares a display utility instance for the Certbot plugins \"\"\"\n    displayer = display_obj.NoninteractiveDisplay(sys.stdout)\n    display_obj.set_display(displayer)\n\n\ndef main() -> None:\n    \"\"\"Main test script execution.\"\"\"\n    args = get_args()\n    setup_logging(args)\n    setup_display()\n\n    if args.plugin not in PLUGINS:\n        raise errors.Error(\"Unknown plugin {0}\".format(args.plugin))\n\n    temp_dir = tempfile.mkdtemp()\n    plugin = PLUGINS[args.plugin](args)\n    try:\n        overall_success = True\n        while plugin.has_more_configs():\n            success = True\n\n            try:\n                config = plugin.load_config()\n                logger.info(\"Loaded configuration: %s\", config)\n                if args.auth:\n                    success = test_authenticator(plugin, config, temp_dir)\n                if success and args.install:\n                    success = test_installer(args, plugin, config, temp_dir)\n            except errors.Error:\n                logger.error(\"Tests on %s raised:\", config, exc_info=True)\n                success = False\n\n            if success:\n                logger.info(\"All tests on %s succeeded\", config)\n            else:\n                overall_success = False\n                logger.error(\"Tests on %s failed\", config)\n    finally:\n        plugin.cleanup_from_tests()\n\n    if overall_success:\n        logger.warning(\"All compatibility tests succeeded\")\n        sys.exit(0)\n    else:\n        logger.warning(\"One or more compatibility tests failed\")\n        sys.exit(1)\n\n\n@contextlib.contextmanager\ndef _fake_dns_resolution(resolved_ip: str) -> Generator[None, None, None]:\n    \"\"\"Monkey patch urllib3 to make any hostname be resolved to the provided IP\"\"\"\n    _original_create_connection = connection.create_connection\n\n    def _patched_create_connection(address: tuple[str, int],\n                                   *args: Any, **kwargs: Any) -> socket.socket:\n        _, port = address\n        return _original_create_connection((resolved_ip, port), *args, **kwargs)\n\n    try:\n        connection.create_connection = _patched_create_connection\n        yield\n    finally:\n        connection.create_connection = _original_create_connection\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/testdata/empty_cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICqDCCAZACCQCRC1UKg2WfRTANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtl\neGFtcGxlLmNvbTAeFw0yMDA4MTkyMzM5MjdaFw0yMDA5MTgyMzM5MjdaMBYxFDAS\nBgNVBAMMC2V4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEA5tViHnJx4y+BbCb8Qz9uxsnqp1ynONR7ET/XL+M/jQ4xPeJg4L2uZ3YnogPc\nWdEoey17WXBg3KRqKfg+7PqIdGqVeonSCfXhD1HoGJRsThSUJ2fK3uoQ+zGgJTWR\nFYWa8Cb6xsuq0xaYtw2jaJBp+697Np60PWs4pY5FkadT50wZ0TYDnYt3NSAdn+Pt\nj3cpI4ocZZ2FLiOFn+UFOaRcetGtpnU1QwvmygD9tiL7kJ55B4CWGEv6DMRQk/UE\neMUETzse1NkVlaxQ1TCd5iAfBTluiV30EpmmWa+OsXJWxCK+EEOkXD1r3CdXAldY\nnRYxJrn4udrFe69QX95wiRZNXwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCJvtDC\n875CK7SKNf006gSciXsNPNSVORGPjc/5OQ23baK4iPhxftI4LGZN8773N14jWp3E\nQnQLL1gZ9/G+98SlI5lm97a4m4XZyNaULbmQwRKgI22H0F1AWbvsG0SppjnhVlJ+\n93ZUqSQBXgbXelFHSsNfk1AB6Kvo6+UvS8s0vkz7SfkPOZGx0b+3RJSJZnZHvYih\nggudN/jJggSgRrb+F6lpaelJE9pZsznJFb9R7mFI33AGBpQWV4r3p1ZbM1vGMqGc\n4PGBzDzi28BhLBplSOPZZxqRiINQzGiQ5T2SfN06usr7EafFr6+7YKNhgrCdlVjU\nthzJ5MgHZgALNXsh\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/util.py",
    "content": "\"\"\"Utility functions for Certbot plugin tests.\"\"\"\nimport argparse\nimport copy\nfrom datetime import datetime, timedelta, timezone\nimport os\nimport re\nimport shutil\nimport tarfile\n\nfrom cryptography import x509\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import types\nimport josepy as jose\n\nfrom certbot._internal import constants\nfrom certbot.tests import util as test_util\nfrom certbot_compatibility_test import errors\n\n_KEY_BASE = \"rsa2048_key.pem\"\nKEY_PATH = test_util.vector_path(_KEY_BASE)\nKEY = test_util.load_rsa_private_key_pem(_KEY_BASE)\nJWK = jose.JWKRSA(key=test_util.load_jose_rsa_private_key_pem(_KEY_BASE))\nIP_REGEX = re.compile(r\"^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$\")\n\n\ndef create_le_config(parent_dir: str) -> argparse.Namespace:\n    \"\"\"Sets up LE dirs in parent_dir and returns the config dict\"\"\"\n    config = copy.deepcopy(constants.CLI_DEFAULTS)\n\n    le_dir = os.path.join(parent_dir, \"certbot\")\n    os.mkdir(le_dir)\n    for dir_name in (\"config\", \"logs\", \"work\"):\n        full_path = os.path.join(le_dir, dir_name)\n        os.mkdir(full_path)\n        full_name = dir_name + \"_dir\"\n        config[full_name] = full_path\n\n    config[\"domains\"] = None\n\n    return argparse.Namespace(**config)\n\n\ndef extract_configs(configs: str, parent_dir: str) -> str:\n    \"\"\"Extracts configs to a new dir under parent_dir and returns it\"\"\"\n    config_dir = os.path.join(parent_dir, \"configs\")\n\n    if os.path.isdir(configs):\n        shutil.copytree(configs, config_dir, symlinks=True)\n    elif tarfile.is_tarfile(configs):\n        with tarfile.open(configs, \"r\") as tar:\n            tar.extractall(config_dir)\n    else:\n        raise errors.Error(\"Unknown configurations file type\")\n\n    return config_dir\n\n\ndef make_self_signed_cert(private_key: types.CertificateIssuerPrivateKeyTypes,\n                          domains: list[str],\n                          ) -> x509.Certificate:\n    \"\"\"Generate new self-signed certificate.\n\n    :param buffer private_key_pem: Private key, in PEM PKCS#8 format.\n    :type domains: `list` of `str`\n    :param buffer private_key_pem: One of\n    `cryptography.hazmat.primitives.asymmetric.types.CertificateIssuerPrivateKeyTypes`\n\n    If more than one domain is provided, all of the domains are put into\n    ``subjectAltName`` X.509 extension and first domain is set as the\n    subject CN. If only one domain is provided no ``subjectAltName``\n    extension is used.\n\n    \"\"\"\n    assert domains, \"Must provide one or more hostnames for the cert.\"\n\n    builder = x509.CertificateBuilder()\n    builder = builder.serial_number(x509.random_serial_number())\n\n    builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)\n\n    name_attrs = [x509.NameAttribute(x509.OID_COMMON_NAME, domains[0])]\n\n    builder = builder.subject_name(x509.Name(name_attrs))\n    builder = builder.issuer_name(x509.Name(name_attrs))\n\n    sanlist: list[x509.GeneralName] = []\n    for address in domains:\n        sanlist.append(x509.DNSName(address))\n    if len(domains) > 1:\n        builder = builder.add_extension(\n            x509.SubjectAlternativeName(sanlist),\n            critical=False\n        )\n\n    not_before = datetime.now(tz=timezone.utc)\n    validity = timedelta(seconds=7 * 24 * 60 * 60)\n    builder = builder.not_valid_before(not_before)\n    builder = builder.not_valid_after(not_before + validity)\n\n    public_key = private_key.public_key()\n    builder = builder.public_key(public_key)\n    return builder.sign(private_key, hashes.SHA256())\n"
  },
  {
    "path": "certbot-compatibility-test/src/certbot_compatibility_test/validator.py",
    "content": "\"\"\"Validators to determine the current webserver configuration\"\"\"\nimport contextlib\nimport logging\nimport socket\nfrom typing import cast\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Union\n\nfrom cryptography import x509\nfrom OpenSSL import SSL\nimport requests\n\nfrom acme import errors as acme_errors\n\nlogger = logging.getLogger(__name__)\n\n\n_VALIDATION_TIMEOUT = 10\n\n\nclass Validator:\n    \"\"\"Collection of functions to test a live webserver's configuration\"\"\"\n\n    def certificate(self, cert: x509.Certificate, name: Union[str, bytes],\n                    alt_host: Optional[str] = None, port: int = 443) -> bool:\n        \"\"\"Verifies the certificate presented at name is cert\"\"\"\n        if alt_host is None:\n            # In fact, socket.gethostbyname accepts both bytes and str, but types do not know that.\n            host = socket.gethostbyname(cast(str, name)).encode()\n        elif isinstance(alt_host, bytes):\n            host = alt_host\n        else:\n            host = alt_host.encode()\n        name = name if isinstance(name, bytes) else name.encode()\n\n        try:\n            presented_cert = _probe_sni(name, host, port)\n        except acme_errors.Error as error:\n            logger.exception(str(error))\n            return False\n\n        return presented_cert == cert\n\n    def redirect(self, name: str, port: int = 80,\n                 headers: Optional[Mapping[str, str]] = None) -> bool:\n        \"\"\"Test whether webserver redirects to secure connection.\"\"\"\n        url = \"http://{0}:{1}\".format(name, port)\n        if headers:\n            response = requests.get(url, headers=headers,\n                                    allow_redirects=False,\n                                    timeout=_VALIDATION_TIMEOUT)\n        else:\n            response = requests.get(url, allow_redirects=False,\n                                    timeout=_VALIDATION_TIMEOUT)\n\n        redirect_location = response.headers.get(\"location\", \"\")\n        # We're checking that the redirect we added behaves correctly.\n        # It's okay for some server configuration to redirect to an\n        # http URL, as long as it's on some other domain.\n        if not redirect_location.startswith(\"https://\"):\n            return False\n\n        if response.status_code != 301:\n            logger.error(\"Server did not redirect with permanent code\")\n            return False\n\n        return True\n\n    def any_redirect(self, name: str, port: int = 80,\n                     headers: Optional[Mapping[str, str]] = None) -> bool:\n        \"\"\"Test whether webserver redirects.\"\"\"\n        url = \"http://{0}:{1}\".format(name, port)\n        if headers:\n            response = requests.get(url, headers=headers,\n                                    allow_redirects=False,\n                                    timeout=_VALIDATION_TIMEOUT)\n        else:\n            response = requests.get(url, allow_redirects=False,\n                                    timeout=_VALIDATION_TIMEOUT)\n\n        return response.status_code in range(300, 309)\n\n    def hsts(self, name: str) -> bool:\n        \"\"\"Test for HTTP Strict Transport Security header\"\"\"\n        headers = requests.get(\"https://\" + name,\n                               timeout=_VALIDATION_TIMEOUT).headers\n        hsts_header = headers.get(\"strict-transport-security\")\n\n        if not hsts_header:\n            return False\n\n        # Split directives following RFC6797, section 6.1\n        directives = [d.split(\"=\") for d in hsts_header.split(\";\")]\n        max_age = [d for d in directives if d[0] == \"max-age\"]\n\n        if not max_age:\n            logger.error(\"Server responded with invalid HSTS header field\")\n            return False\n\n        try:\n            max_age_value = int(max_age[0][1])\n        except ValueError:\n            logger.error(\"Server responded with invalid HSTS header field\")\n            return False\n\n        # Test whether HSTS does not expire for at least two weeks.\n        if max_age_value <= (2 * 7 * 24 * 3600):\n            logger.error(\"HSTS should not expire in less than two weeks\")\n            return False\n\n        return True\n\n    def ocsp_stapling(self, name: str) -> None:\n        \"\"\"Verify ocsp stapling for domain.\"\"\"\n        raise NotImplementedError()\n\n\n\ndef _probe_sni(name: bytes, host: bytes, port: int = 443) -> x509.Certificate:\n    \"\"\"Probe SNI server for SSL certificate.\n\n    :param bytes name: Byte string to send as the server name in the\n        client hello message.\n    :param bytes host: Host to connect to.\n    :param int port: Port to connect to.\n\n    :raises acme.errors.Error: In case of any problems.\n\n    :returns: SSL certificate presented by the server.\n    :rtype: cryptography.x509.Certificate\n\n    \"\"\"\n\n    # Default SSL method selected here is the most compatible, while secure\n    # SSL method: TLSv1_METHOD is only compatible with\n    # TLSv1_METHOD, while TLS_method is compatible with all other\n    # methods, including TLSv2_METHOD (read more at\n    # https://docs.openssl.org/master/man3/SSL_CTX_new/#notes). _serve_sni\n    # should be changed to use \"set_options\" to disable SSLv2 and SSLv3,\n    # in case it's used for things other than probing/serving!\n    context = SSL.Context(SSL.TLS_METHOD)\n    context.set_timeout(300) # timeout in seconds\n\n    # Enables multi-path probing (selection\n    # of source interface). See `socket.creation_connection` for more\n    # info. Available only in Python 2.7+.\n    source_address: tuple[str, int] = ('', 0)\n    socket_kwargs = {'source_address': source_address}\n\n    try:\n        logger.debug(\n            \"Attempting to connect to %s:%d%s.\", host, port,\n            \" from {0}:{1}\".format(\n                source_address[0],\n                source_address[1]\n            ) if any(source_address) else \"\"\n        )\n        socket_tuple: tuple[bytes, int] = (host, port)\n        sock = socket.create_connection(socket_tuple, **socket_kwargs)  # type: ignore[arg-type]\n    except OSError as error:\n        raise acme_errors.Error(error)\n\n    with contextlib.closing(sock) as client:\n        client_ssl = SSL.Connection(context, client)\n        client_ssl.set_connect_state()\n        client_ssl.set_tlsext_host_name(name)  # pyOpenSSL>=0.13\n        try:\n            client_ssl.do_handshake()\n            client_ssl.shutdown()\n        except SSL.Error as error:\n            raise acme_errors.Error(error)\n    cert = client_ssl.get_peer_certificate()\n    assert cert # Appease mypy. We would have crashed out by now if there was no certificate.\n    return cert.to_cryptography()\n"
  },
  {
    "path": "certbot-dns-cloudflare/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-cloudflare/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-cloudflare/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-cloudflare/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-cloudflare/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_cloudflare/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-cloudflare/README.rst",
    "content": "Cloudflare DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-cloudflare\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-cloudflare documentation build configuration file, created by\n# sphinx-quickstart on Tue May  9 10:20:04 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-cloudflare'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-cloudflaredoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-cloudflare.tex', u'certbot-dns-cloudflare Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-cloudflare', u'certbot-dns-cloudflare Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-cloudflare', u'certbot-dns-cloudflare Documentation',\n     author, 'certbot-dns-cloudflare', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/index.rst",
    "content": ".. certbot-dns-cloudflare documentation master file, created by\n   sphinx-quickstart on Tue May  9 10:20:04 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-cloudflare's documentation!\n==================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_cloudflare\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-cloudflare/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-cloudflare\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-cloudflare/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-cloudflare\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Cloudflare DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-cloudflare = \"certbot_dns_cloudflare._internal.dns_cloudflare:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-cloudflare/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-cloudflare[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-cloudflare[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-cloudflare[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-cloudflare[docs]\n"
  },
  {
    "path": "certbot-dns-cloudflare/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    # for now, do not upgrade to cloudflare>=2.20 to avoid deprecation warnings and the breaking\n    # changes in version 3.0. see https://github.com/certbot/certbot/issues/9938\n    'cloudflare>=2.19, <2.20',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the Cloudflare API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n========================================  =====================================\n``--dns-cloudflare-credentials``          Cloudflare credentials_ INI file.\n                                          (Required)\n``--dns-cloudflare-propagation-seconds``  The number of seconds to wait for DNS\n                                          to propagate before asking the ACME\n                                          server to verify the DNS record.\n                                          (Default: 10)\n========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing Cloudflare API\ncredentials, obtained from your\n`Cloudflare dashboard <https://dash.cloudflare.com/profile/api-tokens>`_.\n\nPreviously, Cloudflare's \"Global API Key\" was used for authentication, however\nthis key can access the entire Cloudflare API for all domains in your account,\nmeaning it could cause a lot of damage if leaked.\n\nCloudflare's newer API Tokens can be restricted to specific domains and\noperations, and are therefore now the recommended authentication option.\n\nThe Token needed by Certbot requires ``Zone:DNS:Edit`` permissions for only the\nzones you need certificates for.\n\nUsing Cloudflare Tokens also requires at least version 2.3.1 of the ``cloudflare``\nPython module. If the version that automatically installed with this plugin is\nolder than that, and you can't upgrade it on your system, you'll have to stick to\nthe Global key.\n\n.. code-block:: ini\n   :name: certbot_cloudflare_token.ini\n   :caption: Example credentials file using restricted API Token (recommended):\n\n   # Cloudflare API token used by Certbot\n   dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567\n\n.. code-block:: ini\n   :name: certbot_cloudflare_key.ini\n   :caption: Example credentials file using Global API Key (not recommended):\n\n   # Cloudflare API credentials used by Certbot\n   dns_cloudflare_email = cloudflare@example.com\n   dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234\n\nThe path to this file can be provided interactively or using the\n``--dns-cloudflare-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   Cloudflare account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n.. note::\n    Please note that the ``cloudflare`` Python module used by the plugin has\n    additional methods of providing credentials to the module, e.g. environment\n    variables or the ``cloudflare.cfg`` configuration file. These methods are not\n    supported by Certbot. If any of those additional methods of providing\n    credentials is being used, they must provide the same credentials (i.e.,\n    email and API key *or* an API token) as the credentials file provided to\n    Certbot. If there is a discrepancy, the ``cloudflare`` Python module will\n    raise an error. Also note that the credentials provided to Certbot will take\n    precedence over any other method of providing credentials to the ``cloudflare``\n    Python module.\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-cloudflare \\\\\n     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-cloudflare \\\\\n     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-cloudflare \\\\\n     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\\\\n     --dns-cloudflare-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_cloudflare.dns_cloudflare` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py",
    "content": "\"\"\"DNS Authenticator for Cloudflare.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\nfrom typing import cast\n\nimport CloudFlare\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common\nfrom certbot.plugins.dns_common import CredentialsConfiguration\n\nlogger = logging.getLogger(__name__)\n\nACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'\n\n\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for Cloudflare\n\n    This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = ('Obtain certificates using a DNS TXT record (if you are using Cloudflare for '\n                   'DNS).')\n    ttl = 120\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.credentials: Optional[CredentialsConfiguration] = None\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 10) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='Cloudflare credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Cloudflare API.'\n\n    def _validate_credentials(self, credentials: CredentialsConfiguration) -> None:\n        token = credentials.conf('api-token')\n        email = credentials.conf('email')\n        key = credentials.conf('api-key')\n        if token:\n            if email or key:\n                raise errors.PluginError('{}: dns_cloudflare_email and dns_cloudflare_api_key are '\n                                         'not needed when using an API Token'\n                                         .format(credentials.confobj.filename))\n        elif email or key:\n            if not email:\n                raise errors.PluginError('{}: dns_cloudflare_email is required when using a Global '\n                                         'API Key. (should be email address associated with '\n                                         'Cloudflare account)'.format(credentials.confobj.filename))\n            if not key:\n                raise errors.PluginError('{}: dns_cloudflare_api_key is required when using a '\n                                         'Global API Key. (see {})'\n                                         .format(credentials.confobj.filename, ACCOUNT_URL))\n        else:\n            raise errors.PluginError('{}: Either dns_cloudflare_api_token (recommended), or '\n                                     'dns_cloudflare_email and dns_cloudflare_api_key are required.'\n                                     ' (see {})'.format(credentials.confobj.filename, ACCOUNT_URL))\n\n    def _setup_credentials(self) -> None:\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'Cloudflare credentials INI file',\n            None,\n            self._validate_credentials\n        )\n\n    def _perform(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_cloudflare_client().add_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_cloudflare_client().del_txt_record(domain, validation_name, validation)\n\n    def _get_cloudflare_client(self) -> \"_CloudflareClient\":\n        if not self.credentials:  # pragma: no cover\n            raise errors.Error(\"Plugin has not been prepared.\")\n        if self.credentials.conf('api-token'):\n            return _CloudflareClient(api_token = self.credentials.conf('api-token'))\n        return _CloudflareClient(email = self.credentials.conf('email'),\n                                 api_key = self.credentials.conf('api-key'))\n\n\nclass _CloudflareClient:\n    \"\"\"\n    Encapsulates all communication with the Cloudflare API.\n    \"\"\"\n\n    def __init__(self, email: Optional[str] = None, api_key: Optional[str] = None,\n                 api_token: Optional[str] = None) -> None:\n        if email:\n            # If an email was specified, we're using an email/key combination and not a token.\n            # We can't use named arguments in this case, as it would break compatibility with\n            # the Cloudflare library since version 2.10.1, as the `token` argument was used for\n            # tokens and keys alike and the `key` argument did not exist in earlier versions.\n            self.cf = CloudFlare.CloudFlare(email, api_key)\n        else:\n            # If no email was specified, we're using just a token. Let's use the named argument\n            # for simplicity, which is compatible with all (current) versions of the Cloudflare\n            # library.\n            self.cf = CloudFlare.CloudFlare(token=api_token)\n\n    def add_txt_record(self, domain: str, record_name: str, record_content: str,\n                       record_ttl: int) -> None:\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the Cloudflare zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Cloudflare API\n        \"\"\"\n\n        zone_id = self._find_zone_id(domain)\n\n        data = {'type': 'TXT',\n                'name': record_name,\n                'content': record_content,\n                'ttl': record_ttl}\n\n        try:\n            logger.debug('Attempting to add record to zone %s: %s', zone_id, data)\n            self.cf.zones.dns_records.post(zone_id, data=data)  # zones | pylint: disable=no-member\n        except CloudFlare.exceptions.CloudFlareAPIError as e:\n            code = int(e)\n            hint = None\n\n            if code == 1009:\n                hint = 'Does your API token have \"Zone:DNS:Edit\" permissions?'\n\n            logger.error('Encountered CloudFlareAPIError adding TXT record: %d %s', e, e)\n            raise errors.PluginError('Error communicating with the Cloudflare API: {0}{1}'\n                                     .format(e, ' ({0})'.format(hint) if hint else ''))\n\n        record_id = self._find_txt_record_id(zone_id, record_name, record_content)\n        logger.debug('Successfully added TXT record with record_id: %s', record_id)\n\n    def del_txt_record(self, domain: str, record_name: str, record_content: str) -> None:\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        Note that both the record's name and content are used to ensure that similar records\n        created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.\n\n        Failures are logged, but not raised.\n\n        :param str domain: The domain to use to look up the Cloudflare zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        \"\"\"\n\n        try:\n            zone_id = self._find_zone_id(domain)\n        except errors.PluginError as e:\n            logger.debug('Encountered error finding zone_id during deletion: %s', e)\n            return\n\n        if zone_id:\n            record_id = self._find_txt_record_id(zone_id, record_name, record_content)\n            if record_id:\n                try:\n                    # zones | pylint: disable=no-member\n                    self.cf.zones.dns_records.delete(zone_id, record_id)\n                    logger.debug('Successfully deleted TXT record.')\n                except CloudFlare.exceptions.CloudFlareAPIError as e:\n                    logger.warning('Encountered CloudFlareAPIError deleting TXT record: %s', e)\n            else:\n                logger.debug('TXT record not found; no cleanup needed.')\n        else:\n            logger.debug('Zone not found; no cleanup needed.')\n\n    def _find_zone_id(self, domain: str) -> str:\n        \"\"\"\n        Find the zone_id for a given domain.\n\n        :param str domain: The domain for which to find the zone_id.\n        :returns: The zone_id, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if no zone_id is found.\n        \"\"\"\n\n        zone_name_guesses = dns_common.base_domain_name_guesses(domain)\n        zones: list[dict[str, Any]] = []\n        code = msg = None\n\n        for zone_name in zone_name_guesses:\n            params = {'name': zone_name,\n                      'per_page': 1}\n\n            try:\n                zones = self.cf.zones.get(params=params)  # zones | pylint: disable=no-member\n            except CloudFlare.exceptions.CloudFlareAPIError as e:\n                code = int(e)\n                msg = str(e)\n                hint = None\n\n                if code == 6003:\n                    hint = ('Did you copy your entire API token/key? To use Cloudflare tokens, '\n                            'you\\'ll need the python package cloudflare>=2.3.1.{}'\n                    .format(' This certbot is running cloudflare ' + str(CloudFlare.__version__)\n                    if hasattr(CloudFlare, '__version__') else ''))\n                elif code == 9103:\n                    hint = 'Did you enter the correct email address and Global key?'\n                elif code == 9109:\n                    hint = 'Did you enter a valid Cloudflare Token?'\n\n                if hint:\n                    raise errors.PluginError('Error determining zone_id: {0} {1}. Please confirm '\n                                  'that you have supplied valid Cloudflare API credentials. ({2})'\n                                                                         .format(code, msg, hint))\n                else:\n                    logger.debug('Unrecognised CloudFlareAPIError while finding zone_id: %d %s. '\n                                 'Continuing with next zone guess...', e, e)\n\n            if zones:\n                zone_id: str = zones[0]['id']\n                logger.debug('Found zone_id of %s for %s using name %s', zone_id, domain, zone_name)\n                return zone_id\n\n        if msg is not None:\n            if 'com.cloudflare.api.account.zone.list' in msg:\n                raise errors.PluginError('Unable to determine zone_id for {0} using zone names: '\n                                         '{1}. Please confirm that the domain name has been '\n                                         'entered correctly and your Cloudflare Token has access '\n                                         'to the domain.'.format(domain, zone_name_guesses))\n            else:\n                raise errors.PluginError('Unable to determine zone_id for {0} using zone names: '\n                                         '{1}. The error from Cloudflare was: {2} {3}.'\n                                         .format(domain, zone_name_guesses, code, msg))\n        else:\n            raise errors.PluginError('Unable to determine zone_id for {0} using zone names: '\n                                     '{1}. Please confirm that the domain name has been '\n                                     'entered correctly and is already associated with the '\n                                     'supplied Cloudflare account.'\n                                     .format(domain, zone_name_guesses))\n\n    def _find_txt_record_id(self, zone_id: str, record_name: str,\n                            record_content: str) -> Optional[str]:\n        \"\"\"\n        Find the record_id for a TXT record with the given name and content.\n\n        :param str zone_id: The zone_id which contains the record.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :returns: The record_id, if found.\n        :rtype: str\n        \"\"\"\n\n        params = {'type': 'TXT',\n                  'name': record_name,\n                  'content': record_content,\n                  'per_page': 1}\n        try:\n            # zones | pylint: disable=no-member\n            records = self.cf.zones.dns_records.get(zone_id, params=params)\n        except CloudFlare.exceptions.CloudFlareAPIError as e:\n            logger.debug('Encountered CloudFlareAPIError getting TXT record_id: %s', e)\n            records = []\n\n        if records:\n            # Cleanup is returning the system to the state we found it. If, for some reason,\n            # there are multiple matching records, we only delete one because we only added one.\n            return cast(str, records[0]['id'])\n        logger.debug('Unable to find TXT record.')\n        return None\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-cloudflare tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py",
    "content": "\"\"\"Tests for certbot_dns_cloudflare._internal.dns_cloudflare.\"\"\"\n\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport CloudFlare\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')\n\nAPI_TOKEN = 'an-api-token'\n\nAPI_KEY = 'an-api-key'\nEMAIL = 'example@example.com'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):\n\n    def setUp(self):\n        from certbot_dns_cloudflare._internal.dns_cloudflare import Authenticator\n\n        super().setUp()\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"cloudflare_email\": EMAIL, \"cloudflare_api_key\": API_KEY}, path)\n\n        self.config = mock.MagicMock(cloudflare_credentials=path,\n                                     cloudflare_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"cloudflare\")\n\n        self.mock_client = mock.MagicMock()\n        # _get_cloudflare_client | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_cloudflare_client', mock.MagicMock(return_value=self.mock_client))\n\n    @test_util.patch_display_util()\n    def test_perform(self, unused_mock_get_utility):\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_cleanup(self):\n        # _attempt_cleanup | pylint: disable=protected-access\n        self.auth._attempt_cleanup = True\n        self.auth.cleanup([self.achall])\n\n        expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    @test_util.patch_display_util()\n    def test_api_token(self, unused_mock_get_utility):\n        dns_test_common.write({\"cloudflare_api_token\": API_TOKEN},\n                              self.config.cloudflare_credentials)\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_no_creds(self):\n        dns_test_common.write({}, self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    def test_missing_email_or_key(self):\n        dns_test_common.write({\"cloudflare_api_key\": API_KEY}, self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n        dns_test_common.write({\"cloudflare_email\": EMAIL}, self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    def test_email_or_key_with_token(self):\n        dns_test_common.write({\"cloudflare_api_token\": API_TOKEN, \"cloudflare_email\": EMAIL},\n                              self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n        dns_test_common.write({\"cloudflare_api_token\": API_TOKEN, \"cloudflare_api_key\": API_KEY},\n                              self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n        dns_test_common.write({\"cloudflare_api_token\": API_TOKEN, \"cloudflare_email\": EMAIL,\n                               \"cloudflare_api_key\": API_KEY}, self.config.cloudflare_credentials)\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n\nclass CloudflareClientTest(unittest.TestCase):\n    record_name = \"foo\"\n    record_content = \"bar\"\n    record_ttl = 42\n    zone_id = 1\n    record_id = 2\n\n    def setUp(self):\n        from certbot_dns_cloudflare._internal.dns_cloudflare import _CloudflareClient\n\n        self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)\n\n        self.cf = mock.MagicMock()\n        self.cloudflare_client.cf = self.cf\n\n    def test_add_txt_record(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n\n        self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content,\n                                              self.record_ttl)\n\n        self.cf.zones.dns_records.post.assert_called_with(self.zone_id, data=mock.ANY)\n\n        post_data = self.cf.zones.dns_records.post.call_args[1]['data']\n\n        assert 'TXT' == post_data['type']\n        assert self.record_name == post_data['name']\n        assert self.record_content == post_data['content']\n        assert self.record_ttl == post_data['ttl']\n\n    def test_add_txt_record_error(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n\n        self.cf.zones.dns_records.post.side_effect = CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')\n\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_add_txt_record_error_during_zone_lookup(self):\n        self.cf.zones.get.side_effect = API_ERROR\n\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_add_txt_record_zone_not_found(self):\n        self.cf.zones.get.return_value = []\n\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_add_txt_record_bad_creds(self):\n        self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(6003, '', '')\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9103, '', '')\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(9109, '', '')\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        self.cf.zones.get.side_effect = CloudFlare.exceptions.CloudFlareAPIError(0, 'com.cloudflare.api.account.zone.list', '')\n        with pytest.raises(errors.PluginError):\n            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_del_txt_record(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n        expected = [mock.call.zones.get(params=mock.ANY),\n                    mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY),\n                    mock.call.zones.dns_records.delete(self.zone_id, self.record_id)]\n\n        assert expected == self.cf.mock_calls\n\n        get_data = self.cf.zones.dns_records.get.call_args[1]['params']\n\n        assert 'TXT' == get_data['type']\n        assert self.record_name == get_data['name']\n        assert self.record_content == get_data['content']\n\n    def test_del_txt_record_error_during_zone_lookup(self):\n        self.cf.zones.get.side_effect = API_ERROR\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_error_during_delete(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]\n        self.cf.zones.dns_records.delete.side_effect = API_ERROR\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n        expected = [mock.call.zones.get(params=mock.ANY),\n                    mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY),\n                    mock.call.zones.dns_records.delete(self.zone_id, self.record_id)]\n\n        assert expected == self.cf.mock_calls\n\n    def test_del_txt_record_error_during_get(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n        self.cf.zones.dns_records.get.side_effect = API_ERROR\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n        expected = [mock.call.zones.get(params=mock.ANY),\n                    mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)]\n\n        assert expected == self.cf.mock_calls\n\n    def test_del_txt_record_no_record(self):\n        self.cf.zones.get.return_value = [{'id': self.zone_id}]\n        self.cf.zones.dns_records.get.return_value = []\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n        expected = [mock.call.zones.get(params=mock.ANY),\n                    mock.call.zones.dns_records.get(self.zone_id, params=mock.ANY)]\n\n        assert expected == self.cf.mock_calls\n\n    def test_del_txt_record_no_zone(self):\n        self.cf.zones.get.return_value = [{'id': None}]\n\n        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n        expected = [mock.call.zones.get(params=mock.ANY)]\n\n        assert expected == self.cf.mock_calls\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-cloudflare/src/certbot_dns_cloudflare/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-digitalocean/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-digitalocean/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-digitalocean/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-digitalocean/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-digitalocean/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_digitalocean/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-digitalocean/README.rst",
    "content": "DigitalOcean DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-digitalocean/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-digitalocean/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-digitalocean\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-digitalocean/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-digitalocean/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-digitalocean documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 10:52:06 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-digitalocean'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-digitaloceandoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-digitalocean.tex', u'certbot-dns-digitalocean Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-digitalocean', u'certbot-dns-digitalocean Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-digitalocean', u'certbot-dns-digitalocean Documentation',\n     author, 'certbot-dns-digitalocean', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-digitalocean/docs/index.rst",
    "content": ".. certbot-dns-digitalocean documentation master file, created by\n   sphinx-quickstart on Wed May 10 10:52:06 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-digitalocean's documentation!\n====================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_digitalocean\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-digitalocean/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-digitalocean\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-digitalocean/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-digitalocean\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"DigitalOcean DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-digitalocean = \"certbot_dns_digitalocean._internal.dns_digitalocean:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-digitalocean/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-digitalocean[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-digitalocean[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-digitalocean[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-digitalocean[docs]\n"
  },
  {
    "path": "certbot-dns-digitalocean/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'python-digitalocean>=1.15.0', # 1.15.0 or newer is recommended for TTL support\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_digitalocean.dns_digitalocean` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the DigitalOcean API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n==========================================  ===================================\n``--dns-digitalocean-credentials``          DigitalOcean credentials_ INI file.\n                                            (Required)\n``--dns-digitalocean-propagation-seconds``  The number of seconds to wait for\n                                            DNS to propagate before asking the\n                                            ACME server to verify the DNS\n                                            record.\n                                            (Default: 10)\n==========================================  ===================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing DigitalOcean API\ncredentials, obtained from your DigitalOcean account's `Applications & API\nTokens page <https://cloud.digitalocean.com/settings/api/tokens>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # DigitalOcean API credentials used by Certbot\n   dns_digitalocean_token = 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff\n\nThe path to this file can be provided interactively or using the\n``--dns-digitalocean-credentials`` command-line argument. Certbot records the\npath to this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   DigitalOcean account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-digitalocean \\\\\n     --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-digitalocean \\\\\n     --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a wildcard certificate for ``*.example.com``\n\n   certbot certonly \\\\\n     --dns-digitalocean \\\\\n     --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\\\\n     -d example.com \\\\\n     -d '*.example.com'\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-digitalocean \\\\\n     --dns-digitalocean-credentials ~/.secrets/certbot/digitalocean.ini \\\\\n     --dns-digitalocean-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_digitalocean.dns_digitalocean` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/_internal/dns_digitalocean.py",
    "content": "\"\"\"DNS Authenticator for DigitalOcean.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Optional\n\nimport digitalocean\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common\nfrom certbot.plugins.dns_common import CredentialsConfiguration\n\nlogger = logging.getLogger(__name__)\n\n\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for DigitalOcean\n\n    This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are ' + \\\n                  'using DigitalOcean for DNS).'\n    ttl = 30\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.credentials: Optional[CredentialsConfiguration] = None\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 10) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='DigitalOcean credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the DigitalOcean API.'\n\n    def _setup_credentials(self) -> None:\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'DigitalOcean credentials INI file',\n            {\n                'token': 'API token for DigitalOcean account'\n            }\n        )\n\n    def _perform(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation,\n                                                       self.ttl)\n\n    def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)\n\n    def _get_digitalocean_client(self) -> \"_DigitalOceanClient\":\n        if not self.credentials:  # pragma: no cover\n            raise errors.Error(\"Plugin has not been prepared.\")\n        return _DigitalOceanClient(cast(str, self.credentials.conf('token')))\n\n\nclass _DigitalOceanClient:\n    \"\"\"\n    Encapsulates all communication with the DigitalOcean API.\n    \"\"\"\n\n    def __init__(self, token: str) -> None:\n        self.manager = digitalocean.Manager(token=token)\n\n    def add_txt_record(self, domain_name: str, record_name: str, record_content: str,\n                       record_ttl: int) -> None:\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL.\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean\n                                            API\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n            # The TTL value is set to the SOA record's TTL. Unless set to a falsy value,\n            # the optional TTL argument to add_txt_record() would be ignored.\n            # See https://github.com/certbot/certbot/pull/9149 for details.\n            domain.ttl = None\n        except digitalocean.Error as e:\n            hint = None\n\n            if str(e).startswith(\"Unable to authenticate\"):\n                hint = 'Did you provide a valid API token?'\n\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error finding domain using the DigitalOcean API: {0}{1}'\n                                     .format(e, ' ({0})'.format(hint) if hint else ''))\n\n        try:\n            result = domain.create_new_domain_record(\n                type='TXT',\n                name=self._compute_record_name(domain, record_name),\n                data=record_content,\n                ttl=record_ttl) # ttl kwarg is only effective starting python-digitalocean 1.15.0\n\n            record_id = result['domain_record']['id']\n\n            logger.debug('Successfully added TXT record with id: %d', record_id)\n        except digitalocean.Error as e:\n            logger.debug('Error adding TXT record using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain_name: str, record_name: str, record_content: str) -> None:\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        Note that both the record's name and content are used to ensure that similar records\n        created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.\n\n        Failures are logged, but not raised.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n        except digitalocean.Error as e:\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            return\n\n        try:\n            domain_records = domain.get_records()\n\n            matching_records = [record for record in domain_records\n                                if record.type == 'TXT'\n                                and record.name == self._compute_record_name(domain, record_name)\n                                and record.data == record_content]\n        except digitalocean.Error as e:\n            logger.debug('Error getting DNS records using the DigitalOcean API: %s', e)\n            return\n\n        for record in matching_records:\n            try:\n                logger.debug('Removing TXT record with id: %s', record.id)\n                record.destroy()\n            except digitalocean.Error as e:\n                logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s',\n                            record.id, e)\n\n    def _find_domain(self, domain_name: str) -> digitalocean.Domain:\n        \"\"\"\n        Find the domain object for a given domain name.\n\n        :param str domain_name: The domain name for which to find the corresponding Domain.\n        :returns: The Domain, if found.\n        :rtype: `~digitalocean.Domain`\n        :raises certbot.errors.PluginError: if no matching Domain is found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)\n\n        domains = self.manager.get_all_domains()\n\n        for guess in domain_name_guesses:\n            matches = [domain for domain in domains if domain.name == guess]\n\n            if matches:\n                domain = matches[0]\n                logger.debug('Found base domain for %s using name %s', domain_name, guess)\n                return domain\n\n        raise errors.PluginError(f'Unable to determine base domain for {domain_name} using names: '\n                                 f'{domain_name_guesses}.')\n\n    @staticmethod\n    def _compute_record_name(domain: digitalocean.Domain, full_record_name: str) -> str:\n        # The domain, from DigitalOcean's point of view, is automatically appended.\n        return full_record_name.rpartition(\".\" + domain.name)[0]\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-digitalocean tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/_internal/tests/dns_digitalocean_test.py",
    "content": "\"\"\"Tests for certbot_dns_digitalocean._internal.dns_digitalocean.\"\"\"\n\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport digitalocean\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_ERROR = digitalocean.DataReadError()\nTOKEN = 'a-token'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):\n\n    def setUp(self):\n        from certbot_dns_digitalocean._internal.dns_digitalocean import Authenticator\n\n        super().setUp()\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"digitalocean_token\": TOKEN}, path)\n\n        self.config = mock.MagicMock(digitalocean_credentials=path,\n                                     digitalocean_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"digitalocean\")\n\n        self.mock_client = mock.MagicMock()\n        # _get_digitalocean_client | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_digitalocean_client',\n            mock.MagicMock(return_value=self.mock_client))\n\n    @test_util.patch_display_util()\n    def test_perform(self, unused_mock_get_utility):\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, 30)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_cleanup(self):\n        # _attempt_cleanup | pylint: disable=protected-access\n        self.auth._attempt_cleanup = True\n        self.auth.cleanup([self.achall])\n\n        expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n\nclass DigitalOceanClientTest(unittest.TestCase):\n\n    id_num = 1\n    record_prefix = \"_acme-challenge\"\n    record_name = record_prefix + \".\" + DOMAIN\n    record_content = \"bar\"\n    record_ttl = 60\n\n    def setUp(self):\n        from certbot_dns_digitalocean._internal.dns_digitalocean import _DigitalOceanClient\n\n        self.digitalocean_client = _DigitalOceanClient(TOKEN)\n\n        self.manager = mock.MagicMock()\n        self.digitalocean_client.manager = self.manager\n\n    def test_add_txt_record(self):\n        wrong_domain_mock = mock.MagicMock()\n        wrong_domain_mock.name = \"other.invalid\"\n        wrong_domain_mock.create_new_domain_record.side_effect = AssertionError('Wrong Domain')\n\n        domain_mock = mock.MagicMock()\n        domain_mock.name = DOMAIN\n        domain_mock.create_new_domain_record.return_value = {'domain_record': {'id': self.id_num}}\n\n        self.manager.get_all_domains.return_value = [wrong_domain_mock, domain_mock]\n\n        self.digitalocean_client.add_txt_record(DOMAIN, self.record_name, self.record_content,\n                                                self.record_ttl)\n\n        domain_mock.create_new_domain_record.assert_called_with(type='TXT',\n                                                                name=self.record_prefix,\n                                                                data=self.record_content,\n                                                                ttl=self.record_ttl)\n\n    def test_add_txt_record_fail_to_find_domain(self):\n        self.manager.get_all_domains.return_value = []\n\n        with pytest.raises(errors.PluginError):\n            self.digitalocean_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_add_txt_record_error_finding_domain(self):\n        self.manager.get_all_domains.side_effect = API_ERROR\n\n        with pytest.raises(errors.PluginError):\n            self.digitalocean_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_add_txt_record_error_creating_record(self):\n        domain_mock = mock.MagicMock()\n        domain_mock.name = DOMAIN\n        domain_mock.create_new_domain_record.side_effect = API_ERROR\n\n        self.manager.get_all_domains.return_value = [domain_mock]\n\n        with pytest.raises(errors.PluginError):\n            self.digitalocean_client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    def test_del_txt_record(self):\n        first_record_mock = mock.MagicMock()\n        first_record_mock.type = 'TXT'\n        first_record_mock.name = \"DIFFERENT\"\n        first_record_mock.data = self.record_content\n\n        correct_record_mock = mock.MagicMock()\n        correct_record_mock.type = 'TXT'\n        correct_record_mock.name = self.record_prefix\n        correct_record_mock.data = self.record_content\n\n        last_record_mock = mock.MagicMock()\n        last_record_mock.type = 'TXT'\n        last_record_mock.name = self.record_prefix\n        last_record_mock.data = \"DIFFERENT\"\n\n        domain_mock = mock.MagicMock()\n        domain_mock.name = DOMAIN\n        domain_mock.get_records.return_value = [first_record_mock,\n                                                correct_record_mock,\n                                                last_record_mock]\n\n        self.manager.get_all_domains.return_value = [domain_mock]\n\n        self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n        assert correct_record_mock.destroy.called\n\n        assert not first_record_mock.destroy.call_args_list\n        assert not last_record_mock.destroy.call_args_list\n\n    def test_del_txt_record_error_finding_domain(self):\n        self.manager.get_all_domains.side_effect = API_ERROR\n\n        self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_error_finding_record(self):\n        domain_mock = mock.MagicMock()\n        domain_mock.name = DOMAIN\n        domain_mock.get_records.side_effect = API_ERROR\n\n        self.manager.get_all_domains.return_value = [domain_mock]\n\n        self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n    def test_del_txt_record_error_deleting_record(self):\n        record_mock = mock.MagicMock()\n        record_mock.type = 'TXT'\n        record_mock.name = self.record_prefix\n        record_mock.data = self.record_content\n        record_mock.destroy.side_effect = API_ERROR\n\n        domain_mock = mock.MagicMock()\n        domain_mock.name = DOMAIN\n        domain_mock.get_records.return_value = [record_mock]\n\n        self.manager.get_all_domains.return_value = [domain_mock]\n\n        self.digitalocean_client.del_txt_record(DOMAIN, self.record_name, self.record_content)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-digitalocean/src/certbot_dns_digitalocean/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-dnsimple/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-dnsimple/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-dnsimple/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-dnsimple/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-dnsimple/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_dnsimple/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-dnsimple/README.rst",
    "content": "DNSimple DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-dnsimple/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-dnsimple/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-dnsimple\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-dnsimple/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-dnsimple/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-dnsimple documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:23:41 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-dnsimple'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-dnsimpledoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-dnsimple.tex', u'certbot-dns-dnsimple Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-dnsimple', u'certbot-dns-dnsimple Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-dnsimple', u'certbot-dns-dnsimple Documentation',\n     author, 'certbot-dns-dnsimple', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-dnsimple/docs/index.rst",
    "content": ".. certbot-dns-dnsimple documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:23:41 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-dnsimple's documentation!\n================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_dnsimple\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-dnsimple/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-dnsimple\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-dnsimple/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-dnsimple\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"DNSimple DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-dnsimple = \"certbot_dns_dnsimple._internal.dns_dnsimple:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-dnsimple/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-dnsimple[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-dnsimple[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-dnsimple[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-dnsimple[docs]\n"
  },
  {
    "path": "certbot-dns-dnsimple/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    # This version of lexicon is required to address the problem described in\n    # https://github.com/AnalogJ/lexicon/issues/387.\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_dnsimple.dns_dnsimple` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the DNSimple API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n========================================  =====================================\n``--dns-dnsimple-credentials``            DNSimple credentials_ INI file.\n                                          (Required)\n``--dns-dnsimple-propagation-seconds``    The number of seconds to wait for DNS\n                                          to propagate before asking the ACME\n                                          server to verify the DNS record.\n                                          (Default: 30)\n========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing DNSimple API\ncredentials, obtained from your DNSimple\n`account page <https://dnsimple.com/user>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # DNSimple API credentials used by Certbot\n   dns_dnsimple_token = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\n\nThe path to this file can be provided interactively or using the\n``--dns-dnsimple-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   DNSimple account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-dnsimple \\\\\n     --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-dnsimple \\\\\n     --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-dnsimple \\\\\n     --dns-dnsimple-credentials ~/.secrets/certbot/dnsimple.ini \\\\\n     --dns-dnsimple-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_dnsimple.dns_dnsimple` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/_internal/dns_dnsimple.py",
    "content": "\"\"\"DNS Authenticator for DNSimple DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nACCOUNT_URL = 'https://dnsimple.com/user'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for DNSimple\n\n    This Authenticator uses the DNSimple v2 API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using DNSimple for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('token',\n                                  f'User access token for DNSimple v2 API. (See {ACCOUNT_URL}.)',\n                                  'auth_token')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 30) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='DNSimple credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the DNSimple API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'dnsimple'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:\n        hint = None\n        if str(e).startswith('401 Client Error: Unauthorized for url:'):\n            hint = 'Is your API token value correct?'\n\n        hint_disp = f' ({hint})' if hint else ''\n\n        return errors.PluginError(f'Error determining zone identifier for {domain_name}: '\n                                  f'{e}.{hint_disp}')\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-dnsimple tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/_internal/tests/dns_dnsimple_test.py",
    "content": "\"\"\"Tests for certbot_dns_dnsimple._internal.dns_dnsimple.\"\"\"\nfrom unittest import mock\nimport sys\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.tests import util as test_util\n\nTOKEN = 'foo'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    LOGIN_ERROR = HTTPError('401 Client Error: Unauthorized for url: ...', response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_dnsimple._internal.dns_dnsimple import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"dnsimple_token\": TOKEN}, path)\n\n        self.config = mock.MagicMock(dnsimple_credentials=path,\n                                     dnsimple_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"dnsimple\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-dnsimple/src/certbot_dns_dnsimple/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-dnsmadeeasy/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-dnsmadeeasy/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-dnsmadeeasy/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_dnsmadeeasy/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/README.rst",
    "content": "DNS Made Easy DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-dnsmadeeasy\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-dnsmadeeasy documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:39:34 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-dnsmadeeasy'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-dnsmadeeasydoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-dnsmadeeasy.tex', u'certbot-dns-dnsmadeeasy Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-dnsmadeeasy', u'certbot-dns-dnsmadeeasy Documentation',\n     author, 'certbot-dns-dnsmadeeasy', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/index.rst",
    "content": ".. certbot-dns-dnsmadeeasy documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:39:34 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-dnsmadeeasy's documentation!\n===================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_dnsmadeeasy\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-dnsmadeeasy\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-dnsmadeeasy\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"DNS Made Easy DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-dnsmadeeasy = \"certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-dnsmadeeasy[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-dnsmadeeasy[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-dnsmadeeasy[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-dnsmadeeasy[docs]\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the DNS Made Easy API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n=========================================  =====================================\n``--dns-dnsmadeeasy-credentials``          DNS Made Easy credentials_ INI file.\n                                           (Required)\n``--dns-dnsmadeeasy-propagation-seconds``  The number of seconds to wait for DNS\n                                           to propagate before asking the ACME\n                                           server to verify the DNS record.\n                                           (Default: 60)\n=========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing DNS Made Easy API\ncredentials, obtained from your DNS Made Easy\n`account page <https://cp.dnsmadeeasy.com/account/info>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # DNS Made Easy API credentials used by Certbot\n   dns_dnsmadeeasy_api_key = 1c1a3c91-4770-4ce7-96f4-54c0eb0e457a\n   dns_dnsmadeeasy_secret_key = c9b5625f-9834-4ff8-baba-4ed5f32cae55\n\nThe path to this file can be provided interactively or using the\n``--dns-dnsmadeeasy-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   DNS Made Easy account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-dnsmadeeasy \\\\\n     --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-dnsmadeeasy \\\\\n     --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 2 minutes\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-dnsmadeeasy \\\\\n     --dns-dnsmadeeasy-credentials ~/.secrets/certbot/dnsmadeeasy.ini \\\\\n     --dns-dnsmadeeasy-propagation-seconds 120 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_dnsmadeeasy.dns_dnsmadeeasy` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/_internal/dns_dnsmadeeasy.py",
    "content": "\"\"\"DNS Authenticator for DNS Made Easy DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nACCOUNT_URL = 'https://cp.dnsmadeeasy.com/account/info'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for DNS Made Easy\n\n    This Authenticator uses the DNS Made Easy API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = ('Obtain certificates using a DNS TXT record (if you are using DNS Made Easy for '\n                   'DNS).')\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('api-key',\n                                  'API key for DNS Made Easy account, '\n                                  f'obtained from {ACCOUNT_URL}',\n                                  'auth_username')\n        self._add_provider_option('secret-key',\n                                  'Secret key for DNS Made Easy account, '\n                                  f'obtained from {ACCOUNT_URL}',\n                                  'auth_token')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 60) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='DNS Made Easy credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the DNS Made Easy API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'dnsmadeeasy'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        if domain_name in str(e) and str(e).startswith('404 Client Error: Not Found for url:'):\n            return None\n\n        hint = None\n        if str(e).startswith('403 Client Error: Forbidden for url:'):\n            hint = 'Are your API key and Secret key values correct?'\n\n        hint_disp = f' ({hint})' if hint else ''\n\n        return errors.PluginError(f'Error determining zone identifier: {e}.{hint_disp}')\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-dnsmadeeasy tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/_internal/tests/dns_dnsmadeeasy_test.py",
    "content": "\"\"\"Tests for certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy.\"\"\"\n\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_KEY = 'foo'\nSECRET_KEY = 'bar'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.', response=Response())\n    LOGIN_ERROR = HTTPError(f'403 Client Error: Forbidden for url: {DOMAIN}.', response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_dnsmadeeasy._internal.dns_dnsmadeeasy import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"dnsmadeeasy_api_key\": API_KEY,\n                               \"dnsmadeeasy_secret_key\": SECRET_KEY},\n                              path)\n\n        self.config = mock.MagicMock(dnsmadeeasy_credentials=path,\n                                     dnsmadeeasy_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"dnsmadeeasy\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-dnsmadeeasy/src/certbot_dns_dnsmadeeasy/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-gehirn/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-gehirn/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-gehirn/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-gehirn/LICENSE.txt",
    "content": "   Copyright 2018 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-gehirn/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_gehirn/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-gehirn/README.rst",
    "content": "Gehirn Infrastructure Service DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-gehirn/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-gehirn/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-gehirn\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-gehirn/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-gehirn/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-gehirn documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:30:40 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-gehirn'\ncopyright = u'2018, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-gehirndoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-gehirn.tex', u'certbot-dns-gehirn Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-gehirn', u'certbot-dns-gehirn Documentation',\n     author, 'certbot-dns-gehirn', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-gehirn/docs/index.rst",
    "content": ".. certbot-dns-gehirn documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:30:40 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-gehirn's documentation!\n==============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n.. automodule:: certbot_dns_gehirn\n   :members:\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-gehirn/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-gehirn\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-gehirn/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-gehirn\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Gehirn Infrastructure Service DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-gehirn = \"certbot_dns_gehirn._internal.dns_gehirn:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-gehirn/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-gehirn[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-gehirn[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-gehirn[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-gehirn[docs]\n"
  },
  {
    "path": "certbot-dns-gehirn/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_gehirn.dns_gehirn` plugin automates the process of completing\na ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently\nremoving, TXT records using the Gehirn Infrastructure Service DNS API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n========================================  =====================================\n``--dns-gehirn-credentials``              Gehirn Infrastructure Service\n                                          credentials_ INI file.\n                                          (Required)\n``--dns-gehirn-propagation-seconds``      The number of seconds to wait for DNS\n                                          to propagate before asking the ACME\n                                          server to verify the DNS record.\n                                          (Default: 30)\n========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing\nGehirn Infrastructure Service DNS API credentials,\nobtained from your Gehirn Infrastructure Service\n`dashboard <https://gis.gehirn.jp/>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # Gehirn Infrastructure Service API credentials used by Certbot\n   dns_gehirn_api_token  = 00000000-0000-0000-0000-000000000000\n   dns_gehirn_api_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\n\nThe path to this file can be provided interactively or using the\n``--dns-gehirn-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   Gehirn Infrastructure Service account. Users who can read this file can use\n   these credentials to issue arbitrary API calls on your behalf. Users who can\n   cause Certbot to run using these credentials can complete a ``dns-01``\n   challenge to acquire new certificates or revoke existing certificates for\n   associated domains, even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-gehirn \\\\\n     --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-gehirn \\\\\n     --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-gehirn \\\\\n     --dns-gehirn-credentials ~/.secrets/certbot/gehirn.ini \\\\\n     --dns-gehirn-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_gehirn.dns_gehirn` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/_internal/dns_gehirn.py",
    "content": "\"\"\"DNS Authenticator for Gehirn Infrastructure Service DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nDASHBOARD_URL = \"https://gis.gehirn.jp/\"\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for Gehirn Infrastructure Service DNS\n\n    This Authenticator uses the Gehirn Infrastructure Service API to fulfill\n    a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record ' + \\\n                  '(if you are using Gehirn Infrastructure Service for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('api-token',\n                                  'API token for Gehirn Infrastructure Service '\n                                  f'API obtained from {DASHBOARD_URL}',\n                                  'auth_token')\n        self._add_provider_option('api-secret',\n                                  'API secret for Gehirn Infrastructure Service '\n                                  f'API obtained from {DASHBOARD_URL}',\n                                  'auth_secret')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 30) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='Gehirn Infrastructure Service credentials file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Gehirn Infrastructure Service API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'gehirn'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')):\n            return None  # Expected errors when zone name guess is wrong\n        return super()._handle_http_error(e, domain_name)\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-gehirn tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/_internal/tests/dns_gehirn_test.py",
    "content": "\"\"\"Tests for certbot_dns_gehirn._internal.dns_gehirn.\"\"\"\n\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_TOKEN = '00000000-0000-0000-0000-000000000000'\nAPI_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.', response=Response())\n    LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.', response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_gehirn._internal.dns_gehirn import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write(\n            {\"gehirn_api_token\": API_TOKEN, \"gehirn_api_secret\": API_SECRET},\n            path\n        )\n\n        self.config = mock.MagicMock(gehirn_credentials=path,\n                                     gehirn_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"gehirn\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-gehirn/src/certbot_dns_gehirn/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-google/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-google/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-google/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-google/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-google/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\nrecursive-include src/certbot_dns_google/_internal/tests/testdata *\ninclude src/certbot_dns_google/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-google/README.rst",
    "content": "Google Cloud DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-google/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-google/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-google\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-google/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-google/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-google documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 15:47:49 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\nimport os\nimport sys\n\nsys.path.insert(0, os.path.abspath('_ext'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-google'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-googledoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-google.tex', u'certbot-dns-google Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-google', u'certbot-dns-google Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-google', u'certbot-dns-google Documentation',\n     author, 'certbot-dns-google', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-google/docs/index.rst",
    "content": ".. certbot-dns-google documentation master file, created by\n   sphinx-quickstart on Wed May 10 15:47:49 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-google's documentation!\n==============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_google\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-google/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-google\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-google/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-google\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Google Cloud DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-google = \"certbot_dns_google._internal.dns_google:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-google/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-google[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-google[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-google[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-google[docs]\n"
  },
  {
    "path": "certbot-dns-google/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'google-api-python-client>=1.6.5',\n    'google-auth>=2.16.0',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_google.dns_google` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the Google Cloud DNS API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n========================================  =====================================\n``--dns-google-credentials``              Google Cloud Platform credentials_\n                                          JSON file.\n\n                                          (Required if not using `Application Default\n                                          Credentials <https://cloud.google.com/docs/authentication/\n                                          application-default-credentials>`_.)\n``--dns-google-project``                  The ID of the Google Cloud project that the Google\n                                          Cloud DNS managed zone(s) reside in.\n\n                                          (Default: project that the Google credentials_ belong to)\n``--dns-google-propagation-seconds``      The number of seconds to wait for DNS\n                                          to propagate before asking the ACME\n                                          server to verify the DNS record.\n                                          (Default: 60)\n========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires Google Cloud Platform credentials with the ability to modify the Cloud\nDNS managed zone(s) for which certificates are being issued.\n\nIn most cases, configuring credentials for Certbot will require `creating a service account\n<https://cloud.google.com/iam/docs/service-accounts-create>`_, and then either `granting permissions\nwith predefined roles`_ or `granting permissions with custom roles`_ using IAM.\n\n\nProviding Credentials\n^^^^^^^^^^^^^^^^^^^^^\n\nThe preferred method of providing credentials is to `set up Application Default Credentials\n<https://cloud.google.com/docs/authentication/provide-credentials-adc>`_ (ADC) in the environment\nthat Certbot is running in.\n\nIf you are running Certbot on Google Cloud then a service account can be assigned directly to most\ntypes of workload, including `Compute Engine VMs <https://cloud.google.com/compute/docs/access/\ncreate-enable-service-accounts-for-instances>`_, `Kubernetes Engine Pods <https://cloud.google.com/\nkubernetes-engine/docs/how-to/workload-identity>`_, `Cloud Run jobs <https://cloud.google.com/run\n/docs/securing/service-identity>`_, `Cloud Functions <https://cloud.google.com/functions/docs/\nsecuring/function-identity>`_, and `Cloud Builds <https://cloud.google.com/build/docs/securing-\nbuilds/configure-user-specified-service-accounts>`_.\n\nIf you are not running Certbot on Google Cloud then a credentials file should be provided using the\n``--dns-google-credentials`` command-line argument. Google provides documentation for `creating\nservice account keys <https://cloud.google.com/iam/docs/keys-create-delete#creating>`_, which is the\nmost common method of using a service account outside of Google Cloud.\n\n.. code-block:: json\n   :name: credentials-sa.json\n   :caption: Example service account key file:\n\n   {\n      \"type\": \"service_account\",\n      \"project_id\": \"...\",\n      \"private_key_id\": \"...\",\n      \"private_key\": \"...\",\n      \"client_email\": \"...\",\n      \"client_id\": \"...\",\n      \"auth_uri\": \"https://accounts.google.com/o/oauth2/auth\",\n      \"token_uri\": \"https://accounts.google.com/o/oauth2/token\",\n      \"auth_provider_x509_cert_url\": \"https://www.googleapis.com/oauth2/v1/certs\",\n      \"client_x509_cert_url\": \"...\"\n   }\n\n.. caution::\n   You should protect these credentials as you would a password. Users who\n   can read this file can use these credentials to issue some types of API calls\n   on your behalf, limited by the permissions assigned to the account. Users who\n   can cause Certbot to run using these credentials can complete a ``dns-01``\n   challenge to acquire new certificates or revoke existing certificates for\n   domains these credentials are authorized to manage.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\nIf you are running Certbot within another cloud platform, a CI platform, or any other platform that\nsupports issuing OpenID Connect Tokens, then you may also have the option of securely authenticating\nwith `workload identity federation <https://cloud.google.com/iam/docs/workload-identity-\nfederation>`_. Instructions are generally available for most platforms, including `AWS or Azure\n<https://cloud.google.com/iam/docs/workload-identity-federation-with-other-clouds>`_, `GitHub\nActions <https://cloud.google.com/blog/products/identity-security/enabling-keyless-authentication\n-from-github-actions>`_, and `GitLab CI <https://docs.gitlab.com/ee/ci/cloud_services/\ngoogle_cloud/>`_.\n\n\nGranting Permissions with Predefined Roles\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThe simplest method of granting the required permissions to the user or service account that Certbot\nis authenticating with is to use either of these predefined role strategies:\n\n* `dns.admin <https://cloud.google.com/dns/docs/access-control#dns.admin>`_ against the *DNS\n  zone(s)* that Certbot will be issuing certificates for.\n* `dns.reader <https://cloud.google.com/dns/docs/access-control#dns.reader>`_ against the *project*\n  containing the relevant DNS zones.\n\n*or*\n\n* `dns.admin <https://cloud.google.com/dns/docs/access-control#dns.admin>`_ against the *project*\n  containing the relevant DNS zones\n\nFor instructions on how to grant roles, please read the Google provided documentation for `granting\naccess roles against a project <https://cloud.google.com/iam/docs/granting-changing-revoking-access\n#single-role>`_ and `granting access roles against zones <https://cloud.google.com/dns/docs/zones/\niam-per-resource-zones#set_access_control_policy_for_a_specific_resource>`_.\n\n.. caution::\n   Granting the ``dns.admin`` role at the project level can present a significant security risk. It\n   provides full administrative access to all DNS zones within the project, granting the ability to\n   perform any action up to and including deleting all zones within a project.\n\n\nGranting Permissions with Custom Roles\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nCustom roles are an alternative to predefined roles that provide the ability to define fine grained\npermission sets for specific use cases. They should generally be used when it is desirable to adhere\nto the principle of least privilege, such as within production or other security sensitive\nworkloads.\n\nThe following is an example strategy for granting granular permissions to Certbot using custom\nroles. If you are not already familiar with how to do so, Google provides documentation for\n`creating a custom IAM role <https://cloud.google.com/iam/docs/creating-custom-roles#creating>`_.\n\nFirstly, create a custom role containing the permissions required to make DNS record updates. We\nsuggest naming the custom role ``Certbot - Zone Editor`` with the ID ``certbot.zoneEditor``. The\nfollowing permissions are required:\n\n* ``dns.changes.create``\n* ``dns.changes.get``\n* ``dns.changes.list``\n* ``dns.resourceRecordSets.create``\n* ``dns.resourceRecordSets.delete``\n* ``dns.resourceRecordSets.list``\n* ``dns.resourceRecordSets.update``\n\nNext, create a custom role granting Certbot the ability to discover DNS zones. We suggest naming the\ncustom role ``Certbot - Zone Lister`` with the ID ``certbot.zoneLister``. The following permissions\nare required:\n\n* ``dns.managedZones.get``\n* ``dns.managedZones.list``\n\nFinally, grant the custom roles to the user or service account that Certbot is authenticating with:\n\n* Grant your custom ``Certbot - Zone Editor`` role against the *DNS zone(s)* that Certbot will be\n  issuing certificates for.\n* Grant your custom ``Certbot - Zone Lister`` role against the *project* containing the relevant DNS\n  zones.\n\nFor instructions on how to grant roles, please read the Google provided documentation for `granting\naccess roles against a project <https://cloud.google.com/iam/docs/granting-changing-revoking-access\n#single-role>`_ and `granting access roles against zones <https://cloud.google.com/dns/docs/zones/\niam-per-resource-zones#set_access_control_policy_for_a_specific_resource>`_.\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, providing a credentials file\n\n   certbot certonly \\\\\n     --dns-google \\\\\n     --dns-google-credentials ~/.secrets/certbot/google.json \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, where ADC is available and\n             a credentials file is not required\n\n   certbot certonly \\\\\n     --dns-google \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-google \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, where the managed DNS\n             zone resides in another Google Cloud project.\n\n   certbot certonly \\\\\n     --dns-google \\\\\n     --dns-google-credentials ~/.secrets/certbot/google-project-test-foo.json \\\\\n     --dns-google-project test-bar \\\\\n     -d example.com\n\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 120 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-google \\\\\n     --dns-google-propagation-seconds 120 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_google.dns_google` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/_internal/dns_google.py",
    "content": "\"\"\"DNS Authenticator for Google Cloud DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\nfrom typing import cast\n\nimport google.auth\n\nfrom google.auth import exceptions as googleauth_exceptions\nfrom googleapiclient import discovery\nfrom googleapiclient import errors as googleapiclient_errors\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\nADC_URL = 'https://cloud.google.com/docs/authentication/application-default-credentials'\nACCT_URL = 'https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount'\nPERMISSIONS_URL = 'https://cloud.google.com/dns/access-control#permissions_and_roles'\nMETADATA_URL = 'http://metadata.google.internal/computeMetadata/v1/'\nMETADATA_HEADERS = {'Metadata-Flavor': 'Google'}\n\n\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for Google Cloud DNS\n\n    This Authenticator uses the Google Cloud DNS API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = ('Obtain certificates using a DNS TXT record (if you are using Google Cloud DNS '\n                   'for DNS).')\n    ttl = 60\n    google_client = None\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 60) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials',\n            help=('Path to Google Cloud DNS service account JSON file to use instead of relying on'\n                  ' Application Default Credentials (ADC). (See {0} for information about ADC, {1}'\n                  ' for information about creating a service account, and {2} for information about'\n                  ' the permissions required to modify Cloud DNS records.)')\n                  .format(ADC_URL, ACCT_URL, PERMISSIONS_URL),\n            default=None)\n\n        add('project',\n            help=('The ID of the Google Cloud project that the Google Cloud DNS managed zone(s)' +\n                  ' reside in. This will be determined automatically if not specified.'),\n            default=None)\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Google Cloud DNS API.'\n\n    def _setup_credentials(self) -> None:\n        if self.conf('credentials') is not None:\n            self._configure_file('credentials',\n                                 'path to Google Cloud DNS service account JSON file')\n\n            dns_common.validate_file_permissions(self.conf('credentials'))\n\n        try:\n            self._get_google_client()\n        except googleauth_exceptions.DefaultCredentialsError as e:\n            raise errors.PluginError('Authentication using Google Application Default Credentials '\n                                     'failed ({}). Please configure credentials using'\n                                     ' --dns-google-credentials <file>'.format(e))\n\n    def _perform(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_google_client().add_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _cleanup(self, domain: str, validation_name: str, validation: str) -> None:\n        self._get_google_client().del_txt_record(domain, validation_name, validation, self.ttl)\n\n    def _get_google_client(self) -> '_GoogleClient':\n        if self.google_client is None:\n            self.google_client = _GoogleClient(self.conf('credentials'), self.conf('project'))\n        return self.google_client\n\n\n\nclass _GoogleClient:\n    \"\"\"\n    Encapsulates all communication with the Google Cloud DNS API.\n    \"\"\"\n\n    def __init__(self, account_json: Optional[str] = None,\n                 dns_project_id: Optional[str] = None,\n                 dns_api: Optional[discovery.Resource] = None) -> None:\n\n        scopes = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']\n        credentials = None\n        project_id = None\n\n        if account_json is not None:\n            try:\n                credentials, project_id = google.auth.load_credentials_from_file( # type: ignore [no-untyped-call, unused-ignore] # pylint: disable=line-too-long\n                    account_json, scopes=scopes)\n            except googleauth_exceptions.GoogleAuthError as e:\n                raise errors.PluginError(\n                    \"Error loading credentials file '{}': {}\".format(account_json, e))\n        else:\n            credentials, project_id = google.auth.default(scopes=scopes) # type: ignore [no-untyped-call, unused-ignore] # pylint: disable=line-too-long\n\n        if dns_project_id is not None:\n            project_id = dns_project_id\n\n        if not project_id:\n            raise errors.PluginError('The Google Cloud project could not be automatically '\n                                     'determined. Please configure it using --dns-google-project'\n                                     ' <project>.')\n        self.project_id = project_id\n\n        if not dns_api:\n            self.dns = discovery.build('dns', 'v1',\n                                       credentials=credentials,\n                                       cache_discovery=False)\n        else:\n            self.dns = dns_api\n\n    def add_txt_record(self, domain: str, record_name: str, record_content: str,\n                       record_ttl: int) -> None:\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        zone_id = self._find_managed_zone_id(domain)\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            # If it wasn't possible to fetch the records at this label (missing .list permission),\n            # assume there aren't any (#5678). If there are actually records here, this will fail\n            # with HTTP 409/412 API errors.\n            record_contents = {\"rrdatas\": []}\n        add_records = record_contents[\"rrdatas\"][:]\n\n        if \"\\\"\"+record_content+\"\\\"\" in record_contents[\"rrdatas\"]:\n            # The process was interrupted previously and validation token exists\n            return\n\n        add_records.append(record_content)\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"additions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": add_records,\n                    \"ttl\": record_ttl,\n                },\n            ],\n        }\n\n        if record_contents[\"rrdatas\"]:\n            # We need to remove old records in the same request\n            data[\"deletions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents[\"rrdatas\"],\n                    \"ttl\": record_contents[\"ttl\"],\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            response = request.execute()\n\n            status = response['status']\n            change = response['id']\n            while status == 'pending':\n                request = changes.get(project=self.project_id, managedZone=zone_id, changeId=change)\n                response = request.execute()\n                status = response['status']\n        except googleapiclient_errors.Error as e:\n            logger.error('Encountered error adding TXT record: %s', e)\n            raise errors.PluginError('Error communicating with the Google Cloud DNS API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain: str, record_name: str, record_content: str,\n                       record_ttl: int) -> None:\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str domain: The domain to use to look up the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the Google API\n        \"\"\"\n\n        try:\n            zone_id = self._find_managed_zone_id(domain)\n        except errors.PluginError:\n            logger.warning('Error finding zone. Skipping cleanup.')\n            return\n\n        record_contents = self.get_existing_txt_rrset(zone_id, record_name)\n        if record_contents is None:\n            # If it wasn't possible to fetch the records at this label (missing .list permission),\n            # assume there aren't any (#5678). If there are actually records here, this will fail\n            # with HTTP 409/412 API errors.\n            record_contents = {\"rrdatas\": [\"\\\"\" + record_content + \"\\\"\"], \"ttl\": record_ttl}\n\n        data = {\n            \"kind\": \"dns#change\",\n            \"deletions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": record_contents[\"rrdatas\"],\n                    \"ttl\": record_contents[\"ttl\"],\n                },\n            ],\n        }\n\n        # Remove the record being deleted from the list\n        readd_contents = [r for r in record_contents[\"rrdatas\"]\n                            if r != \"\\\"\" + record_content + \"\\\"\"]\n        if readd_contents:\n            # We need to remove old records in the same request\n            data[\"additions\"] = [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": record_name + \".\",\n                    \"rrdatas\": readd_contents,\n                    \"ttl\": record_contents[\"ttl\"],\n                },\n            ]\n\n        changes = self.dns.changes()\n\n        try:\n            request = changes.create(project=self.project_id, managedZone=zone_id, body=data)\n            request.execute()\n        except googleapiclient_errors.Error as e:\n            logger.warning('Encountered error deleting TXT record: %s', e)\n\n    def get_existing_txt_rrset(self, zone_id: str, record_name: str) -> Optional[dict[str, Any]]:\n        \"\"\"\n        Get existing TXT records from the RRset for the record name.\n\n        If an error occurs while requesting the record set, it is suppressed\n        and None is returned.\n\n        :param str zone_id: The ID of the managed zone.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n\n        :returns: The resourceRecordSet corresponding to `record_name` or None\n        :rtype: `resourceRecordSet <https://cloud.google.com/dns/docs/reference/v1/resourceRecordSets#resource>` or `None` # pylint: disable=line-too-long\n\n        \"\"\"\n        rrs_request = self.dns.resourceRecordSets()\n        # Add dot as the API returns absolute domains\n        record_name += \".\"\n        request = rrs_request.list(project=self.project_id, managedZone=zone_id, name=record_name,\n                                   type=\"TXT\")\n        try:\n            response = request.execute()\n        except googleapiclient_errors.Error:\n            logger.info(\"Unable to list existing records. If you're \"\n                        \"requesting a wildcard certificate, this might not work.\")\n            logger.debug(\"Error was:\", exc_info=True)\n        else:\n            if response and response[\"rrsets\"]:\n                return cast(dict[str, Any], response[\"rrsets\"][0])\n        return None\n\n    def _find_managed_zone_id(self, domain: str) -> str:\n        \"\"\"\n        Find the managed zone for a given domain.\n\n        :param str domain: The domain for which to find the managed zone.\n        :returns: The ID of the managed zone, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if the managed zone cannot be found.\n        \"\"\"\n\n        zone_dns_name_guesses = dns_common.base_domain_name_guesses(domain)\n\n        mz = self.dns.managedZones()\n        for zone_name in zone_dns_name_guesses:\n            try:\n                request = mz.list(project=self.project_id, dnsName=zone_name + '.')\n                response = request.execute()\n                zones = response['managedZones']\n            except googleapiclient_errors.Error as e:\n                raise errors.PluginError('Encountered error finding managed zone: {0}'\n                                         .format(e))\n\n            for zone in zones:\n                zone_id: str = zone['id']\n                if zone['visibility'] == \"public\":\n                    logger.debug('Found id of %s for %s using name %s', zone_id, domain, zone_name)\n                    return zone_id\n\n        raise errors.PluginError('Unable to determine managed zone for {0} using zone names: {1}.'\n                                 .format(domain, zone_dns_name_guesses))\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-google tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/_internal/tests/dns_google_test.py",
    "content": "\"\"\"Tests for certbot_dns_google._internal.dns_google.\"\"\"\nfrom __future__ import annotations\nimport sys\nfrom typing import Optional\nimport unittest\nfrom unittest import mock\n\nfrom google.auth import exceptions as googleauth_exceptions\nfrom googleapiclient import discovery\nfrom googleapiclient.errors import Error\nfrom googleapiclient.http import HttpMock\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.errors import PluginError\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nACCOUNT_JSON_PATH = '/not/a/real/path.json'\nAPI_ERROR = Error()\nPROJECT_ID = \"test-test-1\"\nSCOPES = ['https://www.googleapis.com/auth/ndev.clouddns.readwrite']\n\nclass AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_google._internal.dns_google import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.json')\n        open(path, \"wb\").close()\n\n        super().setUp()\n        self.config = mock.MagicMock(google_credentials=path,\n                                     google_project=PROJECT_ID,\n                                     google_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"google\")\n\n        self.mock_client = mock.MagicMock()\n\n    @test_util.patch_display_util()\n    def test_perform(self, unused_mock_get_utility):\n        # _get_google_client | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_google_client', mock.MagicMock(return_value=self.mock_client))\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_cleanup(self):\n        # _get_google_client | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_google_client', mock.MagicMock(return_value=self.mock_client))\n        # _attempt_cleanup | pylint: disable=protected-access\n        self.auth._attempt_cleanup = True\n        self.auth.cleanup([self.achall])\n\n        expected = [mock.call.del_txt_record(DOMAIN, '_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    @test_util.patch_display_util()\n    def test_without_auth(self, unused_mock_get_utility):\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_google_client', mock.MagicMock(side_effect=googleauth_exceptions.DefaultCredentialsError))\n        self.config.google_credentials = None\n        with pytest.raises(PluginError):\n            self.auth.perform([self.achall])\n\n    @mock.patch('certbot_dns_google._internal.dns_google._GoogleClient')\n    def test_get_google_client(self, client_mock):\n        test_client = mock.MagicMock()\n        client_mock.return_value = test_client\n\n        self.auth._get_google_client()\n        assert client_mock.called\n        assert self.auth.google_client is test_client\n\n    def test_get_google_client_cached(self):\n        test_client = mock.MagicMock()\n        self.auth.google_client = test_client\n        assert self.auth._get_google_client() is test_client\n\n\nclass GoogleClientTest(unittest.TestCase):\n    record_name = \"foo\"\n    record_content = \"bar\"\n    record_ttl = 42\n    zone = \"ZONE_ID\"\n    change = \"an-id\"\n    visibility = \"public\"\n\n    import certbot_dns_google # should get overwritten later; need for typing\n\n    def _setUp_client_with_mock(self,\n        zone_request_side_effect: list[dict[str, list[dict[str, str]]]],\n        rrs_list_side_effect: Optional[Error] = None\n        ) -> tuple['certbot_dns_google._internal.dns_google._GoogleClient', mock.MagicMock]:\n        from certbot_dns_google._internal.dns_google import _GoogleClient\n\n        pwd = os.path.dirname(__file__)\n        rel_path = 'testdata/discovery.json'\n        discovery_file = os.path.join(pwd, rel_path)\n        http_mock = HttpMock(discovery_file, {'status': '200'})\n        dns_api = discovery.build('dns', 'v1', http=http_mock)\n\n        client = _GoogleClient(ACCOUNT_JSON_PATH, None, dns_api)\n\n        # Setup\n        mock_mz = mock.MagicMock()\n        mock_mz.list.return_value.execute.side_effect = zone_request_side_effect\n\n        mock_rrs = mock.MagicMock()\n        def rrs_list(project=None, managedZone=None, name=None, type=None):\n            response: dict[str, list[dict[str, str | int | list[str]]]]= {\"rrsets\": []}\n            if name == \"_acme-challenge.example.org.\":\n                response = {\"rrsets\": [{\"name\": \"_acme-challenge.example.org.\", \"type\": \"TXT\",\n                              \"rrdatas\": [\"\\\"example-txt-contents\\\"\"], \"ttl\": 60}]}\n            mock_return = mock.MagicMock()\n            mock_return.execute.return_value = response\n            mock_return.execute.side_effect = rrs_list_side_effect\n            return mock_return\n        mock_rrs.list.side_effect = rrs_list\n        mock_changes = mock.MagicMock()\n\n        client.dns.managedZones = mock.MagicMock(return_value=mock_mz)\n        client.dns.changes = mock.MagicMock(return_value=mock_changes)\n        client.dns.resourceRecordSets = mock.MagicMock(return_value=mock_rrs)\n\n        return client, mock_changes\n\n    @mock.patch('googleapiclient.discovery.build')\n    @mock.patch('google.auth.default')\n    def test_client_with_default_credentials(self, credential_mock, discovery_mock):\n        test_credentials = mock.MagicMock()\n        credential_mock.return_value = (test_credentials, PROJECT_ID)\n        from certbot_dns_google._internal.dns_google import _GoogleClient\n        client = _GoogleClient(None)\n        credential_mock.assert_called_once_with(scopes=SCOPES)\n        assert client.project_id == PROJECT_ID\n        discovery_mock.assert_called_once_with('dns', 'v1',\n                                credentials=test_credentials,\n                                cache_discovery=False)\n\n    @mock.patch('googleapiclient.discovery.build')\n    @mock.patch('google.auth.load_credentials_from_file')\n    def test_client_with_json_credentials(self, credential_mock, discovery_mock):\n        test_credentials = mock.MagicMock()\n        credential_mock.return_value = (test_credentials, PROJECT_ID)\n        from certbot_dns_google._internal.dns_google import _GoogleClient\n        client = _GoogleClient(ACCOUNT_JSON_PATH)\n        credential_mock.assert_called_once_with(ACCOUNT_JSON_PATH, scopes=SCOPES)\n        assert credential_mock.called\n        assert client.project_id == PROJECT_ID\n        discovery_mock.assert_called_once_with('dns', 'v1',\n                                       credentials=test_credentials,\n                                       cache_discovery=False)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    def test_client_bad_credentials_file(self, credential_mock):\n        google_dce: type[googleauth_exceptions.DefaultCredentialsError] = googleauth_exceptions.DefaultCredentialsError\n        credential_mock.side_effect = google_dce('Some exception buried in google.auth')\n        with pytest.raises(errors.PluginError) as exc_info:\n            self._setUp_client_with_mock([])\n        assert str(exc_info.value) == \\\n            \"Error loading credentials file '/not/a/real/path.json': \" \\\n            \"Some exception buried in google.auth\"\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    def test_client_missing_project_id(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), \"\")\n        with pytest.raises(errors.PluginError) as exc_info:\n            self._setUp_client_with_mock([])\n        assert str(exc_info.value) == \\\n            \"The Google Cloud project could not be automatically determined. \" \\\n            \"Please configure it using --dns-google-project <project>.\"\n\n    @mock.patch('googleapiclient.discovery.build')\n    @mock.patch('google.auth.default')\n    def test_client_with_project_id(self, credential_mock, unused_discovery_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n        from certbot_dns_google._internal.dns_google import _GoogleClient\n        client = _GoogleClient(None, \"test-project-2\")\n        assert credential_mock.called\n        assert client.project_id == \"test-project-2\"\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        credential_mock.assert_called_once_with('/not/a/real/path.json', scopes=SCOPES)\n\n        client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        expected_body = {\n            \"kind\": \"dns#change\",\n            \"additions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": self.record_name + \".\",\n                    \"rrdatas\": [self.record_content, ],\n                    \"ttl\": self.record_ttl,\n                },\n            ],\n        }\n\n        changes.create.assert_called_with(body=expected_body,\n                                               managedZone=self.zone,\n                                               project=PROJECT_ID)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_and_poll(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change}\n        changes.get.return_value.execute.return_value = {'status': 'done'}\n\n        client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        changes.create.assert_called_with(body=mock.ANY,\n                                               managedZone=self.zone,\n                                               project=PROJECT_ID)\n\n        changes.get.assert_called_with(changeId=self.change,\n                                            managedZone=self.zone,\n                                            project=PROJECT_ID)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_and_poll_split_horizon(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': '{zone}-private'.format(zone=self.zone), 'dnsName': DOMAIN, 'visibility': 'private'},{'id': '{zone}-public'.format(zone=self.zone), 'dnsName': DOMAIN, 'visibility': self.visibility}]}])\n        changes.create.return_value.execute.return_value = {'status': 'pending', 'id': self.change}\n        changes.get.return_value.execute.return_value = {'status': 'done'}\n\n        client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n        changes.create.assert_called_with(body=mock.ANY,\n                                               managedZone='{zone}-public'.format(zone=self.zone),\n                                               project=PROJECT_ID)\n\n        changes.get.assert_called_with(changeId=self.change,\n                                            managedZone='{zone}-public'.format(zone=self.zone),\n                                            project=PROJECT_ID)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_delete_old(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        # pylint: disable=line-too-long\n        mock_get_rrs = \"certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset\"\n        with mock.patch(mock_get_rrs) as mock_rrs:\n            mock_rrs.return_value = {\"rrdatas\": [\"sample-txt-contents\"], \"ttl\": self.record_ttl}\n            client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n            assert changes.create.called is True\n            deletions = changes.create.call_args_list[0][1][\"body\"][\"deletions\"][0]\n            assert \"sample-txt-contents\" in deletions[\"rrdatas\"]\n            assert self.record_ttl == deletions[\"ttl\"]\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_delete_old_ttl_case(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        # pylint: disable=line-too-long\n        mock_get_rrs = \"certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset\"\n        with mock.patch(mock_get_rrs) as mock_rrs:\n            custom_ttl = 300\n            mock_rrs.return_value = {\"rrdatas\": [\"sample-txt-contents\"], \"ttl\": custom_ttl}\n            client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n            assert changes.create.called is True\n            deletions = changes.create.call_args_list[0][1][\"body\"][\"deletions\"][0]\n            assert \"sample-txt-contents\" in deletions[\"rrdatas\"]\n            assert custom_ttl == deletions[\"ttl\"] #otherwise HTTP 412\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_noop(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        client.add_txt_record(DOMAIN, \"_acme-challenge.example.org\",\n                              \"example-txt-contents\", self.record_ttl)\n        assert changes.create.called is False\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_error_during_zone_lookup(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock(API_ERROR)\n\n        with pytest.raises(errors.PluginError):\n            client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_zone_not_found(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock([{'managedZones': []},\n                                                               {'managedZones': []}])\n\n        with pytest.raises(errors.PluginError):\n            client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_add_txt_record_error_during_add(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        changes.create.side_effect = API_ERROR\n\n        with pytest.raises(errors.PluginError):\n            client.add_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_del_txt_record_multi_rrdatas(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        # pylint: disable=line-too-long\n        mock_get_rrs = \"certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset\"\n        with mock.patch(mock_get_rrs) as mock_rrs:\n            mock_rrs.return_value = {\"rrdatas\": [\"\\\"sample-txt-contents\\\"\",\n                                     \"\\\"example-txt-contents\\\"\"], \"ttl\": self.record_ttl}\n            client.del_txt_record(DOMAIN, \"_acme-challenge.example.org\",\n                                \"example-txt-contents\", self.record_ttl)\n\n        expected_body = {\n            \"kind\": \"dns#change\",\n            \"deletions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": \"_acme-challenge.example.org.\",\n                    \"rrdatas\": [\"\\\"sample-txt-contents\\\"\", \"\\\"example-txt-contents\\\"\"],\n                    \"ttl\": self.record_ttl,\n                },\n            ],\n            \"additions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": \"_acme-challenge.example.org.\",\n                    \"rrdatas\": [\"\\\"sample-txt-contents\\\"\", ],\n                    \"ttl\": self.record_ttl,\n                },\n            ],\n        }\n\n        changes.create.assert_called_with(body=expected_body,\n                                               managedZone=self.zone,\n                                               project=PROJECT_ID)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_del_txt_record_single_rrdatas(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        # pylint: disable=line-too-long\n        mock_get_rrs = \"certbot_dns_google._internal.dns_google._GoogleClient.get_existing_txt_rrset\"\n        with mock.patch(mock_get_rrs) as mock_rrs:\n            mock_rrs.return_value = {\"rrdatas\": [\"\\\"example-txt-contents\\\"\"], \"ttl\": self.record_ttl}\n            client.del_txt_record(DOMAIN, \"_acme-challenge.example.org\",\n                                \"example-txt-contents\", self.record_ttl)\n\n        expected_body = {\n            \"kind\": \"dns#change\",\n            \"deletions\": [\n                {\n                    \"kind\": \"dns#resourceRecordSet\",\n                    \"type\": \"TXT\",\n                    \"name\": \"_acme-challenge.example.org.\",\n                    \"rrdatas\": [\"\\\"example-txt-contents\\\"\"],\n                    \"ttl\": self.record_ttl,\n                },\n            ],\n        }\n\n        changes.create.assert_called_with(body=expected_body,\n                                               managedZone=self.zone,\n                                               project=PROJECT_ID)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_del_txt_record_error_during_zone_lookup(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock(API_ERROR)\n        client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n        changes.create.assert_not_called()\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_del_txt_record_zone_not_found(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': []},\n                                                               {'managedZones': []}])\n        client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n        changes.create.assert_not_called()\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_del_txt_record_error_during_delete(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, changes = self._setUp_client_with_mock([{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        changes.create.side_effect = API_ERROR\n\n        client.del_txt_record(DOMAIN, self.record_name, self.record_content, self.record_ttl)\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_get_existing_found(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        # Record name mocked in setUp\n        found = client.get_existing_txt_rrset(self.zone, \"_acme-challenge.example.org\")\n        assert found\n        assert found[\"rrdatas\"] == [\"\\\"example-txt-contents\\\"\"]\n        assert found[\"ttl\"] == 60\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_get_existing_not_found(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}])\n        not_found = client.get_existing_txt_rrset(self.zone, \"nonexistent.tld\")\n        assert not_found is None\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_get_existing_with_error(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}], API_ERROR)\n        # Record name mocked in setUp\n        found = client.get_existing_txt_rrset(self.zone, \"_acme-challenge.example.org\")\n        assert found is None\n\n    @mock.patch('google.auth.load_credentials_from_file')\n    @mock.patch('certbot_dns_google._internal.dns_google.open',\n                mock.mock_open(read_data='{\"project_id\": \"' + PROJECT_ID + '\"}'), create=True)\n    def test_get_existing_fallback(self, credential_mock):\n        credential_mock.return_value = (mock.MagicMock(), PROJECT_ID)\n\n        client, unused_changes = self._setUp_client_with_mock(\n            [{'managedZones': [{'id': self.zone, 'visibility': self.visibility}]}], API_ERROR)\n        rrset = client.get_existing_txt_rrset(self.zone, \"_acme-challenge.example.org\")\n        assert not rrset\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/_internal/tests/testdata/discovery.json",
    "content": "{\n \"kind\": \"discovery#restDescription\",\n \"etag\": \"\\\"-iA1DTNe4s-I6JZXPt1t1Ypy8IU/gSzgHqX4Zwypnde2YApimTf_qmE\\\"\",\n \"discoveryVersion\": \"v1\",\n \"id\": \"dns:v1\",\n \"name\": \"dns\",\n \"version\": \"v1\",\n \"revision\": \"20180314\",\n \"title\": \"Google Cloud DNS API\",\n \"description\": \"Configures and serves authoritative DNS records.\",\n \"ownerDomain\": \"google.com\",\n \"ownerName\": \"Google\",\n \"icons\": {\n  \"x16\": \"https://www.gstatic.com/images/branding/product/1x/googleg_16dp.png\",\n  \"x32\": \"https://www.gstatic.com/images/branding/product/1x/googleg_32dp.png\"\n },\n \"documentationLink\": \"https://developers.google.com/cloud-dns\",\n \"protocol\": \"rest\",\n \"baseUrl\": \"https://www.googleapis.com/dns/v1/projects/\",\n \"basePath\": \"/dns/v1/projects/\",\n \"rootUrl\": \"https://www.googleapis.com/\",\n \"servicePath\": \"dns/v1/projects/\",\n \"batchPath\": \"batch/dns/v1\",\n \"parameters\": {\n  \"alt\": {\n   \"type\": \"string\",\n   \"description\": \"Data format for the response.\",\n   \"default\": \"json\",\n   \"enum\": [\n    \"json\"\n   ],\n   \"enumDescriptions\": [\n    \"Responses with Content-Type of application/json\"\n   ],\n   \"location\": \"query\"\n  },\n  \"fields\": {\n   \"type\": \"string\",\n   \"description\": \"Selector specifying which fields to include in a partial response.\",\n   \"location\": \"query\"\n  },\n  \"key\": {\n   \"type\": \"string\",\n   \"description\": \"API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.\",\n   \"location\": \"query\"\n  },\n  \"oauth_token\": {\n   \"type\": \"string\",\n   \"description\": \"OAuth 2.0 token for the current user.\",\n   \"location\": \"query\"\n  },\n  \"prettyPrint\": {\n   \"type\": \"boolean\",\n   \"description\": \"Returns response with indentations and line breaks.\",\n   \"default\": \"true\",\n   \"location\": \"query\"\n  },\n  \"quotaUser\": {\n   \"type\": \"string\",\n   \"description\": \"Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.\",\n   \"location\": \"query\"\n  },\n  \"userIp\": {\n   \"type\": \"string\",\n   \"description\": \"IP address of the site where the request originates. Use this if you want to enforce per-user limits.\",\n   \"location\": \"query\"\n  }\n },\n \"auth\": {\n  \"oauth2\": {\n   \"scopes\": {\n    \"https://www.googleapis.com/auth/cloud-platform\": {\n     \"description\": \"View and manage your data across Google Cloud Platform services\"\n    },\n    \"https://www.googleapis.com/auth/cloud-platform.read-only\": {\n     \"description\": \"View your data across Google Cloud Platform services\"\n    },\n    \"https://www.googleapis.com/auth/ndev.clouddns.readonly\": {\n     \"description\": \"View your DNS records hosted by Google Cloud DNS\"\n    },\n    \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\": {\n     \"description\": \"View and manage your DNS records hosted by Google Cloud DNS\"\n    }\n   }\n  }\n },\n \"schemas\": {\n  \"Change\": {\n   \"id\": \"Change\",\n   \"type\": \"object\",\n   \"description\": \"An atomic update to a collection of ResourceRecordSets.\",\n   \"properties\": {\n    \"additions\": {\n     \"type\": \"array\",\n     \"description\": \"Which ResourceRecordSets to add?\",\n     \"items\": {\n      \"$ref\": \"ResourceRecordSet\"\n     }\n    },\n    \"deletions\": {\n     \"type\": \"array\",\n     \"description\": \"Which ResourceRecordSets to remove? Must match existing data exactly.\",\n     \"items\": {\n      \"$ref\": \"ResourceRecordSet\"\n     }\n    },\n    \"id\": {\n     \"type\": \"string\",\n     \"description\": \"Unique identifier for the resource; defined by the server (output only).\"\n    },\n    \"isServing\": {\n     \"type\": \"boolean\",\n     \"description\": \"If the DNS queries for the zone will be served.\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#change\\\".\",\n     \"default\": \"dns#change\"\n    },\n    \"startTime\": {\n     \"type\": \"string\",\n     \"description\": \"The time that this operation was started by the server (output only). This is in RFC3339 text format.\"\n    },\n    \"status\": {\n     \"type\": \"string\",\n     \"description\": \"Status of the operation (output only).\",\n     \"enum\": [\n      \"done\",\n      \"pending\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\"\n     ]\n    }\n   }\n  },\n  \"ChangesListResponse\": {\n   \"id\": \"ChangesListResponse\",\n   \"type\": \"object\",\n   \"description\": \"The response to a request to enumerate Changes to a ResourceRecordSets collection.\",\n   \"properties\": {\n    \"changes\": {\n     \"type\": \"array\",\n     \"description\": \"The requested changes.\",\n     \"items\": {\n      \"$ref\": \"Change\"\n     }\n    },\n    \"header\": {\n     \"$ref\": \"ResponseHeader\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Type of resource.\",\n     \"default\": \"dns#changesListResponse\"\n    },\n    \"nextPageToken\": {\n     \"type\": \"string\",\n     \"description\": \"The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\\n\\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a \\\"snapshot\\\" of collections larger than the maximum page size.\"\n    }\n   }\n  },\n  \"DnsKey\": {\n   \"id\": \"DnsKey\",\n   \"type\": \"object\",\n   \"description\": \"A DNSSEC key pair.\",\n   \"properties\": {\n    \"algorithm\": {\n     \"type\": \"string\",\n     \"description\": \"String mnemonic specifying the DNSSEC algorithm of this key. Immutable after creation time.\",\n     \"enum\": [\n      \"ecdsap256sha256\",\n      \"ecdsap384sha384\",\n      \"rsasha1\",\n      \"rsasha256\",\n      \"rsasha512\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\",\n      \"\",\n      \"\",\n      \"\"\n     ]\n    },\n    \"creationTime\": {\n     \"type\": \"string\",\n     \"description\": \"The time that this resource was created in the control plane. This is in RFC3339 text format. Output only.\"\n    },\n    \"description\": {\n     \"type\": \"string\",\n     \"description\": \"A mutable string of at most 1024 characters associated with this resource for the user's convenience. Has no effect on the resource's function.\"\n    },\n    \"digests\": {\n     \"type\": \"array\",\n     \"description\": \"Cryptographic hashes of the DNSKEY resource record associated with this DnsKey. These digests are needed to construct a DS record that points at this DNS key. Output only.\",\n     \"items\": {\n      \"$ref\": \"DnsKeyDigest\"\n     }\n    },\n    \"id\": {\n     \"type\": \"string\",\n     \"description\": \"Unique identifier for the resource; defined by the server (output only).\"\n    },\n    \"isActive\": {\n     \"type\": \"boolean\",\n     \"description\": \"Active keys will be used to sign subsequent changes to the ManagedZone. Inactive keys will still be present as DNSKEY Resource Records for the use of resolvers validating existing signatures.\"\n    },\n    \"keyLength\": {\n     \"type\": \"integer\",\n     \"description\": \"Length of the key in bits. Specified at creation time then immutable.\",\n     \"format\": \"uint32\"\n    },\n    \"keyTag\": {\n     \"type\": \"integer\",\n     \"description\": \"The key tag is a non-cryptographic hash of the a DNSKEY resource record associated with this DnsKey. The key tag can be used to identify a DNSKEY more quickly (but it is not a unique identifier). In particular, the key tag is used in a parent zone's DS record to point at the DNSKEY in this child ManagedZone. The key tag is a number in the range [0, 65535] and the algorithm to calculate it is specified in RFC4034 Appendix B. Output only.\",\n     \"format\": \"int32\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#dnsKey\\\".\",\n     \"default\": \"dns#dnsKey\"\n    },\n    \"publicKey\": {\n     \"type\": \"string\",\n     \"description\": \"Base64 encoded public half of this key. Output only.\"\n    },\n    \"type\": {\n     \"type\": \"string\",\n     \"description\": \"One of \\\"KEY_SIGNING\\\" or \\\"ZONE_SIGNING\\\". Keys of type KEY_SIGNING have the Secure Entry Point flag set and, when active, will be used to sign only resource record sets of type DNSKEY. Otherwise, the Secure Entry Point flag will be cleared and this key will be used to sign only resource record sets of other types. Immutable after creation time.\",\n     \"enum\": [\n      \"keySigning\",\n      \"zoneSigning\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\"\n     ]\n    }\n   }\n  },\n  \"DnsKeyDigest\": {\n   \"id\": \"DnsKeyDigest\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"digest\": {\n     \"type\": \"string\",\n     \"description\": \"The base-16 encoded bytes of this digest. Suitable for use in a DS resource record.\"\n    },\n    \"type\": {\n     \"type\": \"string\",\n     \"description\": \"Specifies the algorithm used to calculate this digest.\",\n     \"enum\": [\n      \"sha1\",\n      \"sha256\",\n      \"sha384\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\",\n      \"\"\n     ]\n    }\n   }\n  },\n  \"DnsKeySpec\": {\n   \"id\": \"DnsKeySpec\",\n   \"type\": \"object\",\n   \"description\": \"Parameters for DnsKey key generation. Used for generating initial keys for a new ManagedZone and as default when adding a new DnsKey.\",\n   \"properties\": {\n    \"algorithm\": {\n     \"type\": \"string\",\n     \"description\": \"String mnemonic specifying the DNSSEC algorithm of this key.\",\n     \"enum\": [\n      \"ecdsap256sha256\",\n      \"ecdsap384sha384\",\n      \"rsasha1\",\n      \"rsasha256\",\n      \"rsasha512\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\",\n      \"\",\n      \"\",\n      \"\"\n     ]\n    },\n    \"keyLength\": {\n     \"type\": \"integer\",\n     \"description\": \"Length of the keys in bits.\",\n     \"format\": \"uint32\"\n    },\n    \"keyType\": {\n     \"type\": \"string\",\n     \"description\": \"One of \\\"KEY_SIGNING\\\" or \\\"ZONE_SIGNING\\\". Keys of type KEY_SIGNING have the Secure Entry Point flag set and, when active, will be used to sign only resource record sets of type DNSKEY. Otherwise, the Secure Entry Point flag will be cleared and this key will be used to sign only resource record sets of other types.\",\n     \"enum\": [\n      \"keySigning\",\n      \"zoneSigning\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\"\n     ]\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#dnsKeySpec\\\".\",\n     \"default\": \"dns#dnsKeySpec\"\n    }\n   }\n  },\n  \"DnsKeysListResponse\": {\n   \"id\": \"DnsKeysListResponse\",\n   \"type\": \"object\",\n   \"description\": \"The response to a request to enumerate DnsKeys in a ManagedZone.\",\n   \"properties\": {\n    \"dnsKeys\": {\n     \"type\": \"array\",\n     \"description\": \"The requested resources.\",\n     \"items\": {\n      \"$ref\": \"DnsKey\"\n     }\n    },\n    \"header\": {\n     \"$ref\": \"ResponseHeader\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Type of resource.\",\n     \"default\": \"dns#dnsKeysListResponse\"\n    },\n    \"nextPageToken\": {\n     \"type\": \"string\",\n     \"description\": \"The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\\n\\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a \\\"snapshot\\\" of collections larger than the maximum page size.\"\n    }\n   }\n  },\n  \"ManagedZone\": {\n   \"id\": \"ManagedZone\",\n   \"type\": \"object\",\n   \"description\": \"A zone is a subtree of the DNS namespace under one administrative responsibility. A ManagedZone is a resource that represents a DNS zone hosted by the Cloud DNS service.\",\n   \"properties\": {\n    \"creationTime\": {\n     \"type\": \"string\",\n     \"description\": \"The time that this resource was created on the server. This is in RFC3339 text format. Output only.\"\n    },\n    \"description\": {\n     \"type\": \"string\",\n     \"description\": \"A mutable string of at most 1024 characters associated with this resource for the user's convenience. Has no effect on the managed zone's function.\"\n    },\n    \"dnsName\": {\n     \"type\": \"string\",\n     \"description\": \"The DNS name of this managed zone, for instance \\\"example.com.\\\".\"\n    },\n    \"dnssecConfig\": {\n     \"$ref\": \"ManagedZoneDnsSecConfig\",\n     \"description\": \"DNSSEC configuration.\"\n    },\n    \"id\": {\n     \"type\": \"string\",\n     \"description\": \"Unique identifier for the resource; defined by the server (output only)\",\n     \"format\": \"uint64\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#managedZone\\\".\",\n     \"default\": \"dns#managedZone\"\n    },\n    \"labels\": {\n     \"type\": \"object\",\n     \"description\": \"User labels.\",\n     \"additionalProperties\": {\n      \"type\": \"string\"\n     }\n    },\n    \"name\": {\n     \"type\": \"string\",\n     \"description\": \"User assigned name for this resource. Must be unique within the project. The name must be 1-63 characters long, must begin with a letter, end with a letter or digit, and only contain lowercase letters, digits or dashes.\"\n    },\n    \"nameServerSet\": {\n     \"type\": \"string\",\n     \"description\": \"Optionally specifies the NameServerSet for this ManagedZone. A NameServerSet is a set of DNS name servers that all host the same ManagedZones. Most users will leave this field unset.\"\n    },\n    \"nameServers\": {\n     \"type\": \"array\",\n     \"description\": \"Delegate your managed_zone to these virtual name servers; defined by the server (output only)\",\n     \"items\": {\n      \"type\": \"string\"\n     }\n    },\n    \"visibility\": {\n     \"type\": \"string\",\n     \"description\": \"The zone's visibility: public zones are exposed to the Internet, while private zones are visible only to Virtual Private Cloud resources.\",\n     \"default\": \"public\"\n    }\n   }\n  },\n  \"ManagedZoneDnsSecConfig\": {\n   \"id\": \"ManagedZoneDnsSecConfig\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"defaultKeySpecs\": {\n     \"type\": \"array\",\n     \"description\": \"Specifies parameters that will be used for generating initial DnsKeys for this ManagedZone. Output only while state is not OFF.\",\n     \"items\": {\n      \"$ref\": \"DnsKeySpec\"\n     }\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#managedZoneDnsSecConfig\\\".\",\n     \"default\": \"dns#managedZoneDnsSecConfig\"\n    },\n    \"nonExistence\": {\n     \"type\": \"string\",\n     \"description\": \"Specifies the mechanism used to provide authenticated denial-of-existence responses. Output only while state is not OFF.\",\n     \"enum\": [\n      \"nsec\",\n      \"nsec3\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\"\n     ]\n    },\n    \"state\": {\n     \"type\": \"string\",\n     \"description\": \"Specifies whether DNSSEC is enabled, and what mode it is in.\",\n     \"enum\": [\n      \"off\",\n      \"on\",\n      \"transfer\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\",\n      \"\"\n     ]\n    }\n   }\n  },\n  \"ManagedZoneOperationsListResponse\": {\n   \"id\": \"ManagedZoneOperationsListResponse\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"header\": {\n     \"$ref\": \"ResponseHeader\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Type of resource.\",\n     \"default\": \"dns#managedZoneOperationsListResponse\"\n    },\n    \"nextPageToken\": {\n     \"type\": \"string\",\n     \"description\": \"The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your page token.\\n\\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size.\"\n    },\n    \"operations\": {\n     \"type\": \"array\",\n     \"description\": \"The operation resources.\",\n     \"items\": {\n      \"$ref\": \"Operation\"\n     }\n    }\n   }\n  },\n  \"ManagedZonesListResponse\": {\n   \"id\": \"ManagedZonesListResponse\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"header\": {\n     \"$ref\": \"ResponseHeader\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Type of resource.\",\n     \"default\": \"dns#managedZonesListResponse\"\n    },\n    \"managedZones\": {\n     \"type\": \"array\",\n     \"description\": \"The managed zone resources.\",\n     \"items\": {\n      \"$ref\": \"ManagedZone\"\n     }\n    },\n    \"nextPageToken\": {\n     \"type\": \"string\",\n     \"description\": \"The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your page token.\\n\\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size.\"\n    }\n   }\n  },\n  \"Operation\": {\n   \"id\": \"Operation\",\n   \"type\": \"object\",\n   \"description\": \"An operation represents a successful mutation performed on a Cloud DNS resource. Operations provide: - An audit log of server resource mutations. - A way to recover/retry API calls in the case where the response is never received by the caller. Use the caller specified client_operation_id.\",\n   \"properties\": {\n    \"dnsKeyContext\": {\n     \"$ref\": \"OperationDnsKeyContext\",\n     \"description\": \"Only populated if the operation targeted a DnsKey (output only).\"\n    },\n    \"id\": {\n     \"type\": \"string\",\n     \"description\": \"Unique identifier for the resource. This is the client_operation_id if the client specified it when the mutation was initiated, otherwise, it is generated by the server. The name must be 1-63 characters long and match the regular expression [-a-z0-9]? (output only)\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#operation\\\".\",\n     \"default\": \"dns#operation\"\n    },\n    \"startTime\": {\n     \"type\": \"string\",\n     \"description\": \"The time that this operation was started by the server. This is in RFC3339 text format (output only).\"\n    },\n    \"status\": {\n     \"type\": \"string\",\n     \"description\": \"Status of the operation. Can be one of the following: \\\"PENDING\\\" or \\\"DONE\\\" (output only).\",\n     \"enum\": [\n      \"done\",\n      \"pending\"\n     ],\n     \"enumDescriptions\": [\n      \"\",\n      \"\"\n     ]\n    },\n    \"type\": {\n     \"type\": \"string\",\n     \"description\": \"Type of the operation. Operations include insert, update, and delete (output only).\"\n    },\n    \"user\": {\n     \"type\": \"string\",\n     \"description\": \"User who requested the operation, for example: user@example.com. cloud-dns-system for operations automatically done by the system. (output only)\"\n    },\n    \"zoneContext\": {\n     \"$ref\": \"OperationManagedZoneContext\",\n     \"description\": \"Only populated if the operation targeted a ManagedZone (output only).\"\n    }\n   }\n  },\n  \"OperationDnsKeyContext\": {\n   \"id\": \"OperationDnsKeyContext\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"newValue\": {\n     \"$ref\": \"DnsKey\",\n     \"description\": \"The post-operation DnsKey resource.\"\n    },\n    \"oldValue\": {\n     \"$ref\": \"DnsKey\",\n     \"description\": \"The pre-operation DnsKey resource.\"\n    }\n   }\n  },\n  \"OperationManagedZoneContext\": {\n   \"id\": \"OperationManagedZoneContext\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"newValue\": {\n     \"$ref\": \"ManagedZone\",\n     \"description\": \"The post-operation ManagedZone resource.\"\n    },\n    \"oldValue\": {\n     \"$ref\": \"ManagedZone\",\n     \"description\": \"The pre-operation ManagedZone resource.\"\n    }\n   }\n  },\n  \"Project\": {\n   \"id\": \"Project\",\n   \"type\": \"object\",\n   \"description\": \"A project resource. The project is a top level container for resources including Cloud DNS ManagedZones. Projects can be created only in the APIs console.\",\n   \"properties\": {\n    \"id\": {\n     \"type\": \"string\",\n     \"description\": \"User assigned unique identifier for the resource (output only).\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#project\\\".\",\n     \"default\": \"dns#project\"\n    },\n    \"number\": {\n     \"type\": \"string\",\n     \"description\": \"Unique numeric identifier for the resource; defined by the server (output only).\",\n     \"format\": \"uint64\"\n    },\n    \"quota\": {\n     \"$ref\": \"Quota\",\n     \"description\": \"Quotas assigned to this project (output only).\"\n    }\n   }\n  },\n  \"Quota\": {\n   \"id\": \"Quota\",\n   \"type\": \"object\",\n   \"description\": \"Limits associated with a Project.\",\n   \"properties\": {\n    \"dnsKeysPerManagedZone\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of DnsKeys per ManagedZone.\",\n     \"format\": \"int32\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#quota\\\".\",\n     \"default\": \"dns#quota\"\n    },\n    \"managedZones\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of managed zones in the project.\",\n     \"format\": \"int32\"\n    },\n    \"resourceRecordsPerRrset\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of ResourceRecords per ResourceRecordSet.\",\n     \"format\": \"int32\"\n    },\n    \"rrsetAdditionsPerChange\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of ResourceRecordSets to add per ChangesCreateRequest.\",\n     \"format\": \"int32\"\n    },\n    \"rrsetDeletionsPerChange\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of ResourceRecordSets to delete per ChangesCreateRequest.\",\n     \"format\": \"int32\"\n    },\n    \"rrsetsPerManagedZone\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed number of ResourceRecordSets per zone in the project.\",\n     \"format\": \"int32\"\n    },\n    \"totalRrdataSizePerChange\": {\n     \"type\": \"integer\",\n     \"description\": \"Maximum allowed size for total rrdata in one ChangesCreateRequest in bytes.\",\n     \"format\": \"int32\"\n    },\n    \"whitelistedKeySpecs\": {\n     \"type\": \"array\",\n     \"description\": \"DNSSEC algorithm and key length types that can be used for DnsKeys.\",\n     \"items\": {\n      \"$ref\": \"DnsKeySpec\"\n     }\n    }\n   }\n  },\n  \"ResourceRecordSet\": {\n   \"id\": \"ResourceRecordSet\",\n   \"type\": \"object\",\n   \"description\": \"A unit of data that will be returned by the DNS servers.\",\n   \"properties\": {\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Identifies what kind of resource this is. Value: the fixed string \\\"dns#resourceRecordSet\\\".\",\n     \"default\": \"dns#resourceRecordSet\"\n    },\n    \"name\": {\n     \"type\": \"string\",\n     \"description\": \"For example, www.example.com.\"\n    },\n    \"rrdatas\": {\n     \"type\": \"array\",\n     \"description\": \"As defined in RFC 1035 (section 5) and RFC 1034 (section 3.6.1).\",\n     \"items\": {\n      \"type\": \"string\"\n     }\n    },\n    \"signatureRrdatas\": {\n     \"type\": \"array\",\n     \"description\": \"As defined in RFC 4034 (section 3.2).\",\n     \"items\": {\n      \"type\": \"string\"\n     }\n    },\n    \"ttl\": {\n     \"type\": \"integer\",\n     \"description\": \"Number of seconds that this ResourceRecordSet can be cached by resolvers.\",\n     \"format\": \"int32\"\n    },\n    \"type\": {\n     \"type\": \"string\",\n     \"description\": \"The identifier of a supported record type, for example, A, AAAA, MX, TXT, and so on.\"\n    }\n   }\n  },\n  \"ResourceRecordSetsListResponse\": {\n   \"id\": \"ResourceRecordSetsListResponse\",\n   \"type\": \"object\",\n   \"properties\": {\n    \"header\": {\n     \"$ref\": \"ResponseHeader\"\n    },\n    \"kind\": {\n     \"type\": \"string\",\n     \"description\": \"Type of resource.\",\n     \"default\": \"dns#resourceRecordSetsListResponse\"\n    },\n    \"nextPageToken\": {\n     \"type\": \"string\",\n     \"description\": \"The presence of this field indicates that there exist more results following your last page of results in pagination order. To fetch them, make another list request using this value as your pagination token.\\n\\nIn this way you can retrieve the complete contents of even very large collections one page at a time. However, if the contents of the collection change between the first and last paginated list request, the set of all elements returned will be an inconsistent view of the collection. There is no way to retrieve a consistent snapshot of a collection larger than the maximum page size.\"\n    },\n    \"rrsets\": {\n     \"type\": \"array\",\n     \"description\": \"The resource record set resources.\",\n     \"items\": {\n      \"$ref\": \"ResourceRecordSet\"\n     }\n    }\n   }\n  },\n  \"ResponseHeader\": {\n   \"id\": \"ResponseHeader\",\n   \"type\": \"object\",\n   \"description\": \"Elements common to every response.\",\n   \"properties\": {\n    \"operationId\": {\n     \"type\": \"string\",\n     \"description\": \"For mutating operation requests that completed successfully. This is the client_operation_id if the client specified it, otherwise it is generated by the server (output only).\"\n    }\n   }\n  }\n },\n \"resources\": {\n  \"changes\": {\n   \"methods\": {\n    \"create\": {\n     \"id\": \"dns.changes.create\",\n     \"path\": \"{project}/managedZones/{managedZone}/changes\",\n     \"httpMethod\": \"POST\",\n     \"description\": \"Atomically update the ResourceRecordSet collection.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"request\": {\n      \"$ref\": \"Change\"\n     },\n     \"response\": {\n      \"$ref\": \"Change\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"get\": {\n     \"id\": \"dns.changes.get\",\n     \"path\": \"{project}/managedZones/{managedZone}/changes/{changeId}\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Fetch the representation of an existing Change.\",\n     \"parameters\": {\n      \"changeId\": {\n       \"type\": \"string\",\n       \"description\": \"The identifier of the requested change, from a previous ResourceRecordSetsChangeResponse.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\",\n      \"changeId\"\n     ],\n     \"response\": {\n      \"$ref\": \"Change\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"list\": {\n     \"id\": \"dns.changes.list\",\n     \"path\": \"{project}/managedZones/{managedZone}/changes\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Enumerate Changes to a ResourceRecordSet collection.\",\n     \"parameters\": {\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"maxResults\": {\n       \"type\": \"integer\",\n       \"description\": \"Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.\",\n       \"format\": \"int32\",\n       \"location\": \"query\"\n      },\n      \"pageToken\": {\n       \"type\": \"string\",\n       \"description\": \"Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"sortBy\": {\n       \"type\": \"string\",\n       \"description\": \"Sorting criterion. The only supported value is change sequence.\",\n       \"default\": \"changeSequence\",\n       \"enum\": [\n        \"changeSequence\"\n       ],\n       \"enumDescriptions\": [\n        \"\"\n       ],\n       \"location\": \"query\"\n      },\n      \"sortOrder\": {\n       \"type\": \"string\",\n       \"description\": \"Sorting order direction: 'ascending' or 'descending'.\",\n       \"location\": \"query\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"response\": {\n      \"$ref\": \"ChangesListResponse\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  },\n  \"dnsKeys\": {\n   \"methods\": {\n    \"get\": {\n     \"id\": \"dns.dnsKeys.get\",\n     \"path\": \"{project}/managedZones/{managedZone}/dnsKeys/{dnsKeyId}\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Fetch the representation of an existing DnsKey.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"digestType\": {\n       \"type\": \"string\",\n       \"description\": \"An optional comma-separated list of digest types to compute and display for key signing keys. If omitted, the recommended digest type will be computed and displayed.\",\n       \"location\": \"query\"\n      },\n      \"dnsKeyId\": {\n       \"type\": \"string\",\n       \"description\": \"The identifier of the requested DnsKey.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\",\n      \"dnsKeyId\"\n     ],\n     \"response\": {\n      \"$ref\": \"DnsKey\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"list\": {\n     \"id\": \"dns.dnsKeys.list\",\n     \"path\": \"{project}/managedZones/{managedZone}/dnsKeys\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Enumerate DnsKeys to a ResourceRecordSet collection.\",\n     \"parameters\": {\n      \"digestType\": {\n       \"type\": \"string\",\n       \"description\": \"An optional comma-separated list of digest types to compute and display for key signing keys. If omitted, the recommended digest type will be computed and displayed.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"maxResults\": {\n       \"type\": \"integer\",\n       \"description\": \"Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.\",\n       \"format\": \"int32\",\n       \"location\": \"query\"\n      },\n      \"pageToken\": {\n       \"type\": \"string\",\n       \"description\": \"Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"response\": {\n      \"$ref\": \"DnsKeysListResponse\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  },\n  \"managedZoneOperations\": {\n   \"methods\": {\n    \"get\": {\n     \"id\": \"dns.managedZoneOperations.get\",\n     \"path\": \"{project}/managedZones/{managedZone}/operations/{operation}\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Fetch the representation of an existing Operation.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"operation\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the operation addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\",\n      \"operation\"\n     ],\n     \"response\": {\n      \"$ref\": \"Operation\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"list\": {\n     \"id\": \"dns.managedZoneOperations.list\",\n     \"path\": \"{project}/managedZones/{managedZone}/operations\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Enumerate Operations for the given ManagedZone.\",\n     \"parameters\": {\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"maxResults\": {\n       \"type\": \"integer\",\n       \"description\": \"Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.\",\n       \"format\": \"int32\",\n       \"location\": \"query\"\n      },\n      \"pageToken\": {\n       \"type\": \"string\",\n       \"description\": \"Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"sortBy\": {\n       \"type\": \"string\",\n       \"description\": \"Sorting criterion. The only supported values are START_TIME and ID.\",\n       \"default\": \"startTime\",\n       \"enum\": [\n        \"id\",\n        \"startTime\"\n       ],\n       \"enumDescriptions\": [\n        \"\",\n        \"\"\n       ],\n       \"location\": \"query\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"response\": {\n      \"$ref\": \"ManagedZoneOperationsListResponse\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  },\n  \"managedZones\": {\n   \"methods\": {\n    \"create\": {\n     \"id\": \"dns.managedZones.create\",\n     \"path\": \"{project}/managedZones\",\n     \"httpMethod\": \"POST\",\n     \"description\": \"Create a new ManagedZone.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\"\n     ],\n     \"request\": {\n      \"$ref\": \"ManagedZone\"\n     },\n     \"response\": {\n      \"$ref\": \"ManagedZone\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"delete\": {\n     \"id\": \"dns.managedZones.delete\",\n     \"path\": \"{project}/managedZones/{managedZone}\",\n     \"httpMethod\": \"DELETE\",\n     \"description\": \"Delete a previously created ManagedZone.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"get\": {\n     \"id\": \"dns.managedZones.get\",\n     \"path\": \"{project}/managedZones/{managedZone}\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Fetch the representation of an existing ManagedZone.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"response\": {\n      \"$ref\": \"ManagedZone\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"list\": {\n     \"id\": \"dns.managedZones.list\",\n     \"path\": \"{project}/managedZones\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Enumerate ManagedZones that have been created but not yet deleted.\",\n     \"parameters\": {\n      \"dnsName\": {\n       \"type\": \"string\",\n       \"description\": \"Restricts the list to return only zones with this domain name.\",\n       \"location\": \"query\"\n      },\n      \"maxResults\": {\n       \"type\": \"integer\",\n       \"description\": \"Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.\",\n       \"format\": \"int32\",\n       \"location\": \"query\"\n      },\n      \"pageToken\": {\n       \"type\": \"string\",\n       \"description\": \"Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\"\n     ],\n     \"response\": {\n      \"$ref\": \"ManagedZonesListResponse\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"patch\": {\n     \"id\": \"dns.managedZones.patch\",\n     \"path\": \"{project}/managedZones/{managedZone}\",\n     \"httpMethod\": \"PATCH\",\n     \"description\": \"Update an existing ManagedZone. This method supports patch semantics.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"request\": {\n      \"$ref\": \"ManagedZone\"\n     },\n     \"response\": {\n      \"$ref\": \"Operation\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    },\n    \"update\": {\n     \"id\": \"dns.managedZones.update\",\n     \"path\": \"{project}/managedZones/{managedZone}\",\n     \"httpMethod\": \"PUT\",\n     \"description\": \"Update an existing ManagedZone.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"request\": {\n      \"$ref\": \"ManagedZone\"\n     },\n     \"response\": {\n      \"$ref\": \"Operation\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  },\n  \"projects\": {\n   \"methods\": {\n    \"get\": {\n     \"id\": \"dns.projects.get\",\n     \"path\": \"{project}\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Fetch the representation of an existing Project.\",\n     \"parameters\": {\n      \"clientOperationId\": {\n       \"type\": \"string\",\n       \"description\": \"For mutating operation requests only. An optional identifier specified by the client. Must be unique for operation resources in the Operations collection.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\"\n     ],\n     \"response\": {\n      \"$ref\": \"Project\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  },\n  \"resourceRecordSets\": {\n   \"methods\": {\n    \"list\": {\n     \"id\": \"dns.resourceRecordSets.list\",\n     \"path\": \"{project}/managedZones/{managedZone}/rrsets\",\n     \"httpMethod\": \"GET\",\n     \"description\": \"Enumerate ResourceRecordSets that have been created but not yet deleted.\",\n     \"parameters\": {\n      \"managedZone\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the managed zone addressed by this request. Can be the managed zone name or id.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"maxResults\": {\n       \"type\": \"integer\",\n       \"description\": \"Optional. Maximum number of results to be returned. If unspecified, the server will decide how many results to return.\",\n       \"format\": \"int32\",\n       \"location\": \"query\"\n      },\n      \"name\": {\n       \"type\": \"string\",\n       \"description\": \"Restricts the list to return only records with this fully qualified domain name.\",\n       \"location\": \"query\"\n      },\n      \"pageToken\": {\n       \"type\": \"string\",\n       \"description\": \"Optional. A tag returned by a previous list request that was truncated. Use this parameter to continue a previous list request.\",\n       \"location\": \"query\"\n      },\n      \"project\": {\n       \"type\": \"string\",\n       \"description\": \"Identifies the project addressed by this request.\",\n       \"required\": true,\n       \"location\": \"path\"\n      },\n      \"type\": {\n       \"type\": \"string\",\n       \"description\": \"Restricts the list to return only records of this type. If present, the \\\"name\\\" parameter must also be present.\",\n       \"location\": \"query\"\n      }\n     },\n     \"parameterOrder\": [\n      \"project\",\n      \"managedZone\"\n     ],\n     \"response\": {\n      \"$ref\": \"ResourceRecordSetsListResponse\"\n     },\n     \"scopes\": [\n      \"https://www.googleapis.com/auth/cloud-platform\",\n      \"https://www.googleapis.com/auth/cloud-platform.read-only\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readonly\",\n      \"https://www.googleapis.com/auth/ndev.clouddns.readwrite\"\n     ]\n    }\n   }\n  }\n }\n}\n"
  },
  {
    "path": "certbot-dns-google/src/certbot_dns_google/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-linode/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-linode/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-linode/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-linode/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-linode/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_linode/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-linode/README.rst",
    "content": "Linode DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-linode/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-linode/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-linode\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "certbot-dns-linode/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-linode/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-linode documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 10:52:06 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-linode'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-linodedoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-linode.tex', u'certbot-dns-linode Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-linode', u'certbot-dns-linode Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-linode', u'certbot-dns-linode Documentation',\n     author, 'certbot-dns-linode', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-linode/docs/index.rst",
    "content": ".. certbot-dns-linode documentation master file, created by\n   sphinx-quickstart on Wed May 10 10:52:06 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-linode's documentation!\n====================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n.. automodule:: certbot_dns_linode\n   :members:\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-linode/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-linode\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-linode/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-linode\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Linode DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-linode = \"certbot_dns_linode._internal.dns_linode:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-linode/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-linode[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-linode[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-linode[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-linode[docs]\n"
  },
  {
    "path": "certbot-dns-linode/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_linode.dns_linode` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the Linode API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n==========================================  ===================================\n``--dns-linode-credentials``                Linode credentials_ INI file.\n                                            (Required)\n``--dns-linode-propagation-seconds``        The number of seconds to wait for\n                                            DNS to propagate before asking the\n                                            ACME server to verify the DNS\n                                            record.\n                                            (Default: 120 because Linode\n                                            updates its first DNS every 60\n                                            seconds and we allow 60 more seconds\n                                            for the update to reach other 5\n                                            servers)\n==========================================  ===================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing Linode API\ncredentials, obtained from your Linode account's `Applications & API\nTokens page (legacy) <https://manager.linode.com/profile/api>`_ or `Applications\n& API Tokens page (new) <https://cloud.linode.com/profile/tokens>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # Linode API credentials used by Certbot\n   dns_linode_key = 0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ64\n\nThe path to this file can be provided interactively or using the\n``--dns-linode-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   Linode account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-linode \\\\\n     --dns-linode-credentials ~/.secrets/certbot/linode.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-linode \\\\\n     --dns-linode-credentials ~/.secrets/certbot/linode.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 120 seconds\n             for DNS propagation (Linode updates its first DNS every minute\n             and we allow some extra time for the update to reach other 5\n             servers)\n\n   certbot certonly \\\\\n     --dns-linode \\\\\n     --dns-linode-credentials ~/.secrets/certbot/linode.ini \\\\\n     --dns-linode-propagation-seconds 120 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_linode.dns_linode` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/_internal/dns_linode.py",
    "content": "\"\"\"DNS Authenticator for Linode.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nAPI_KEY_URL_V4 = 'https://cloud.linode.com/profile/tokens'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for Linode\n\n    This Authenticator uses the Linode API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using Linode for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('key',\n                                  'API key for Linode account, '\n                                  f'obtained from {API_KEY_URL_V4}',\n                                  'auth_token')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 120) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='Linode credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Linode API.'\n\n    @property\n    def _provider_name(self) -> str:\n        if not hasattr(self, '_credentials'):  # pragma: no cover\n            self._setup_credentials()\n\n        return 'linode4'\n\n    def _setup_credentials(self) -> None:\n        self._credentials = self._configure_credentials(\n            key='credentials',\n            label='Credentials INI file for linode DNS authenticator',\n            required_variables={item[0]: item[1] for item in self._provider_options},\n        )\n\n    def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:\n        if not str(e).startswith('Domain not found'):\n            return errors.PluginError('Unexpected error determining zone identifier '\n                                      f'for {domain_name}: {e}')\n        return None\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-linode tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/_internal/tests/dns_linode_test.py",
    "content": "\"\"\"Tests for certbot_dns_linode._internal.dns_linode.\"\"\"\n\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.tests import util as test_util\nfrom certbot_dns_linode._internal.dns_linode import Authenticator\n\nTOKEN = 'a-token'\nTOKEN_V4 = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = Exception('Domain not found')\n\n    def setUp(self):\n        super().setUp()\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"linode_key\": TOKEN}, path)\n\n        self.config = mock.MagicMock(linode_credentials=path,\n                                     linode_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"linode\")\n\n    # pylint: disable=protected-access\n    def test_api_version_4_detection(self):\n        path = os.path.join(self.tempdir, 'file_4_auto.ini')\n        dns_test_common.write({\"linode_key\": TOKEN_V4}, path)\n\n        config = mock.MagicMock(linode_credentials=path,\n                                linode_propagation_seconds=0)\n        auth = Authenticator(config, \"linode\")\n\n        assert auth._provider_name == \"linode4\"\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-linode/src/certbot_dns_linode/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-luadns/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-luadns/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-luadns/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-luadns/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-luadns/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_luadns/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-luadns/README.rst",
    "content": "LuaDNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-luadns/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-luadns/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-luadns\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "certbot-dns-luadns/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-luadns/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-luadns documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:46:01 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-luadns'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-luadnsdoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-luadns.tex', u'certbot-dns-luadns Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-luadns', u'certbot-dns-luadns Documentation',\n     author, 'certbot-dns-luadns', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-luadns/docs/index.rst",
    "content": ".. certbot-dns-luadns documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:46:01 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-luadns's documentation!\n==============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_luadns\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-luadns/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-luadns\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-luadns/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-luadns\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"LuaDNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-luadns = \"certbot_dns_luadns._internal.dns_luadns:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-luadns/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-luadns[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-luadns[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-luadns[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-luadns[docs]\n"
  },
  {
    "path": "certbot-dns-luadns/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_luadns.dns_luadns` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the LuaDNS API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n=========================================  =====================================\n``--dns-luadns-credentials``               LuaDNS credentials_ INI file.\n                                           (Required)\n``--dns-luadns-propagation-seconds``       The number of seconds to wait for DNS\n                                           to propagate before asking the ACME\n                                           server to verify the DNS record.\n                                           (Default: 30)\n=========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing LuaDNS API\ncredentials, obtained from your LuaDNS\n`account settings page <https://api.luadns.com/settings>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # LuaDNS API credentials used by Certbot\n   dns_luadns_email = user@example.com\n   dns_luadns_token = 0123456789abcdef0123456789abcdef\n\nThe path to this file can be provided interactively or using the\n``--dns-luadns-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   LuaDNS account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-luadns \\\\\n     --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-luadns \\\\\n     --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 2 minutes\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-luadns \\\\\n     --dns-luadns-credentials ~/.secrets/certbot/luadns.ini \\\\\n     --dns-luadns-propagation-seconds 120 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_luadns.dns_luadns` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/_internal/dns_luadns.py",
    "content": "\"\"\"DNS Authenticator for LuaDNS DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nACCOUNT_URL = 'https://api.luadns.com/settings'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for LuaDNS\n\n    This Authenticator uses the LuaDNS API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using LuaDNS for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('email',\n                                  'email address associated with LuaDNS account',\n                                  'auth_username')\n        self._add_provider_option('token',\n                                  f'API token for LuaDNS account, obtained from {ACCOUNT_URL}',\n                                  'auth_token')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 30) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='LuaDNS credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the LuaDNS API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'luadns'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:\n        hint = None\n        if str(e).startswith('401 Client Error: Unauthorized for url:'):\n            hint = 'Are your email and API token values correct?'\n\n        hint_disp = f' ({hint})' if hint else ''\n\n        return errors.PluginError(f'Error determining zone identifier for {domain_name}: '\n                                  f'{e}.{hint_disp}')\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-luadns tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/_internal/tests/dns_luadns_test.py",
    "content": "\"\"\"Tests for certbot_dns_luadns._internal.dns_luadns.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.tests import util as test_util\n\nEMAIL = 'fake@example.com'\nTOKEN = 'foo'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    LOGIN_ERROR = HTTPError(\"401 Client Error: Unauthorized for url: ...\", response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_luadns._internal.dns_luadns import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"luadns_email\": EMAIL, \"luadns_token\": TOKEN}, path)\n\n        self.config = mock.MagicMock(luadns_credentials=path,\n                                     luadns_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"luadns\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-luadns/src/certbot_dns_luadns/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-nsone/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-nsone/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-nsone/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-nsone/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-nsone/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_nsone/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-nsone/README.rst",
    "content": "NS1 DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-nsone/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-nsone/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-nsone\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-nsone/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-nsone/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-nsone documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:30:40 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-nsone'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-nsonedoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-nsone.tex', u'certbot-dns-nsone Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-nsone', u'certbot-dns-nsone Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-nsone', u'certbot-dns-nsone Documentation',\n     author, 'certbot-dns-nsone', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-nsone/docs/index.rst",
    "content": ".. certbot-dns-nsone documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:30:40 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-nsone's documentation!\n=============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_nsone\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-nsone/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-nsone\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-nsone/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-nsone\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"NS1 DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-nsone = \"certbot_dns_nsone._internal.dns_nsone:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-nsone/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-nsone[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-nsone[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-nsone[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-nsone[docs]\n"
  },
  {
    "path": "certbot-dns-nsone/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_nsone.dns_nsone` plugin automates the process of completing\na ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently\nremoving, TXT records using the NS1 API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n========================================  =====================================\n``--dns-nsone-credentials``               NS1 credentials_ INI file.\n                                          (Required)\n``--dns-nsone-propagation-seconds``       The number of seconds to wait for DNS\n                                          to propagate before asking the ACME\n                                          server to verify the DNS record.\n                                          (Default: 30)\n========================================  =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing NS1 API credentials,\nobtained from your NS1\n`account page <https://my.nsone.net/#/account/settings>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # NS1 API credentials used by Certbot\n   dns_nsone_api_key = MDAwMDAwMDAwMDAwMDAw\n\nThe path to this file can be provided interactively or using the\n``--dns-nsone-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   NS1 account. Users who can read this file can use these credentials to issue\n   arbitrary API calls on your behalf. Users who can cause Certbot to run using\n   these credentials can complete a ``dns-01`` challenge to acquire new\n   certificates or revoke existing certificates for associated domains, even if\n   those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-nsone \\\\\n     --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-nsone \\\\\n     --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-nsone \\\\\n     --dns-nsone-credentials ~/.secrets/certbot/nsone.ini \\\\\n     --dns-nsone-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_nsone.dns_nsone` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/_internal/dns_nsone.py",
    "content": "\"\"\"DNS Authenticator for NS1 DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nACCOUNT_URL = 'https://my.nsone.net/#/account/settings'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"\n    DNS Authenticator for NS1\n    This Authenticator uses the NS1 API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using NS1 for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('api-key',\n                                  f'API key for NS1 API, obtained from {ACCOUNT_URL}',\n                                  'auth_token')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 30) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='NS1 credentials file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the NS1 API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'nsone'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:') or\n                                      str(e).startswith(\"400 Client Error: Bad Request for url:\")):\n            return None  # Expected errors when zone name guess is wrong\n        hint = None\n        if str(e).startswith('401 Client Error: Unauthorized for url:'):\n            hint = 'Is your API key correct?'\n\n        hint_disp = f' ({hint})' if hint else ''\n\n        return errors.PluginError(f'Error determining zone identifier: {e}.{hint_disp}')\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-nsone tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/_internal/tests/dns_nsone_test.py",
    "content": "\"\"\"Tests for certbot_dns_nsone._internal.dns_nsone.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_KEY = 'foo'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.', response=Response())\n    LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.', response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_nsone._internal.dns_nsone import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write({\"nsone_api_key\": API_KEY}, path)\n\n        self.config = mock.MagicMock(nsone_credentials=path,\n                                     nsone_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"nsone\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-nsone/src/certbot_dns_nsone/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-ovh/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-ovh/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-ovh/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-ovh/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-ovh/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_ovh/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-ovh/README.rst",
    "content": "OVH DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-ovh/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-ovh/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-ovh\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-ovh/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-ovh/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-ovh documentation build configuration file, created by\n# sphinx-quickstart on Fri Jan 12 10:14:31 2018.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-ovh'\ncopyright = u'2018, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-ovhdoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-ovh.tex', u'certbot-dns-ovh Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-ovh', u'certbot-dns-ovh Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-ovh', u'certbot-dns-ovh Documentation',\n     author, 'certbot-dns-ovh', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-ovh/docs/index.rst",
    "content": ".. certbot-dns-ovh documentation master file, created by\n   sphinx-quickstart on Fri Jan 12 10:14:31 2018.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-ovh's documentation!\n===========================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_ovh\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-ovh/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-ovh\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-ovh/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-ovh\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"OVH DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-ovh = \"certbot_dns_ovh._internal.dns_ovh:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n\n"
  },
  {
    "path": "certbot-dns-ovh/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-ovh[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-ovh[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-ovh[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-ovh[docs]\n"
  },
  {
    "path": "certbot-dns-ovh/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.15.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_ovh.dns_ovh` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the OVH API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n===================================  ==========================================\n``--dns-ovh-credentials``            OVH credentials_ INI file.\n                                     (Required)\n``--dns-ovh-propagation-seconds``    The number of seconds to wait for DNS\n                                     to propagate before asking the ACME\n                                     server to verify the DNS record.\n                                     (Default: 30)\n===================================  ==========================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing OVH API\ncredentials for an account with the following access rules (allowing all domains):\n\n* ``GET /domain/zone/*``\n* ``PUT /domain/zone/*``\n* ``POST /domain/zone/*``\n* ``DELETE /domain/zone/*``\n\nAlternatively, to allow a single domain only, the following access rules apply:\n\n* ``GET /domain/zone/``\n* ``GET /domain/zone/<REQUIRED_DOMAIN>/*``\n* ``PUT /domain/zone/<REQUIRED_DOMAIN>/*``\n* ``POST /domain/zone/<REQUIRED_DOMAIN>/*``\n* ``DELETE /domain/zone/<REQUIRED_DOMAIN>/*``\n\nThese credentials can be obtained at the following links:\n\n* `OVH Europe <https://eu.api.ovh.com/createToken/>`_ (endpoint: ``ovh-eu``)\n* `OVH North America <https://ca.api.ovh.com/createToken/>`_ (endpoint:\n  ``ovh-ca``)\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # OVH API credentials used by Certbot\n   dns_ovh_endpoint = ovh-eu\n   dns_ovh_application_key = MDAwMDAwMDAwMDAw\n   dns_ovh_application_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\n   dns_ovh_consumer_key = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\n\nThe path to this file can be provided interactively or using the\n``--dns-ovh-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   OVH account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire\n   new certificates or revoke existing certificates for associated domains,\n   even if those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-ovh \\\\\n     --dns-ovh-credentials ~/.secrets/certbot/ovh.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-ovh \\\\\n     --dns-ovh-credentials ~/.secrets/certbot/ovh.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-ovh \\\\\n     --dns-ovh-credentials ~/.secrets/certbot/ovh.ini \\\\\n     --dns-ovh-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_ovh.dns_ovh` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/_internal/dns_ovh.py",
    "content": "\"\"\"DNS Authenticator for OVH DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nTOKEN_URL = 'https://eu.api.ovh.com/createToken/ or https://ca.api.ovh.com/createToken/'\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for OVH\n\n    This Authenticator uses the OVH API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using OVH for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('endpoint',\n                                  'OVH API endpoint (ovh-eu or ovh-ca)',\n                                  'auth_entrypoint')\n        self._add_provider_option('application-key',\n                                  f'Application key for OVH API, obtained from {TOKEN_URL}',\n                                  'auth_application_key')\n        self._add_provider_option('application-secret',\n                                  f'Application secret for OVH API, obtained from {TOKEN_URL}',\n                                  'auth_application_secret')\n        self._add_provider_option('consumer-key',\n                                  f'Consumer key for OVH API, obtained from {TOKEN_URL}',\n                                  'auth_consumer_key')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 120) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds)\n        add('credentials', help='OVH credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the OVH API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'ovh'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> errors.PluginError:\n        hint = None\n        if str(e).startswith('400 Client Error:'):\n            hint = 'Is your Application Secret value correct?'\n        if str(e).startswith('403 Client Error:'):\n            hint = 'Are your Application Key and Consumer Key values correct?'\n\n        hint_disp = f' ({hint})' if hint else ''\n\n        return errors.PluginError(f'Error determining zone identifier for {domain_name}: '\n                                  f'{e}.{hint_disp}')\n\n    def _handle_general_error(self, e: Exception, domain_name: str) -> Optional[errors.PluginError]:\n        if domain_name in str(e) and str(e).endswith('not found'):\n            return None\n\n        return super()._handle_general_error(e, domain_name)\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-ovh tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/_internal/tests/dns_ovh_test.py",
    "content": "\"\"\"Tests for certbot_dns_ovh._internal.dns_ovh.\"\"\"\nfrom unittest import mock\nimport sys\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.tests import util as test_util\n\nENDPOINT = 'ovh-eu'\nAPPLICATION_KEY = 'foo'\nAPPLICATION_SECRET = 'bar'\nCONSUMER_KEY = 'spam'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = Exception('Domain example.com not found')\n    LOGIN_ERROR = HTTPError('403 Client Error: Forbidden for url: https://eu.api.ovh.com/1.0/...', response=Response())\n    \n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_ovh._internal.dns_ovh import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        credentials = {\n            \"ovh_endpoint\": ENDPOINT,\n            \"ovh_application_key\": APPLICATION_KEY,\n            \"ovh_application_secret\": APPLICATION_SECRET,\n            \"ovh_consumer_key\": CONSUMER_KEY,\n        }\n        dns_test_common.write(credentials, path)\n\n        self.config = mock.MagicMock(ovh_credentials=path,\n                                     ovh_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, 'ovh')\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-ovh/src/certbot_dns_ovh/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-rfc2136/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-rfc2136/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-rfc2136/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-rfc2136/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-rfc2136/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_rfc2136/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-rfc2136/README.rst",
    "content": "RFC 2136 DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-rfc2136/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-rfc2136/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = python -msphinx\nSPHINXPROJ    = certbot-dns-rfc2136\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-rfc2136/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-rfc2136/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-rfc2136 documentation build configuration file, created by\n# sphinx-quickstart on Thu Jun 15 06:42:51 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-rfc2136'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-rfc2136doc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-rfc2136.tex', u'certbot-dns-rfc2136 Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-rfc2136', u'certbot-dns-rfc2136 Documentation',\n     author, 'certbot-dns-rfc2136', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-rfc2136/docs/index.rst",
    "content": ".. certbot-dns-rfc2136 documentation master file, created by\n   sphinx-quickstart on Thu Jun 15 06:42:51 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-rfc2136's documentation!\n===============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_rfc2136\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-rfc2136/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=python -msphinx\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-rfc2136\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The Sphinx module was not found. Make sure you have Sphinx installed,\r\n\techo.then set the SPHINXBUILD environment variable to point to the full\r\n\techo.path of the 'sphinx-build' executable. Alternatively you may add the\r\n\techo.Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-rfc2136/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-rfc2136\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"RFC 2136 DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-rfc2136 = \"certbot_dns_rfc2136._internal.dns_rfc2136:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-rfc2136/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-rfc2136[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-rfc2136[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-rfc2136[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-rfc2136[docs]\n"
  },
  {
    "path": "certbot-dns-rfc2136/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    # This version was chosen because it is the version packaged in RHEL 9 and Debian unstable. It\n    # is possible this requirement could be relaxed to allow for an even older version of dnspython\n    # if necessary.\n    'dnspython>=2.6.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_rfc2136.dns_rfc2136` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using RFC 2136 Dynamic Updates.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n===================================== =====================================\n``--dns-rfc2136-credentials``         RFC 2136 credentials_ INI file.\n                                      (Required)\n``--dns-rfc2136-propagation-seconds`` The number of seconds to wait for DNS\n                                      to propagate before asking the ACME\n                                      server to verify the DNS record.\n                                      (Default: 60)\n===================================== =====================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing the target DNS\nserver and optional port that supports RFC 2136 Dynamic Updates, the name\nof the TSIG key, the TSIG key secret itself, the algorithm used if it's\ndifferent to HMAC-MD5, and optionally whether to sign the initial SOA query.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # Target DNS server (IPv4 or IPv6 address, not a hostname)\n   dns_rfc2136_server = 192.0.2.1\n   # Target DNS port\n   dns_rfc2136_port = 53\n   # TSIG key name\n   dns_rfc2136_name = keyname.\n   # TSIG key secret\n   dns_rfc2136_secret = 4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \\\nAmKd7ak51vWKgSl12ib86oQRPkpDjg==\n   # TSIG key algorithm\n   dns_rfc2136_algorithm = HMAC-SHA512\n   # TSIG sign SOA query (optional, default: false)\n   dns_rfc2136_sign_query = false\n\n\nThe path to this file can be provided interactively or using the\n``--dns-rfc2136-credentials`` command-line argument. Certbot records the\npath to this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect this TSIG key material as it can be used to potentially\n   add, update, or delete any record in the target DNS server. Users who can\n   read this file can use these credentials to issue arbitrary API calls on\n   your behalf. Users who can cause Certbot to run using these credentials can\n   complete a ``dns-01`` challenge to acquire new certificates or revoke\n   existing certificates for associated domains, even if those domains aren't\n   being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-rfc2136 \\\\\n     --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-rfc2136 \\\\\n     --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 30 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-rfc2136 \\\\\n     --dns-rfc2136-credentials ~/.secrets/certbot/rfc2136.ini \\\\\n     --dns-rfc2136-propagation-seconds 30 \\\\\n     -d example.com\n\n\nSample BIND configuration\n'''''''''''''''''''''''''\n\nHere's a sample BIND configuration for Certbot to use. You will need to\ngenerate a new TSIG key, include it in the BIND configuration and grant it\npermission to issue updates on the target DNS zone.\n\n.. code-block:: bash\n   :caption: Generate a new SHA512 TSIG key\n\n   tsig-keygen -a HMAC-SHA512 keyname.\n\n.. note::\n   Prior to BIND version 9.10.0, you will need to use ``dnssec-keygen`` to generate\n   TSIG keys. Try and use the most secure algorithm supported by your DNS server.\n\n.. code-block:: none\n   :caption: Sample BIND configuration\n\n   key \"keyname.\" {\n     algorithm hmac-sha512;\n     secret \"4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \\\nAmKd7ak51vWKgSl12ib86oQRPkpDjg==\";\n   };\n\n   zone \"example.com.\" IN {\n     type master;\n     file \"named.example.com\";\n     update-policy {\n       grant keyname. name _acme-challenge.example.com. txt;\n     };\n   };\n\n.. note::\n   This configuration limits the scope of the TSIG key to just be able to\n   add and remove TXT records for one specific host for the purpose of\n   completing the ``dns-01`` challenge. If your version of BIND doesn't\n   support the ``update-policy`` directive, then you can use the less-secure\n   ``allow-update`` directive instead. `See the BIND documentation\n   <https://bind9.readthedocs.io/en/latest/reference.html#dynamic-update-policies>`_\n   for details.\n\nSpecial considerations for multiple views in BIND\n'''''''''''''''''''''''''''''''''''''''''''''''''\n\nIf your BIND configuration leverages multiple views, Certbot may fail with an\n``Unable to determine base domain for _acme-challenge.example.com`` error.\nThis error occurs when Certbot isn't able to communicate with an authoritative\nnameserver for the zone, one that answers with the AA (Authoritative Answer) flag\nset in the response.\n\nA common multiple view configuration with two views, external and internal,\ncan cause this error.  If the zone is only present in the external view, and\nthe credentials_ ``dns_rfc2136_server`` setting is set (e.g. 127.0.0.1) so the\nDNS server's ``match-clients`` view option causes the DNS server to route\nCertbot's query to the internal view; the internal view doesn't contain the\nzone, so the response won't have the AA flag set.\n\nOne solution is to logically place the zone into the view Certbot is sending\nqueries to, with an\n`in-view <https://bind9.readthedocs.io/en/latest/reference.html#multiple-views>`_\nzone option.  The zone will be then visible in both zones with exactly the same content.\n\n.. note::\n   Order matters in BIND views: the ``in-view`` zone option must refer to a\n   view defined preceding it.  It cannot refer to a view defined later in the configuration file.\n\n.. code-block:: none\n   :caption: Split-view BIND configuration\n\n   key \"keyname.\" {\n     algorithm hmac-sha512;\n     secret \"4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \\\nAmKd7ak51vWKgSl12ib86oQRPkpDjg==\";\n   };\n\n   // adjust internal-addresses to suit your needs\n   acl internal-address { 127.0.0.0/8; 10.0.0.0/8; 192.168.0.0/16; 172.16.0.0/12; };\n\n   view \"external\" {\n     match-clients { !internal-addresses; any; };\n\n     zone \"example.com.\" IN {\n       type master;\n       file \"named.example.com\";\n       update-policy {\n         grant keyname. name _acme-challenge.example.com. txt;\n       };\n     };\n   };\n\n   view \"internal\" {\n     zone \"example.com.\" IN {\n       in-view external;\n     };\n   };\n\nAnother solution is to add `dns_rfc2136_sign_query = true` to the configuration file and then\nadd the key to the `match-clients` list within the external zone view. All queries signed with\nthis key should then be directed to this view, regardless of source IP.\n\n.. code-block:: none\n   :caption: Split-view BIND configuration with key-based ACLs\n\n   key \"keyname.\" {\n     algorithm hmac-sha512;\n     secret \"4q4wM/2I180UXoMyN4INVhJNi8V9BCV+jMw2mXgZw/CSuxUT8C7NKKFs \\\nAmKd7ak51vWKgSl12ib86oQRPkpDjg==\";\n   };\n\n   // adjust internal-addresses to suit your needs\n   acl internal-address { 127.0.0.0/8; 10.0.0.0/8; 192.168.0.0/16; 172.16.0.0/12; };\n\n   acl certbot-keys { key keyname.; }\n\n   view \"external\" {\n     match-clients { acl certbot-keys; !internal-addresses; any; };\n\n     zone \"example.com.\" IN {\n       type master;\n       file \"named.example.com\";\n       update-policy {\n         grant keyname. name _acme-challenge.example.com. txt;\n       };\n     };\n   };\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_rfc2136.dns_rfc2136` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/_internal/dns_rfc2136.py",
    "content": "\"\"\"DNS Authenticator using RFC 2136 Dynamic Updates.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Optional\n\nimport dns.flags\nimport dns.message\nimport dns.name\nimport dns.query\nimport dns.rdataclass\nimport dns.rdatatype\nimport dns.tsig\nimport dns.tsigkeyring\nimport dns.update\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common\nfrom certbot.plugins.dns_common import CredentialsConfiguration\nfrom certbot.util import is_ipaddress\n\nlogger = logging.getLogger(__name__)\n\nDEFAULT_NETWORK_TIMEOUT = 45\n\n\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator using RFC 2136 Dynamic Updates\n\n    This Authenticator uses RFC 2136 Dynamic Updates to fulfill a dns-01 challenge.\n    \"\"\"\n\n    ALGORITHMS = {\n      'HMAC-MD5': dns.tsig.HMAC_MD5,\n      'HMAC-SHA1': dns.tsig.HMAC_SHA1,\n      'HMAC-SHA224': dns.tsig.HMAC_SHA224,\n      'HMAC-SHA256': dns.tsig.HMAC_SHA256,\n      'HMAC-SHA384': dns.tsig.HMAC_SHA384,\n      'HMAC-SHA512': dns.tsig.HMAC_SHA512\n    }\n\n    PORT = 53\n\n    description = 'Obtain certificates using a DNS TXT record (if you are using BIND for DNS).'\n    ttl = 120\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.credentials: Optional[CredentialsConfiguration] = None\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 60) -> None:\n        super().add_parser_arguments(add, default_propagation_seconds=60)\n        add('credentials', help='RFC 2136 credentials INI file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'RFC 2136 Dynamic Updates.'\n\n    def _validate_credentials(self, credentials: CredentialsConfiguration) -> None:\n        server = cast(str, credentials.conf('server'))\n        if not is_ipaddress(server):\n            raise errors.PluginError(\"The configured target DNS server ({0}) is not a valid IPv4 \"\n                                     \"or IPv6 address. A hostname is not allowed.\".format(server))\n        algorithm = credentials.conf('algorithm')\n        if algorithm:\n            if not self.ALGORITHMS.get(algorithm.upper()):\n                raise errors.PluginError(\"Unknown algorithm: {0}.\".format(algorithm))\n\n    def _setup_credentials(self) -> None:\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'RFC 2136 credentials INI file',\n            {\n                'name': 'TSIG key name',\n                'secret': 'TSIG key secret',\n                'server': 'The target DNS server'\n            },\n            self._validate_credentials\n        )\n\n    def _perform(self, _domain: str, validation_name: str, validation: str) -> None:\n        self._get_rfc2136_client().add_txt_record(validation_name, validation, self.ttl)\n\n    def _cleanup(self, _domain: str, validation_name: str, validation: str) -> None:\n        self._get_rfc2136_client().del_txt_record(validation_name, validation)\n\n    def _get_rfc2136_client(self) -> \"_RFC2136Client\":\n        if not self.credentials:  # pragma: no cover\n            raise errors.Error(\"Plugin has not been prepared.\")\n\n        algorithm: str = (self.credentials.conf('algorithm') or '').upper()\n\n        return _RFC2136Client(cast(str, self.credentials.conf('server')),\n                              int(cast(str, self.credentials.conf('port')) or self.PORT),\n                              cast(str, self.credentials.conf('name')),\n                              cast(str, self.credentials.conf('secret')),\n                              self.ALGORITHMS.get(algorithm, dns.tsig.HMAC_MD5),\n                              (self.credentials.conf('sign_query') or '').upper() == \"TRUE\")\n\n\nclass _RFC2136Client:\n    \"\"\"\n    Encapsulates all communication with the target DNS server.\n    \"\"\"\n    def __init__(self, server: str, port: int, key_name: str, key_secret: str,\n                 key_algorithm: dns.name.Name, sign_query: bool,\n                 timeout: int = DEFAULT_NETWORK_TIMEOUT) -> None:\n        self.server = server\n        self.port = port\n        self.keyring = dns.tsigkeyring.from_text({\n            key_name: key_secret\n        })\n        self.algorithm = key_algorithm\n        self.sign_query = sign_query\n        self._default_timeout = timeout\n\n    def add_txt_record(self, record_name: str, record_content: str, record_ttl: int) -> None:\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.add(rel, record_ttl, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error adding TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully added TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def del_txt_record(self, record_name: str, record_content: str) -> None:\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL (number of seconds that the record may be cached).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DNS server\n        \"\"\"\n\n        domain = self._find_domain(record_name)\n\n        n = dns.name.from_text(record_name)\n        o = dns.name.from_text(domain)\n        rel = n.relativize(o)\n\n        update = dns.update.Update(\n            domain,\n            keyring=self.keyring,\n            keyalgorithm=self.algorithm)\n        update.delete(rel, dns.rdatatype.TXT, record_content)\n\n        try:\n            response = dns.query.tcp(update, self.server, self._default_timeout, self.port)\n        except Exception as e:\n            raise errors.PluginError('Encountered error deleting TXT record: {0}'\n                                     .format(e))\n        rcode = response.rcode()\n\n        if rcode == dns.rcode.NOERROR:\n            logger.debug('Successfully deleted TXT record %s', record_name)\n        else:\n            raise errors.PluginError('Received response from server: {0}'\n                                     .format(dns.rcode.to_text(rcode)))\n\n    def _find_domain(self, record_name: str) -> str:\n        \"\"\"\n        Find the closest domain with an SOA record for a given domain name.\n\n        :param str record_name: The record name for which to find the closest SOA record.\n        :returns: The domain, if found.\n        :rtype: str\n        :raises certbot.errors.PluginError: if no SOA record can be found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(record_name)\n\n        # Loop through until we find an authoritative SOA record\n        for guess in domain_name_guesses:\n            if self._query_soa(guess):\n                return guess\n\n        raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'\n                                 .format(record_name, domain_name_guesses))\n\n    def _query_soa(self, domain_name: str) -> bool:\n        \"\"\"\n        Query a domain name for an authoritative SOA record.\n\n        :param str domain_name: The domain name to query for an SOA record.\n        :returns: True if found, False otherwise.\n        :rtype: bool\n        :raises certbot.errors.PluginError: if no response is received.\n        \"\"\"\n\n        domain = dns.name.from_text(domain_name)\n\n        request = dns.message.make_query(domain, dns.rdatatype.SOA, dns.rdataclass.IN)\n        # Turn off Recursion Desired bit in query\n        request.flags ^= dns.flags.RD\n        # Use our TSIG keyring if configured\n        if self.sign_query:\n            request.use_tsig(self.keyring, algorithm=self.algorithm)\n\n        try:\n            try:\n                response = dns.query.tcp(request, self.server, self._default_timeout, self.port)\n            except (OSError, dns.exception.Timeout) as e:\n                logger.debug('TCP query failed, fallback to UDP: %s', e)\n                response = dns.query.udp(request, self.server, self._default_timeout, self.port)\n            rcode = response.rcode()\n\n            # Authoritative Answer bit should be set\n            if (rcode == dns.rcode.NOERROR\n                    and response.get_rrset(response.answer,\n                                           domain, dns.rdataclass.IN, dns.rdatatype.SOA)\n                    and response.flags & dns.flags.AA):\n                logger.debug('Received authoritative SOA response for %s', domain_name)\n                return True\n\n            logger.debug('No authoritative SOA record found for %s', domain_name)\n            return False\n        except Exception as e:\n            raise errors.PluginError('Encountered error when making query: {0}'\n                                     .format(e))\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-rfc2136 tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/_internal/tests/dns_rfc2136_test.py",
    "content": "\"\"\"Tests for certbot_dns_rfc2136._internal.dns_rfc2136.\"\"\"\n\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport dns.flags\nimport dns.rcode\nimport dns.tsig\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nSERVER = '192.0.2.1'\nPORT = 53\nNAME = 'a-tsig-key.'\nSECRET = 'SSB3b25kZXIgd2hvIHdpbGwgYm90aGVyIHRvIGRlY29kZSB0aGlzIHRleHQK'\nVALID_CONFIG = {\"rfc2136_server\": SERVER, \"rfc2136_name\": NAME, \"rfc2136_secret\": SECRET}\nTIMEOUT = 45\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthenticatorTest):\n\n    def setUp(self):\n        from certbot_dns_rfc2136._internal.dns_rfc2136 import Authenticator\n\n        super().setUp()\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write(VALID_CONFIG, path)\n\n        self.config = mock.MagicMock(rfc2136_credentials=path,\n                                     rfc2136_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"rfc2136\")\n\n        self.mock_client = mock.MagicMock()\n        # _get_rfc2136_client | pylint: disable=protected-access\n        self.orig_get_client = self.auth._get_rfc2136_client\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.auth, '_get_rfc2136_client', mock.MagicMock(return_value=self.mock_client))\n\n    def test_get_client_default_conf_values(self):\n        # algorithm and sign_query are intentionally absent to test that the default (None)\n        # value does not crash Certbot.\n        creds = { \"server\": SERVER, \"port\": PORT, \"name\": NAME, \"secret\": SECRET }\n        self.auth.credentials = mock.MagicMock()\n        self.auth.credentials.conf = lambda key: creds.get(key, None)\n        client = self.orig_get_client()\n        assert client.algorithm == self.auth.ALGORITHMS[\"HMAC-MD5\"]\n        assert client.sign_query is False\n\n    @test_util.patch_display_util()\n    def test_perform(self, unused_mock_get_utility):\n        self.auth.perform([self.achall])\n\n        expected = [mock.call.add_txt_record('_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_cleanup(self):\n        # _attempt_cleanup | pylint: disable=protected-access\n        self.auth._attempt_cleanup = True\n        self.auth.cleanup([self.achall])\n\n        expected = [mock.call.del_txt_record('_acme-challenge.'+DOMAIN, mock.ANY)]\n        assert expected == self.mock_client.mock_calls\n\n    def test_invalid_algorithm_raises(self):\n        config = VALID_CONFIG.copy()\n        config[\"rfc2136_algorithm\"] = \"INVALID\"\n        dns_test_common.write(config, self.config.rfc2136_credentials)\n\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    @test_util.patch_display_util()\n    @mock.patch('certbot_dns_rfc2136._internal.dns_rfc2136._RFC2136Client')\n    def test_valid_algorithm_passes(self, client, unused_mock_get_utility):\n        from certbot_dns_rfc2136._internal.dns_rfc2136 import Authenticator\n\n        config = VALID_CONFIG.copy()\n        config[\"rfc2136_algorithm\"] = \"HMAC-sha512\"\n        dns_test_common.write(config, self.config.rfc2136_credentials)\n        self.auth = Authenticator(self.config, \"rfc2136\")\n\n        self.auth.perform([self.achall])\n        assert dns.tsig.HMAC_SHA512 in client.call_args.args\n\n    def test_invalid_server_raises(self):\n        config = VALID_CONFIG.copy()\n        config[\"rfc2136_server\"] = \"example.com\"\n        dns_test_common.write(config, self.config.rfc2136_credentials)\n\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    @test_util.patch_display_util()\n    def test_valid_server_passes(self, unused_mock_get_utility):\n        config = VALID_CONFIG.copy()\n        dns_test_common.write(config, self.config.rfc2136_credentials)\n\n        self.auth.perform([self.achall])\n\n        config[\"rfc2136_server\"] = \"2001:db8:3333:4444:cccc:dddd:eeee:ffff\"\n        dns_test_common.write(config, self.config.rfc2136_credentials)\n\n        self.auth.perform([self.achall])\n\n\nclass RFC2136ClientTest(unittest.TestCase):\n\n    def setUp(self):\n        from certbot_dns_rfc2136._internal.dns_rfc2136 import _RFC2136Client\n\n        self.rfc2136_client = _RFC2136Client(SERVER, PORT, NAME, SECRET, dns.tsig.HMAC_MD5,\n        False, TIMEOUT)\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_add_txt_record(self, query_mock):\n        query_mock.return_value.rcode.return_value = dns.rcode.NOERROR\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        self.rfc2136_client.add_txt_record(\"bar\", \"baz\", 42)\n\n        query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        assert 'bar. 42 IN TXT \"baz\"' in str(query_mock.call_args[0][0])\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_add_txt_record_wraps_errors(self, query_mock):\n        query_mock.side_effect = Exception\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client.add_txt_record(\"bar\", \"baz\", 42)\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_add_txt_record_server_error(self, query_mock):\n        query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client.add_txt_record(\"bar\", \"baz\", 42)\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_del_txt_record(self, query_mock):\n        query_mock.return_value.rcode.return_value = dns.rcode.NOERROR\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        self.rfc2136_client.del_txt_record(\"bar\", \"baz\")\n\n        query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        assert 'bar. 0 NONE TXT \"baz\"' in str(query_mock.call_args[0][0])\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_del_txt_record_wraps_errors(self, query_mock):\n        query_mock.side_effect = Exception\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client.del_txt_record(\"bar\", \"baz\")\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_del_txt_record_server_error(self, query_mock):\n        query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN\n        # _find_domain | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_find_domain', mock.MagicMock(return_value=\"example.com\"))\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client.del_txt_record(\"bar\", \"baz\")\n\n    def test_find_domain(self):\n        # _query_soa | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_query_soa', mock.MagicMock(side_effect=[False, False, True]))\n\n        # _find_domain | pylint: disable=protected-access\n        domain = self.rfc2136_client._find_domain('foo.bar.'+DOMAIN)\n\n        assert domain == DOMAIN\n\n    def test_find_domain_wraps_errors(self):\n        # _query_soa | pylint: disable=protected-access\n        # workaround for wont-fix https://github.com/python/mypy/issues/2427 that works with\n        # both strict and non-strict mypy\n        setattr(self.rfc2136_client, '_query_soa', mock.MagicMock(return_value=False))\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client._find_domain('foo.bar.'+DOMAIN)\n\n    @mock.patch(\"dns.query.tcp\")\n    @mock.patch(\"dns.message.make_query\")\n    def test_query_soa_found(self, mock_make_query, query_mock):\n        query_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA)\n        query_mock.return_value.rcode.return_value = dns.rcode.NOERROR\n        mock_make_query.return_value = mock.MagicMock()\n\n        # _query_soa | pylint: disable=protected-access\n        result = self.rfc2136_client._query_soa(DOMAIN)\n\n        query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        mock_make_query.return_value.use_tsig.assert_not_called()\n        assert result\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_query_soa_not_found(self, query_mock):\n        query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN\n\n        # _query_soa | pylint: disable=protected-access\n        result = self.rfc2136_client._query_soa(DOMAIN)\n\n        query_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        assert not result\n\n    @mock.patch(\"dns.query.tcp\")\n    def test_query_soa_wraps_errors(self, query_mock):\n        query_mock.side_effect = Exception\n\n        with pytest.raises(errors.PluginError):\n            self.rfc2136_client._query_soa(DOMAIN)\n\n    @mock.patch(\"dns.query.udp\")\n    @mock.patch(\"dns.query.tcp\")\n    def test_query_soa_fallback_to_udp(self, tcp_mock, udp_mock):\n        tcp_mock.side_effect = OSError\n        udp_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA)\n        udp_mock.return_value.rcode.return_value = dns.rcode.NOERROR\n\n        # _query_soa | pylint: disable=protected-access\n        result = self.rfc2136_client._query_soa(DOMAIN)\n\n        tcp_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        udp_mock.assert_called_with(mock.ANY, SERVER, TIMEOUT, PORT)\n        assert result\n\n    @mock.patch(\"dns.query.tcp\")\n    @mock.patch(\"dns.message.make_query\")\n    def test_query_soa_signed(self, mock_make_query, unused_mock_query):\n        mock_make_query.return_value = mock.MagicMock()\n        self.rfc2136_client.sign_query = True\n        self.rfc2136_client.algorithm = dns.tsig.HMAC_MD5\n\n        self.rfc2136_client._query_soa(DOMAIN)\n\n        mock_make_query.return_value.use_tsig.assert_called_with(mock.ANY,\n            algorithm=dns.tsig.HMAC_MD5)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-rfc2136/src/certbot_dns_rfc2136/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-route53/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-route53/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-route53/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-route53/LICENSE.txt",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "certbot-dns-route53/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_route53/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-route53/README.rst",
    "content": "Amazon Web Services Route 53 DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-route53/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-route53/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-route53\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-route53/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-route53/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-route53 documentation build configuration file, created by\n# sphinx-quickstart on Fri Jun  9 11:45:30 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-route53'\ncopyright = u'2017, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-route53doc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-route53.tex', u'certbot-dns-route53 Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-route53', u'certbot-dns-route53 Documentation',\n     author, 'certbot-dns-route53', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-route53/docs/index.rst",
    "content": ".. certbot-dns-route53 documentation master file, created by\n   sphinx-quickstart on Fri Jun  9 11:45:30 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-route53's documentation!\n===============================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. automodule:: certbot_dns_route53\n   :members:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-route53/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-route53\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-route53/examples/sample-aws-policy.json",
    "content": "{\n    \"Version\": \"2012-10-17\",\n    \"Id\": \"certbot-dns-route53 sample policy\",\n    \"Statement\": [\n        {\n            \"Effect\": \"Allow\",\n            \"Action\": [\n                \"route53:ListHostedZones\",\n                \"route53:GetChange\"\n            ],\n            \"Resource\": [\n                \"*\"\n            ]\n        },\n        {\n            \"Effect\" : \"Allow\",\n            \"Action\" : [\n                \"route53:ChangeResourceRecordSets\"\n            ],\n            \"Resource\" : [\n                \"arn:aws:route53:::hostedzone/YOURHOSTEDZONEID\"\n            ],\n            \"Condition\": {\n                \"ForAllValues:StringLike\": {\n                    \"route53:ChangeResourceRecordSetsNormalizedRecordNames\": [\n                        \"_acme-challenge.*\"\n                    ]\n                },\n                \"ForAllValues:StringEquals\": {\n                    \"route53:ChangeResourceRecordSetsRecordTypes\": [\n                        \"TXT\"\n                    ]\n                }\n            }\n        }\n    ]\n}\n"
  },
  {
    "path": "certbot-dns-route53/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-route53\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Route53 DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nkeywords = [\n    \"aws\",\n    \"certbot\",\n    \"route53\",\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\n\"certbot-route53:auth\" = \"certbot_dns_route53._internal.dns_route53:HiddenAuthenticator\"\ndns-route53 = \"certbot_dns_route53._internal.dns_route53:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-route53/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-route53[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-route53[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-route53[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-route53[docs]\n"
  },
  {
    "path": "certbot-dns-route53/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'boto3>=1.20.34',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_route53.dns_route53` plugin automates the process of\ncompleting a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and\nsubsequently removing, TXT records using the Amazon Web Services Route 53 API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nCredentials\n-----------\nUse of this plugin requires a configuration file containing Amazon Web Services\nAPI credentials for an account with the following permissions:\n\n* ``route53:ListHostedZones``\n* ``route53:GetChange``\n* ``route53:ChangeResourceRecordSets``\n\nThese permissions can be captured in an AWS policy like the one below. Amazon\nprovides `information about managing access <https://docs.aws.amazon.com/Route53\n/latest/DeveloperGuide/access-control-overview.html>`_ and `information about\nthe required permissions <https://docs.aws.amazon.com/Route53/latest\n/DeveloperGuide/r53-api-permissions-ref.html>`_\n\n.. code-block:: json\n   :name: sample-aws-policy.json\n   :caption: Example AWS policy file:\n\n   {\n       \"Version\": \"2012-10-17\",\n       \"Id\": \"certbot-dns-route53 sample policy\",\n       \"Statement\": [\n           {\n               \"Effect\": \"Allow\",\n               \"Action\": [\n                   \"route53:ListHostedZones\",\n                   \"route53:GetChange\"\n               ],\n               \"Resource\": [\n                   \"*\"\n               ]\n           },\n           {\n               \"Effect\" : \"Allow\",\n               \"Action\" : [\n                   \"route53:ChangeResourceRecordSets\"\n               ],\n               \"Resource\" : [\n                   \"arn:aws:route53:::hostedzone/YOURHOSTEDZONEID\"\n               ]\n           }\n       ]\n   }\n\nThe `access keys <https://docs.aws.amazon.com/general/latest/gr\n/aws-sec-cred-types.html#access-keys-and-secret-access-keys>`_ for an account\nwith these permissions must be supplied in one of the following ways, which are\ndiscussed in more detail in the Boto3 library's documentation about `configuring\ncredentials <https://boto3.readthedocs.io/en/latest/guide/configuration.html\n#best-practices-for-configuring-credentials>`_.\n\n* Using the ``AWS_ACCESS_KEY_ID`` and ``AWS_SECRET_ACCESS_KEY`` environment\n  variables.\n* Using a credentials configuration file at the default location,\n  ``~/.aws/credentials``. If you're running on sudo, the credentials\n  will be picked up from the root home.\n* Using a credentials configuration file at a path supplied using the\n  ``AWS_CONFIG_FILE`` environment variable.\n\n.. code-block:: ini\n   :name: config.ini\n   :caption: Example credentials config file:\n\n   [default]\n   aws_access_key_id=AKIAIOSFODNN7EXAMPLE\n   aws_secret_access_key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n\n.. caution::\n   You should protect these API credentials as you would a password. Users who\n   can read this file can use these credentials to issue some types of API calls\n   on your behalf, limited by the permissions assigned to the account. Users who\n   can cause Certbot to run using these credentials can complete a ``dns-01``\n   challenge to acquire new certificates or revoke existing certificates for\n   domains these credentials are authorized to manage.\n\n\nExamples\n--------\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-route53 \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-route53 \\\\\n     -d example.com \\\\\n     -d www.example.com\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_route53.dns_route53` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/_internal/dns_route53.py",
    "content": "\"\"\"Certbot Route53 authenticator plugin.\"\"\"\nimport collections\nimport logging\nimport time\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import cast\n\nimport boto3\nfrom botocore.exceptions import ClientError\nfrom botocore.exceptions import NoCredentialsError\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.achallenges import AnnotatedChallenge\nfrom certbot.plugins import common\n\nlogger = logging.getLogger(__name__)\n\nINSTRUCTIONS = (\n    \"To use certbot-dns-route53, configure credentials as described at \"\n    \"https://boto3.readthedocs.io/en/latest/guide/configuration.html#best-practices-for-configuring-credentials \"  # pylint: disable=line-too-long\n    \"and add the necessary permissions for Route53 access.\")\n\n\nclass Authenticator(common.Plugin, interfaces.Authenticator):\n    \"\"\"Route53 Authenticator\n\n    This authenticator solves a DNS01 challenge by uploading the answer to AWS\n    Route53.\n    \"\"\"\n\n    description = (\"Obtain certificates using a DNS TXT record (if you are using AWS Route53 for \"\n                   \"DNS).\")\n    ttl = 10\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self.r53 = boto3.client(\"route53\")\n        self._attempt_cleanup = False\n        self._resource_records: collections.defaultdict[str, list[dict[str, str]]] = \\\n            collections.defaultdict(list)\n\n    def more_info(self) -> str:\n        return \"Solve a DNS01 challenge using AWS Route53\"\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        # This authenticator currently adds no extra arguments.\n        pass\n\n    def auth_hint(self, failed_achalls: list[achallenges.AnnotatedChallenge]) -> str:\n        return (\n            'The Certificate Authority failed to verify the DNS TXT records created by '\n            '--dns-route53. Ensure the above domains have their DNS hosted by AWS Route53.'\n        )\n\n    def prepare(self) -> None:\n        pass\n\n    def get_chall_pref(self, unused_identifier: str) -> Iterable[type[challenges.Challenge]]:\n        return [challenges.DNS01]\n\n    def perform(self, achalls: list[AnnotatedChallenge]) -> list[challenges.ChallengeResponse]:\n        self._attempt_cleanup = True\n\n        try:\n            change_ids = [\n                self._change_txt_record(\"UPSERT\",\n                  achall.validation_domain_name(achall.identifier.value),\n                  achall.validation(achall.account_key))\n                for achall in achalls\n            ]\n\n            for change_id in change_ids:\n                self._wait_for_change(change_id)\n        except (NoCredentialsError, ClientError) as e:\n            logger.debug('Encountered error during perform: %s', e, exc_info=True)\n            raise errors.PluginError(\"\\n\".join([str(e), INSTRUCTIONS]))\n        return [achall.response(achall.account_key) for achall in achalls]\n\n    def cleanup(self, achalls: list[achallenges.AnnotatedChallenge]) -> None:\n        if self._attempt_cleanup:\n            for achall in achalls:\n                domain = achall.identifier.value\n                validation_domain_name = achall.validation_domain_name(domain)\n                validation = achall.validation(achall.account_key)\n\n                self._cleanup(validation_domain_name, validation)\n\n    def _cleanup(self, validation_name: str, validation: str) -> None:\n        try:\n            self._change_txt_record(\"DELETE\", validation_name, validation)\n        except (NoCredentialsError, ClientError) as e:\n            logger.debug('Encountered error during cleanup: %s', e, exc_info=True)\n\n    def _find_zone_id_for_domain(self, domain: str) -> str:\n        \"\"\"Find the zone id responsible a given FQDN.\n\n           That is, the id for the zone whose name is the longest parent of the\n           domain.\n        \"\"\"\n        paginator = self.r53.get_paginator(\"list_hosted_zones\")\n        zones: list[tuple[str, str]] = []\n        target_labels = domain.rstrip(\".\").split(\".\")\n        for page in paginator.paginate():\n            for zone in page[\"HostedZones\"]:\n                if zone[\"Config\"][\"PrivateZone\"]:\n                    continue\n\n                candidate_labels = zone[\"Name\"].rstrip(\".\").split(\".\")\n                if candidate_labels == target_labels[-len(candidate_labels):]:\n                    zones.append((zone[\"Name\"], zone[\"Id\"]))\n\n        if not zones:\n            raise errors.PluginError(\n                \"Unable to find a Route53 hosted zone for {0}\".format(domain)\n            )\n\n        # Order the zones that are suffixes for our desired to domain by\n        # length, this puts them in an order like:\n        # [\"foo.bar.baz.com\", \"bar.baz.com\", \"baz.com\", \"com\"]\n        # And then we choose the first one, which will be the most specific.\n        zones.sort(key=lambda z: len(z[0]), reverse=True)\n        return zones[0][1]\n\n    def _change_txt_record(self, action: str, validation_domain_name: str, validation: str) -> str:\n        zone_id = self._find_zone_id_for_domain(validation_domain_name)\n\n        rrecords = self._resource_records[validation_domain_name]\n        challenge = {\"Value\": '\"{0}\"'.format(validation)}\n        if action == \"DELETE\":\n            # Remove the record being deleted from the list of tracked records\n            rrecords.remove(challenge)\n            if rrecords:\n                # Need to update instead, as we're not deleting the rrset\n                action = \"UPSERT\"\n            else:\n                # Create a new list containing the record to use with DELETE\n                rrecords = [challenge]\n        else:\n            rrecords.append(challenge)\n\n        response = self.r53.change_resource_record_sets(\n            HostedZoneId=zone_id,\n            ChangeBatch={\n                \"Comment\": \"certbot-dns-route53 certificate validation \" + action,\n                \"Changes\": [\n                    {\n                        \"Action\": action,\n                        \"ResourceRecordSet\": {\n                            \"Name\": validation_domain_name,\n                            \"Type\": \"TXT\",\n                            \"TTL\": self.ttl,\n                            \"ResourceRecords\": rrecords,\n                        }\n                    }\n                ]\n            }\n        )\n        return cast(str, response[\"ChangeInfo\"][\"Id\"])\n\n    def _wait_for_change(self, change_id: str) -> None:\n        \"\"\"Wait for a change to be propagated to all Route53 DNS servers.\n           https://docs.aws.amazon.com/Route53/latest/APIReference/API_GetChange.html\n        \"\"\"\n        for unused_n in range(0, 120):\n            response = self.r53.get_change(Id=change_id)\n            if response[\"ChangeInfo\"][\"Status\"] == \"INSYNC\":\n                return\n            time.sleep(5)\n        raise errors.PluginError(\n            \"Timed out waiting for Route53 change. Current status: %s\" %\n            response[\"ChangeInfo\"][\"Status\"])\n\n\n# Our route53 plugin was initially a 3rd party plugin named `certbot-route53:auth` as described at\n# https://github.com/certbot/certbot/issues/4688. This shim exists to allow installations using the\n# old plugin name of `certbot-route53:auth` to continue to work without cluttering things like\n# Certbot's help output with two route53 plugins.\nclass HiddenAuthenticator(Authenticator):\n    \"\"\"A hidden shim around certbot-dns-route53 for backwards compatibility.\"\"\"\n\n    hidden = True\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-route53 tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/_internal/tests/conftest.py",
    "content": "from unittest import mock\n\nimport pytest\n\n@pytest.fixture(autouse=True)\ndef mock_sleep():\n    with mock.patch(\"time.sleep\") as mocked:\n        yield mocked\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/_internal/tests/dns_route53_test.py",
    "content": "\"\"\"Tests for certbot_dns_route53._internal.dns_route53.Authenticator\"\"\"\n\nimport sys\nimport unittest\nfrom unittest import mock\n\nfrom botocore.exceptions import ClientError\nfrom botocore.exceptions import NoCredentialsError\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\n\nKEY = jose.jwk.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass AuthenticatorTest(unittest.TestCase):\n    # pylint: disable=protected-access\n\n    achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n        challb=acme_util.DNS01,\n        identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN),\n        account_key=KEY)\n\n    def setUp(self):\n        from certbot_dns_route53._internal.dns_route53 import Authenticator\n\n        super().setUp()\n\n        self.config = mock.MagicMock()\n\n        # Set up dummy credentials for testing\n        os.environ[\"AWS_ACCESS_KEY_ID\"] = \"dummy_access_key\"\n        os.environ[\"AWS_SECRET_ACCESS_KEY\"] = \"dummy_secret_access_key\"\n\n        self.auth = Authenticator(self.config, \"route53\")\n\n    def tearDown(self):\n        # Remove the dummy credentials from env vars\n        del os.environ[\"AWS_ACCESS_KEY_ID\"]\n        del os.environ[\"AWS_SECRET_ACCESS_KEY\"]\n\n    def test_more_info(self) -> None:\n        self.assertTrue(isinstance(self.auth.more_info(), str))\n\n    def test_get_chall_pref(self) -> None:\n        self.assertEqual(self.auth.get_chall_pref(\"example.org\"), [challenges.DNS01])\n\n    def test_perform(self):\n        self.auth._change_txt_record = mock.MagicMock() # type: ignore[method-assign, unused-ignore]\n        self.auth._wait_for_change = mock.MagicMock() # type: ignore [method-assign, unused-ignore]\n\n        self.auth.perform([self.achall])\n\n        self.auth._change_txt_record.assert_called_once_with(\"UPSERT\",\n                                                             '_acme-challenge.' + DOMAIN,\n                                                             mock.ANY)\n        assert self.auth._wait_for_change.call_count == 1\n\n    def test_perform_no_credentials_error(self):\n        self.auth._change_txt_record = mock.MagicMock( # type: ignore [method-assign, unused-ignore]\n            side_effect=NoCredentialsError)\n\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    def test_perform_client_error(self):\n        self.auth._change_txt_record = mock.MagicMock( # type: ignore [method-assign, unused-ignore]\n            side_effect=ClientError({\"Error\": {\"Code\": \"foo\"}}, \"bar\"))\n\n        with pytest.raises(errors.PluginError):\n            self.auth.perform([self.achall])\n\n    def test_cleanup(self):\n        self.auth._attempt_cleanup = True\n\n        self.auth._change_txt_record = mock.MagicMock() # type: ignore[method-assign, unused-ignore]\n\n        self.auth.cleanup([self.achall])\n\n        self.auth._change_txt_record.assert_called_once_with(\"DELETE\",\n                                                             '_acme-challenge.'+DOMAIN,\n                                                             mock.ANY)\n\n    def test_cleanup_no_credentials_error(self):\n        self.auth._attempt_cleanup = True\n\n        self.auth._change_txt_record = mock.MagicMock( # type: ignore [method-assign, unused-ignore]\n        side_effect=NoCredentialsError)\n\n        self.auth.cleanup([self.achall])\n\n    def test_cleanup_client_error(self):\n        self.auth._attempt_cleanup = True\n\n        self.auth._change_txt_record = mock.MagicMock( # type: ignore [method-assign, unused-ignore]\n            side_effect=ClientError({\"Error\": {\"Code\": \"foo\"}}, \"bar\"))\n\n        self.auth.cleanup([self.achall])\n\n\nclass ClientTest(unittest.TestCase):\n    # pylint: disable=protected-access\n\n    PRIVATE_ZONE = {\n                        \"Id\": \"BAD-PRIVATE\",\n                        \"Name\": \"example.com\",\n                        \"Config\": {\n                            \"PrivateZone\": True\n                        }\n                    }\n\n    EXAMPLE_NET_ZONE = {\n                            \"Id\": \"BAD-WRONG-TLD\",\n                            \"Name\": \"example.net\",\n                            \"Config\": {\n                                \"PrivateZone\": False\n                            }\n                        }\n\n    EXAMPLE_COM_ZONE = {\n                            \"Id\": \"EXAMPLE\",\n                            \"Name\": \"example.com\",\n                            \"Config\": {\n                                \"PrivateZone\": False\n                            }\n                        }\n\n    FOO_EXAMPLE_COM_ZONE = {\n                                \"Id\": \"FOO\",\n                                \"Name\": \"foo.example.com\",\n                                \"Config\": {\n                                    \"PrivateZone\": False\n                                }\n                            }\n\n    def setUp(self):\n        from certbot_dns_route53._internal.dns_route53 import Authenticator\n\n        self.config = mock.MagicMock()\n\n        # Set up dummy credentials for testing\n        os.environ[\"AWS_ACCESS_KEY_ID\"] = \"dummy_access_key\"\n        os.environ[\"AWS_SECRET_ACCESS_KEY\"] = \"dummy_secret_access_key\"\n\n        self.client = Authenticator(self.config, \"route53\")\n\n    def tearDown(self):\n        # Remove the dummy credentials from env vars\n        del os.environ[\"AWS_ACCESS_KEY_ID\"]\n        del os.environ[\"AWS_SECRET_ACCESS_KEY\"]\n\n    def test_find_zone_id_for_domain(self):\n        self.client.r53.get_paginator = mock.MagicMock()\n        self.client.r53.get_paginator().paginate.return_value = [\n            {\n                \"HostedZones\": [\n                    self.EXAMPLE_NET_ZONE,\n                    self.EXAMPLE_COM_ZONE,\n                ]\n            }\n        ]\n\n        result = self.client._find_zone_id_for_domain(\"foo.example.com\")\n        assert result == \"EXAMPLE\"\n\n    def test_find_zone_id_for_domain_pagination(self):\n        self.client.r53.get_paginator = mock.MagicMock()\n        self.client.r53.get_paginator().paginate.return_value = [\n            {\n                \"HostedZones\": [\n                    self.PRIVATE_ZONE,\n                    self.EXAMPLE_COM_ZONE,\n                ]\n            },\n            {\n                \"HostedZones\": [\n                    self.PRIVATE_ZONE,\n                    self.FOO_EXAMPLE_COM_ZONE,\n                ]\n            }\n        ]\n\n        result = self.client._find_zone_id_for_domain(\"foo.example.com\")\n        assert result == \"FOO\"\n\n    def test_find_zone_id_for_domain_no_results(self):\n        self.client.r53.get_paginator = mock.MagicMock()\n        self.client.r53.get_paginator().paginate.return_value = []\n\n        with pytest.raises(errors.PluginError):\n            self.client._find_zone_id_for_domain(\"foo.example.com\")\n\n    def test_find_zone_id_for_domain_no_correct_results(self):\n        self.client.r53.get_paginator = mock.MagicMock()\n        self.client.r53.get_paginator().paginate.return_value = [\n            {\n                \"HostedZones\": [\n                    self.PRIVATE_ZONE,\n                    self.EXAMPLE_NET_ZONE,\n                ]\n            },\n        ]\n\n        with pytest.raises(errors.PluginError):\n            self.client._find_zone_id_for_domain(\"foo.example.com\")\n\n    def test_change_txt_record(self):\n        self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore [method-assign, unused-ignore]\n        self.client.r53.change_resource_record_sets = mock.MagicMock(\n            return_value={\"ChangeInfo\": {\"Id\": 1}})\n\n        self.client._change_txt_record(\"FOO\", DOMAIN, \"foo\")\n\n        call_count = self.client.r53.change_resource_record_sets.call_count\n        assert call_count == 1\n\n    def test_change_txt_record_delete(self):\n        self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore[ method-assign, unused-ignore]\n        self.client.r53.change_resource_record_sets = mock.MagicMock(\n            return_value={\"ChangeInfo\": {\"Id\": 1}})\n\n        validation = \"some-value\"\n        validation_record = {\"Value\": '\"{0}\"'.format(validation)}\n        self.client._resource_records[DOMAIN] = [validation_record]\n\n        self.client._change_txt_record(\"DELETE\", DOMAIN, validation)\n\n        call_count = self.client.r53.change_resource_record_sets.call_count\n        assert call_count == 1\n        call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1]\n        call_args_batch = call_args[\"ChangeBatch\"][\"Changes\"][0]\n        assert call_args_batch[\"Action\"] == \"DELETE\"\n        assert call_args_batch[\"ResourceRecordSet\"][\"ResourceRecords\"] == \\\n            [validation_record]\n\n    def test_change_txt_record_multirecord(self):\n        self.client._find_zone_id_for_domain = mock.MagicMock() # type: ignore [method-assign, unused-ignore]\n        self.client._resource_records[DOMAIN] = [\n            {\"Value\": \"\\\"pre-existing-value\\\"\"},\n            {\"Value\": \"\\\"pre-existing-value-two\\\"\"},\n        ]\n        self.client.r53.change_resource_record_sets = mock.MagicMock(\n            return_value={\"ChangeInfo\": {\"Id\": 1}})\n\n        self.client._change_txt_record(\"DELETE\", DOMAIN, \"pre-existing-value\")\n\n        call_count = self.client.r53.change_resource_record_sets.call_count\n        call_args = self.client.r53.change_resource_record_sets.call_args_list[0][1]\n        call_args_batch = call_args[\"ChangeBatch\"][\"Changes\"][0]\n        assert call_args_batch[\"Action\"] == \"UPSERT\"\n        assert call_args_batch[\"ResourceRecordSet\"][\"ResourceRecords\"] == \\\n            [{\"Value\": \"\\\"pre-existing-value-two\\\"\"}]\n\n        assert call_count == 1\n\n    def test_wait_for_change(self):\n        self.client.r53.get_change = mock.MagicMock(\n            side_effect=[{\"ChangeInfo\": {\"Status\": \"PENDING\"}},\n                         {\"ChangeInfo\": {\"Status\": \"INSYNC\"}}])\n\n        self.client._wait_for_change(\"1\")\n\n        assert self.client.r53.get_change.called\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-route53/src/certbot_dns_route53/py.typed",
    "content": ""
  },
  {
    "path": "certbot-dns-sakuracloud/.readthedocs.yaml",
    "content": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\n\n# Required\nversion: 2\n\n# Set the OS, Python version and other tools you might need\nbuild:\n  os: ubuntu-22.04\n  tools:\n    python: \"3.11\"\n    # You can also specify other tool versions:\n\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n  configuration: certbot-dns-sakuracloud/docs/conf.py\n  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs\n  # builder: \"dirhtml\"\n  # Fail on all warnings to avoid broken references\n  fail_on_warning: true\n\n# Optionally build your docs in additional formats such as PDF and ePub\nformats:\n   - pdf\n   - epub\n\n# Optional but recommended, declare the Python requirements required\n# to build your documentation\n# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html\npython:\n   install:\n   - requirements: certbot-dns-sakuracloud/readthedocs.org.requirements.txt"
  },
  {
    "path": "certbot-dns-sakuracloud/LICENSE.txt",
    "content": "   Copyright 2018 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "certbot-dns-sakuracloud/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include docs *\ninclude src/certbot_dns_sakuracloud/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-dns-sakuracloud/README.rst",
    "content": "Sakura Cloud DNS Authenticator plugin for Certbot\n"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/.gitignore",
    "content": "/_build/\n"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nSPHINXPROJ    = certbot-dns-sakuracloud\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/api.rst",
    "content": "=================\nAPI Documentation\n=================\n\nCertbot plugins implement the Certbot plugins API, and do not otherwise have an external API.\n"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# certbot-dns-sakuracloud documentation build configuration file, created by\n# sphinx-quickstart on Wed May 10 18:30:40 2017.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n#\n# import os\n\n# import sys\n# sys.path.insert(0, os.path.abspath('.'))\n\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\nneeds_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = ['sphinx.ext.autodoc',\n    'sphinx.ext.intersphinx',\n    'sphinx.ext.todo',\n    'sphinx.ext.coverage',\n    'sphinx.ext.viewcode',\n    'sphinx_rtd_theme']\n\nautodoc_member_order = 'bysource'\nautodoc_default_flags = ['show-inheritance']\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = ['_templates']\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\n# source_suffix = ['.rst', '.md']\nsource_suffix = '.rst'\n\n# The master toctree document.\nmaster_doc = 'index'\n\n# General information about the project.\nproject = u'certbot-dns-sakuracloud'\ncopyright = u'2018, Certbot Project'\nauthor = u'Certbot Project'\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = u'0'\n# The full version, including alpha/beta/rc tags.\nrelease = u'0'\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']\n\ndefault_role = 'py:obj'\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = 'sphinx'\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = False\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n#\n\nhtml_theme = 'sphinx_rtd_theme'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\n#html_static_path = ['_static']\n\n\n# -- Options for HTMLHelp output ------------------------------------------\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = 'certbot-dns-sakuraclouddoc'\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    # 'papersize': 'letterpaper',\n\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (master_doc, 'certbot-dns-sakuracloud.tex', u'certbot-dns-sakuracloud Documentation',\n     u'Certbot Project', 'manual'),\n]\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [\n    (master_doc, 'certbot-dns-sakuracloud', u'certbot-dns-sakuracloud Documentation',\n     [author], 1)\n]\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (master_doc, 'certbot-dns-sakuracloud', u'certbot-dns-sakuracloud Documentation',\n     author, 'certbot-dns-sakuracloud', 'One line description of project.',\n     'Miscellaneous'),\n]\n\n\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/', None),\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\n}\n"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/index.rst",
    "content": ".. certbot-dns-sakuracloud documentation master file, created by\n   sphinx-quickstart on Wed May 10 18:30:40 2017.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to certbot-dns-sakuracloud's documentation!\n===================================================\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Contents:\n\n.. toctree::\n   :maxdepth: 1\n\n   api\n\n.. automodule:: certbot_dns_sakuracloud\n   :members:\n\n\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "certbot-dns-sakuracloud/docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\nset SPHINXPROJ=certbot-dns-sakuracloud\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "certbot-dns-sakuracloud/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-dns-sakuracloud\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Sakura Cloud DNS Authenticator plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ndocs = [\n    \"Sphinx>=1.0\", # autodoc_member_order = 'bysource', autodoc_default_flags\n    \"sphinx_rtd_theme\",\n]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\ndns-sakuracloud = \"certbot_dns_sakuracloud._internal.dns_sakuracloud:Authenticator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-dns-sakuracloud/readthedocs.org.requirements.txt",
    "content": "# readthedocs.org gives no way to change the install command to \"pip\n# install -e certbot-dns-sakuracloud[docs]\" (that would in turn install documentation\n# dependencies), but it allows to specify a requirements.txt file at\n# https://readthedocs.org/dashboard/letsencrypt/advanced/ (c.f. #259)\n\n# Although ReadTheDocs certainly doesn't need to install the project\n# in --editable mode (-e), just \"pip install certbot-dns-sakuracloud[docs]\" does not work as\n# expected and \"pip install -e certbot-dns-sakuracloud[docs]\" must be used instead\n\n# We also pin our dependencies for increased stability.\n\n-c ../tools/requirements.txt\n-e acme\n-e certbot\n-e certbot-dns-sakuracloud[docs]\n"
  },
  {
    "path": "certbot-dns-sakuracloud/setup.py",
    "content": "import os\n\nfrom setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    'dns-lexicon>=3.14.1',\n]\n\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\nelse:\n    install_requires.extend([\n        # We specify the minimum acme and certbot version as the current plugin\n        # version for simplicity. See\n        # https://github.com/certbot/certbot/issues/8761 for more info.\n        f'acme>={version}',\n        f'certbot>={version}',\n    ])\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/__init__.py",
    "content": "\"\"\"\nThe `~certbot_dns_sakuracloud.dns_sakuracloud` plugin automates the process of completing\na ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and subsequently\nremoving, TXT records using the Sakura Cloud DNS API.\n\n.. note::\n   The plugin is not installed by default. It can be installed by heading to\n   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, choosing your system and\n   selecting the Wildcard tab.\n\nNamed Arguments\n---------------\n\n==========================================  ======================================\n``--dns-sakuracloud-credentials``           Sakura Cloud credentials_ INI file.\n                                            (Required)\n``--dns-sakuracloud-propagation-seconds``   The number of seconds to wait for DNS\n                                            to propagate before asking the ACME\n                                            server to verify the DNS record.\n                                            (Default: 90)\n==========================================  ======================================\n\n\nCredentials\n-----------\n\nUse of this plugin requires a configuration file containing\nSakura Cloud DNS API credentials, obtained from your Sakura Cloud DNS\n`apikey page <https://secure.sakura.ad.jp/cloud/#!/apikey/top/>`_.\n\n.. code-block:: ini\n   :name: credentials.ini\n   :caption: Example credentials file:\n\n   # Sakura Cloud API credentials used by Certbot\n   dns_sakuracloud_api_token  = 00000000-0000-0000-0000-000000000000\n   dns_sakuracloud_api_secret = MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw\n\nThe path to this file can be provided interactively or using the\n``--dns-sakuracloud-credentials`` command-line argument. Certbot records the path\nto this file for use during renewal, but does not store the file's contents.\n\n.. caution::\n   You should protect these API credentials as you would the password to your\n   Sakura Cloud account. Users who can read this file can use these credentials\n   to issue arbitrary API calls on your behalf. Users who can cause Certbot to\n   run using these credentials can complete a ``dns-01`` challenge to acquire new\n   certificates or revoke existing certificates for associated domains, even if\n   those domains aren't being managed by this server.\n\nCertbot will emit a warning if it detects that the credentials file can be\naccessed by other users on your system. The warning reads \"Unsafe permissions\non credentials configuration file\", followed by the path to the credentials\nfile. This warning will be emitted each time Certbot uses the credentials file,\nincluding for renewal, and cannot be silenced except by addressing the issue\n(e.g., by using a command like ``chmod 600`` to restrict access to the file).\n\n\nExamples\n--------\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``\n\n   certbot certonly \\\\\n     --dns-sakuracloud \\\\\n     --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\\\\n     -d example.com\n\n.. code-block:: bash\n   :caption: To acquire a single certificate for both ``example.com`` and\n             ``www.example.com``\n\n   certbot certonly \\\\\n     --dns-sakuracloud \\\\\n     --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\\\\n     -d example.com \\\\\n     -d www.example.com\n\n.. code-block:: bash\n   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds\n             for DNS propagation\n\n   certbot certonly \\\\\n     --dns-sakuracloud \\\\\n     --dns-sakuracloud-credentials ~/.secrets/certbot/sakuracloud.ini \\\\\n     --dns-sakuracloud-propagation-seconds 60 \\\\\n     -d example.com\n\n\"\"\"\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/_internal/__init__.py",
    "content": "\"\"\"Internal implementation of `~certbot_dns_sakuracloud.dns_sakuracloud` plugin.\"\"\"\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/_internal/dns_sakuracloud.py",
    "content": "\"\"\"DNS Authenticator for Sakura Cloud DNS.\"\"\"\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Optional\n\nfrom requests import HTTPError\n\nfrom certbot import errors\nfrom certbot.plugins import dns_common_lexicon\n\nlogger = logging.getLogger(__name__)\n\nAPIKEY_URL = \"https://secure.sakura.ad.jp/cloud/#!/apikey/top/\"\n\n\nclass Authenticator(dns_common_lexicon.LexiconDNSAuthenticator):\n    \"\"\"DNS Authenticator for Sakura Cloud DNS\n\n    This Authenticator uses the Sakura Cloud API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record ' + \\\n                  '(if you are using Sakura Cloud for DNS).'\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        super().__init__(*args, **kwargs)\n        self._add_provider_option('api-token',\n                                  f'API token for Sakura Cloud API obtained from {APIKEY_URL}',\n                                  'auth_token')\n        self._add_provider_option('api-secret',\n                                  f'API secret for Sakura Cloud API obtained from {APIKEY_URL}',\n                                  'auth_secret')\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None],\n                             default_propagation_seconds: int = 30) -> None:\n        super().add_parser_arguments(\n            add, default_propagation_seconds=90)\n        add('credentials', help='Sakura Cloud credentials file.')\n\n    def more_info(self) -> str:\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the Sakura Cloud API.'\n\n    @property\n    def _provider_name(self) -> str:\n        return 'sakuracloud'\n\n    def _handle_http_error(self, e: HTTPError, domain_name: str) -> Optional[errors.PluginError]:\n        if domain_name in str(e) and (str(e).startswith('404 Client Error: Not Found for url:')):\n            return None  # Expected errors when zone name guess is wrong\n        return super()._handle_http_error(e, domain_name)\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-dns-sakuracloud tests\"\"\"\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/_internal/tests/dns_sakuracloud_test.py",
    "content": "\"\"\"Tests for certbot_dns_sakuracloud._internal.dns_sakuracloud.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\nfrom requests import Response\nfrom requests.exceptions import HTTPError\n\nfrom certbot.compat import os\nfrom certbot.plugins import dns_test_common\nfrom certbot.plugins import dns_test_common_lexicon\nfrom certbot.plugins.dns_test_common import DOMAIN\nfrom certbot.tests import util as test_util\n\nAPI_TOKEN = '00000000-0000-0000-0000-000000000000'\nAPI_SECRET = 'MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw'\n\n\nclass AuthenticatorTest(test_util.TempDirTestCase,\n                        dns_test_common_lexicon.BaseLexiconDNSAuthenticatorTest):\n\n    DOMAIN_NOT_FOUND = HTTPError(f'404 Client Error: Not Found for url: {DOMAIN}.', response=Response())\n    LOGIN_ERROR = HTTPError(f'401 Client Error: Unauthorized for url: {DOMAIN}.', response=Response())\n\n    def setUp(self):\n        super().setUp()\n\n        from certbot_dns_sakuracloud._internal.dns_sakuracloud import Authenticator\n\n        path = os.path.join(self.tempdir, 'file.ini')\n        dns_test_common.write(\n            {\"sakuracloud_api_token\": API_TOKEN, \"sakuracloud_api_secret\": API_SECRET},\n            path\n        )\n\n        self.config = mock.MagicMock(sakuracloud_credentials=path,\n                                     sakuracloud_propagation_seconds=0)  # don't wait during tests\n\n        self.auth = Authenticator(self.config, \"sakuracloud\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-dns-sakuracloud/src/certbot_dns_sakuracloud/py.typed",
    "content": ""
  },
  {
    "path": "certbot-nginx/LICENSE.txt",
    "content": "   Copyright 2015 Electronic Frontier Foundation and others\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n\n   Incorporating code from nginxparser\n   Copyright 2014 Fatih Erikli\n   Licensed MIT\n\n\nText of Apache License\n======================\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\nText of MIT License\n===================\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "certbot-nginx/MANIFEST.in",
    "content": "include LICENSE.txt\ninclude README.rst\nrecursive-include src/certbot_nginx/_internal/tls_configs *.conf\nrecursive-include src/certbot_nginx/_internal/tests/testdata *\ninclude src/certbot_nginx/py.typed\nglobal-exclude __pycache__\nglobal-exclude *.py[cod]\n"
  },
  {
    "path": "certbot-nginx/README.rst",
    "content": "Nginx plugin for Certbot\n"
  },
  {
    "path": "certbot-nginx/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"certbot-nginx\"\ndynamic = [\"version\", \"dependencies\"]\ndescription = \"Nginx plugin for Certbot\"\nreadme = \"README.rst\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Environment :: Plugins\",\n    \"Intended Audience :: System Administrators\",\n    \"Operating System :: POSIX :: Linux\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n    \"Topic :: System :: Installation/Setup\",\n    \"Topic :: System :: Networking\",\n    \"Topic :: System :: Systems Administration\",\n    \"Topic :: Utilities\",\n]\n\n[project.optional-dependencies]\ntest = [\n    \"pytest\",\n]\n\n[project.entry-points.\"certbot.plugins\"]\nnginx = \"certbot_nginx._internal.configurator:NginxConfigurator\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools]\npackage-dir = {\"\" = \"src\"}\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n"
  },
  {
    "path": "certbot-nginx/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\ninstall_requires = [\n    # We specify the minimum acme and certbot version as the current plugin\n    # version for simplicity. See\n    # https://github.com/certbot/certbot/issues/8761 for more info.\n    f'acme>={version}',\n    f'certbot>={version}',\n    # PyOpenSSL>=25.0.0 is just needed to satisfy mypy right now so this dependency can probably be\n    # relaxed to >=24.0.0 if needed.\n    'PyOpenSSL>=25.0.0',\n    'pyparsing>=3.0.0',\n]\n\nsetup(\n    version=version,\n    install_requires=install_requires,\n)\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/__init__.py",
    "content": "\"\"\"Certbot nginx plugin.\"\"\"\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/__init__.py",
    "content": "\"\"\"Certbot nginx plugin internal implementation.\"\"\"\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/configurator.py",
    "content": "# pylint: disable=too-many-lines\n\"\"\"Nginx Configuration\"\"\"\nimport atexit\nfrom contextlib import ExitStack\nimport logging\nimport importlib.resources\nimport re\nimport socket\nimport subprocess\nimport tempfile\nimport time\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\nfrom typing import cast\n\nfrom acme import challenges\nfrom certbot import achallenges\nfrom certbot import errors\nfrom certbot import util\nfrom certbot.compat import os\nfrom certbot.display import util as display_util\nfrom certbot.plugins import common\nfrom certbot_nginx._internal import constants\nfrom certbot_nginx._internal import display_ops\nfrom certbot_nginx._internal import http_01\nfrom certbot_nginx._internal import nginxparser\nfrom certbot_nginx._internal import obj\nfrom certbot_nginx._internal import parser\n\nNAME_RANK = 0\nSTART_WILDCARD_RANK = 1\nEND_WILDCARD_RANK = 2\nREGEX_RANK = 3\nNO_SSL_MODIFIER = 4\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass NginxConfigurator(common.Configurator):\n    \"\"\"Nginx configurator.\n\n    .. todo:: Add proper support for comments in the config. Currently,\n        config files modified by the configurator will lose all their comments.\n\n    :ivar config: Configuration.\n    :type config: certbot.configuration.NamespaceConfig\n\n    :ivar parser: Handles low level parsing\n    :type parser: :class:`~certbot_nginx._internal.parser`\n\n    :ivar str save_notes: Human-readable config change notes\n\n    :ivar reverter: saves and reverts checkpoints\n    :type reverter: :class:`certbot.reverter.Reverter`\n\n    :ivar tup version: version of Nginx\n\n    \"\"\"\n\n    description = \"Nginx Web Server plugin\"\n\n    DEFAULT_LISTEN_PORT = '80'\n\n    # SSL directives that Certbot can add when installing a new certificate.\n    SSL_DIRECTIVES = ['ssl_certificate', 'ssl_certificate_key', 'ssl_dhparam']\n\n    @classmethod\n    def add_parser_arguments(cls, add: Callable[..., None]) -> None:\n        default_server_root = _determine_default_server_root()\n        add(\"server-root\", default=constants.CLI_DEFAULTS[\"server_root\"],\n            help=\"Nginx server root directory. (default: %s)\" % default_server_root)\n        add(\"ctl\", default=constants.CLI_DEFAULTS[\"ctl\"], help=\"Path to the \"\n            \"'nginx' binary, used for 'configtest' and retrieving nginx \"\n            \"version number.\")\n        add(\"sleep-seconds\", default=constants.CLI_DEFAULTS[\"sleep_seconds\"], type=int,\n            help=\"Number of seconds to wait for nginx configuration changes \"\n            \"to apply when reloading.\")\n\n    @property\n    def nginx_conf(self) -> str:\n        \"\"\"Nginx config file path.\"\"\"\n        return os.path.join(self.conf(\"server_root\"), \"nginx.conf\")\n\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Initialize an Nginx Configurator.\n\n        :param tup version: version of Nginx as a tuple (1, 4, 7)\n            (used mostly for unittesting)\n\n        :param tup openssl_version: version of OpenSSL linked to Nginx as a tuple (1, 4, 7)\n            (used mostly for unittesting)\n\n        \"\"\"\n        version = kwargs.pop(\"version\", None)\n        openssl_version = kwargs.pop(\"openssl_version\", None)\n        super().__init__(*args, **kwargs)\n\n        # Files to save\n        self.save_notes = \"\"\n\n        # For creating new vhosts if no names match\n        self.new_vhost: Optional[obj.VirtualHost] = None\n\n        # List of vhosts configured per wildcard domain on this run.\n        # used by deploy_cert() and enhance()\n        self._wildcard_vhosts: dict[str, list[obj.VirtualHost]] = {}\n        self._wildcard_redirect_vhosts: dict[str, list[obj.VirtualHost]] = {}\n\n        # Add number of outstanding challenges\n        self._chall_out = 0\n\n        # These will be set in the prepare function\n        self.version = version\n        self.openssl_version = openssl_version\n        self._enhance_func = {\"redirect\": self._enable_redirect,\n                              \"ensure-http-header\": self._set_http_header,\n                              \"staple-ocsp\": self._enable_ocsp_stapling}\n\n        self.reverter.recovery_routine()\n        self.parser: parser.NginxParser\n\n    @property\n    def mod_ssl_conf_src(self) -> str:\n        \"\"\"Full absolute path to SSL configuration file source.\"\"\"\n\n        # Why all this complexity? Well, we want to support Mozilla's intermediate\n        # recommendations. But TLS1.3 is only supported by newer versions of Nginx.\n        # And as for session tickets, our ideal is to turn them off across the board.\n        # But! Turning them off at all is only supported with new enough versions of\n        # Nginx. And older versions of OpenSSL have a bug that leads to browser errors\n        # given certain configurations. While we'd prefer to have forward secrecy, we'd\n        # rather fail open than error out. Unfortunately, Nginx can be compiled against\n        # many versions of OpenSSL. So we have to check both for the two different features,\n        # leading to four different combinations of options.\n        # For a complete history, check out https://github.com/certbot/certbot/issues/7322\n        #\n        # Technically, nginx >= 1.23.2 has session tickets off by default, and therefore\n        # no longer needs it explicitly set. But since older versions than that are still\n        # around in the oldest non-deprecated rhel, debian, and ubuntu, we will keep explicitly\n        # setting it for now to reduce complexity.\n\n        use_tls13 = self.version >= (1, 13, 0)\n        min_openssl_version = util.parse_loose_version('1.0.2l')\n        session_tix_off = self.version >= (1, 5, 9) and self.openssl_version and\\\n            util.parse_loose_version(self.openssl_version) >= min_openssl_version\n\n        deprecated_conf = True\n\n        if use_tls13:\n            if session_tix_off:\n                # current version\n                config_filename = \"options-ssl-nginx.conf\"\n                deprecated_conf = False\n            else:\n                config_filename = \"options-ssl-nginx-tls13-session-tix-on.conf\"\n        else:\n            if session_tix_off:\n                config_filename = \"options-ssl-nginx-tls12-only.conf\"\n            else:\n                config_filename = \"options-ssl-nginx-old.conf\"\n\n        if deprecated_conf:\n            logger.warning('Certbot has detected that nginx version < 1.13.0 or compiled against '\n                'openssl < 1.0.2l. Since these are deprecated, the configuration file being '\n                'installed at %s will not receive future updates. To get the latest configuration '\n                'version, update nginx.', self.mod_ssl_conf)\n\n        file_manager = ExitStack()\n        atexit.register(file_manager.close)\n        ref = (importlib.resources.files(\"certbot_nginx\").joinpath(\"_internal\")\n               .joinpath(\"tls_configs\").joinpath(config_filename))\n\n        return str(file_manager.enter_context(importlib.resources.as_file(ref)))\n\n    @property\n    def mod_ssl_conf(self) -> str:\n        \"\"\"Full absolute path to SSL configuration file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)\n\n    @property\n    def updated_mod_ssl_conf_digest(self) -> str:\n        \"\"\"Full absolute path to digest of updated SSL configuration file.\"\"\"\n        return os.path.join(self.config.config_dir, constants.UPDATED_MOD_SSL_CONF_DIGEST)\n\n    def install_ssl_options_conf(self, options_ssl: str, options_ssl_digest: str) -> None:\n        \"\"\"Copy Certbot's SSL options file into the system's config dir if required.\"\"\"\n        common.install_version_controlled_file(\n            options_ssl, options_ssl_digest,\n            self.mod_ssl_conf_src, constants.ALL_SSL_OPTIONS_HASHES)\n\n    # This is called in determine_authenticator and determine_installer\n    def prepare(self) -> None:\n        \"\"\"Prepare the authenticator/installer.\n\n        :raises .errors.NoInstallationError: If Nginx ctl cannot be found\n        :raises .errors.MisconfigurationError: If Nginx is misconfigured\n        \"\"\"\n        # Verify Nginx is installed\n        if not util.exe_exists(self.conf('ctl')):\n            raise errors.NoInstallationError(\n                \"Could not find a usable 'nginx' binary. Ensure nginx exists, \"\n                \"the binary is executable, and your PATH is set correctly.\")\n\n        # Make sure configuration is valid\n        self.config_test()\n\n        self.parser = parser.NginxParser(self.conf('server-root'))\n\n        # Set Version\n        if self.version is None:\n            self.version = self.get_version()\n\n        if self.openssl_version is None:\n            self.openssl_version = self._get_openssl_version()\n\n        self.install_ssl_options_conf(self.mod_ssl_conf, self.updated_mod_ssl_conf_digest)\n\n        self.install_ssl_dhparams()\n\n        # Prevent two Nginx plugins from modifying a config at once\n        try:\n            util.lock_dir_until_exit(self.conf('server-root'))\n        except (OSError, errors.LockError):\n            logger.debug('Encountered error:', exc_info=True)\n            raise errors.PluginError('Unable to lock {0}'.format(self.conf('server-root')))\n\n    # Entry point in main.py for installing cert\n    def deploy_cert(self, domain: str, cert_path: str, key_path: str, chain_path: str,\n                    fullchain_path: str) -> None:\n        \"\"\"Deploys certificate to specified virtual host.\n\n        .. note:: Aborts if the vhost is missing ssl_certificate or\n            ssl_certificate_key.\n\n        .. note:: This doesn't save the config files!\n\n        :raises errors.PluginError: When unable to deploy certificate due to\n            a lack of directives or configuration\n\n        \"\"\"\n        if not fullchain_path:\n            raise errors.PluginError(\n                \"The nginx plugin currently requires --fullchain-path to \"\n                \"install a certificate.\")\n\n        vhosts = self.choose_or_make_vhosts(domain, key_path, fullchain_path)\n        for vhost in vhosts:\n            self._deploy_cert(vhost, cert_path, key_path, chain_path, fullchain_path)\n            display_util.notify(\"Successfully deployed certificate for {} to {}\"\n                                .format(domain, vhost.filep))\n\n    def _deploy_cert(self, vhost: obj.VirtualHost, _cert_path: str, key_path: str,\n                     _chain_path: str, fullchain_path: str) -> None:\n        \"\"\"\n        Helper function for deploy_cert() that handles the actual deployment\n        this exists because we might want to do multiple deployments per\n        domain originally passed for deploy_cert(). This is especially true\n        with wildcard certificates\n        \"\"\"\n        cert_directives = [['\\n    ', 'ssl_certificate', ' ', fullchain_path],\n                           ['\\n    ', 'ssl_certificate_key', ' ', key_path]]\n\n        self.parser.update_or_add_server_directives(vhost, cert_directives)\n        logger.info(\"Deploying Certificate to VirtualHost %s\", vhost.filep)\n\n        self.save_notes += (\"Changed vhost at %s with addresses of %s\\n\" %\n                            (vhost.filep,\n                             \", \".join(str(addr) for addr in vhost.addrs)))\n        self.save_notes += \"\\tssl_certificate %s\\n\" % fullchain_path\n        self.save_notes += \"\\tssl_certificate_key %s\\n\" % key_path\n\n    def _choose_vhosts_wildcard(self, domain: str, prefer_ssl: bool,\n                                no_ssl_filter_port: Optional[str] = None) -> list[obj.VirtualHost]:\n        \"\"\"Prompts user to choose vhosts to install a wildcard certificate for\"\"\"\n        if prefer_ssl:\n            vhosts_cache = self._wildcard_vhosts\n            def preference_test(x: obj.VirtualHost) -> bool:\n                return x.ssl\n        else:\n            vhosts_cache = self._wildcard_redirect_vhosts\n            def preference_test(x: obj.VirtualHost) -> bool:\n                return not x.ssl\n\n        # Caching!\n        if domain in vhosts_cache:\n            # Vhosts for a wildcard domain were already selected\n            return vhosts_cache[domain]\n\n        # Get all vhosts whether or not they are covered by the wildcard domain\n        vhosts = self.parser.get_vhosts()\n\n        # Go through the vhosts, making sure that we cover all the names\n        # present, but preferring the SSL or non-SSL vhosts\n        filtered_vhosts = {}\n        for vhost in vhosts:\n            # Ensure we're listening non-sslishly on no_ssl_filter_port\n            if no_ssl_filter_port is not None:\n                if not self._vhost_listening_on_port_no_ssl(vhost, no_ssl_filter_port):\n                    continue\n            for name in vhost.names:\n                if preference_test(vhost):\n                    # Prefer either SSL or non-SSL vhosts\n                    filtered_vhosts[name] = vhost\n                elif name not in filtered_vhosts:\n                    # Add if not in list previously\n                    filtered_vhosts[name] = vhost\n\n        # Only unique VHost objects\n        dialog_input = set(filtered_vhosts.values())\n\n        # Ask the user which of names to enable, expect list of names back\n        return_vhosts = display_ops.select_vhost_multiple(list(dialog_input))\n\n        for vhost in return_vhosts:\n            if domain not in vhosts_cache:\n                vhosts_cache[domain] = []\n            vhosts_cache[domain].append(vhost)\n\n        return return_vhosts\n\n    #######################\n    # Vhost parsing methods\n    #######################\n    def _choose_vhost_single(self, target_name: str) -> list[obj.VirtualHost]:\n        matches = self._get_ranked_matches(target_name)\n        vhosts = [x for x in [self._select_best_name_match(matches)] if x is not None]\n        return vhosts\n\n    def _choose_vhosts_common(self, target_name: str) -> list[obj.VirtualHost]:\n        if util.is_wildcard_domain(target_name):\n            # Ask user which VHosts to support.\n            return self._choose_vhosts_wildcard(target_name, prefer_ssl=True)\n        else:\n            return self._choose_vhost_single(target_name)\n\n    def choose_vhosts(self, target_name: str) -> list[obj.VirtualHost]:\n        \"\"\"Chooses SSL virtual hosts based on the given domain name.\n\n        If no matching SSL vhosts are found, none are created and an empty list\n        is returned.\n\n        :param str target_name: domain name\n\n        :returns: ssl vhosts associated with name\n        :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost`\n\n        \"\"\"\n        return [vhost for vhost in self._choose_vhosts_common(target_name) if vhost.ssl]\n\n    def choose_or_make_vhosts(self, target_name: str, key_path: str,\n                              fullchain_path: str) -> list[obj.VirtualHost]:\n        \"\"\"Chooses or creates SSL virtual hosts based on the given domain name.\n\n        If no matching vhost is found, we attempt to create a new one from the\n        default vhost. If this fails, a MisconfigurationError is raised.\n\n        .. note:: This makes the vhost SSL-enabled if it isn't already. Follows\n            Nginx's server block selection rules preferring blocks that are\n            already SSL.\n\n        :param str target_name: domain name\n        :param str key_path: key to use when creating SSL vhosts\n        :param str fullchain_path: certificates to use when creating SSL vhosts\n\n        :returns: ssl vhosts associated with name\n        :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost`\n\n        \"\"\"\n        vhosts = self._choose_vhosts_common(target_name)\n        if not vhosts:\n            # result will not be [None] because it errors on failure\n            vhosts = [self._vhost_from_duplicated_default(target_name, True,\n                str(self.config.https_port))]\n        for vhost in vhosts:\n            if not vhost.ssl:\n                self._make_server_ssl(vhost, key_path, fullchain_path)\n\n        return vhosts\n\n    def ipv6_info(self, host: str, port: str) -> tuple[bool, bool]:\n        \"\"\"Returns tuple of booleans (ipv6_active, ipv6only_present)\n        ipv6_active is true if any server block listens ipv6 address in any port\n\n        ipv6only_present is true if ipv6only=on option exists in any server\n        block ipv6 listen directive for the specified port.\n\n        :param str host: Host to check ipv6only=on directive for\n        :param str port: Port to check ipv6only=on directive for\n\n        :returns: Tuple containing information if IPv6 is enabled in the global\n            configuration, and existence of ipv6only directive for specified port\n        :rtype: tuple of type (bool, bool)\n        \"\"\"\n        vhosts = self.parser.get_vhosts()\n        ipv6_active = False\n        ipv6only_present = False\n        for vh in vhosts:\n            for addr in vh.addrs:\n                if addr.ipv6:\n                    ipv6_active = True\n                if addr.ipv6only and addr.get_port() == port and addr.get_addr() == host:\n                    ipv6only_present = True\n        return ipv6_active, ipv6only_present\n\n    def _vhost_from_duplicated_default(self, domain: str, allow_port_mismatch: bool,\n                                       port: str) -> obj.VirtualHost:\n        \"\"\"if allow_port_mismatch is False, only server blocks with matching ports will be\n           used as a default server block template.\n        \"\"\"\n        assert self.parser is not None # prepare should already have been called here\n\n        if self.new_vhost is None:\n            default_vhost = self._get_default_vhost(domain, allow_port_mismatch, port)\n            self.new_vhost = self.parser.duplicate_vhost(default_vhost,\n                remove_singleton_listen_params=True)\n            self.new_vhost.names = set()\n\n        self._add_server_name_to_vhost(self.new_vhost, domain)\n        return self.new_vhost\n\n    def _add_server_name_to_vhost(self, vhost: obj.VirtualHost, domain: str) -> None:\n        vhost.names.add(domain)\n        name_block = [['\\n    ', 'server_name']]\n        for name in vhost.names:\n            name_block[0].append(' ')\n            name_block[0].append(name)\n        self.parser.update_or_add_server_directives(vhost, name_block)\n\n    def _get_default_vhost(self, domain: str, allow_port_mismatch: bool,\n                           port: str) -> obj.VirtualHost:\n        \"\"\"Helper method for _vhost_from_duplicated_default; see argument documentation there\"\"\"\n        vhost_list = self.parser.get_vhosts()\n        # if one has default_server set, return that one\n        all_default_vhosts = []\n        port_matching_vhosts = []\n        for vhost in vhost_list:\n            for addr in vhost.addrs:\n                if addr.default:\n                    all_default_vhosts.append(vhost)\n                    if self._port_matches(port, addr.get_port()):\n                        port_matching_vhosts.append(vhost)\n                    break\n\n        if len(port_matching_vhosts) == 1:\n            return port_matching_vhosts[0]\n        elif len(all_default_vhosts) == 1 and allow_port_mismatch:\n            return all_default_vhosts[0]\n\n        # TODO: present a list of vhosts for user to choose from\n\n        raise errors.MisconfigurationError(\"Could not automatically find a matching server \"\n                                           f\"block for {domain}. Set the `server_name` directive \"\n                                           \"to use the Nginx installer.\")\n\n    def _get_ranked_matches(self, target_name: str) -> list[dict[str, Any]]:\n        \"\"\"Returns a ranked list of vhosts that match target_name.\n        The ranking gives preference to SSL vhosts.\n\n        :param str target_name: The name to match\n        :returns: list of dicts containing the vhost, the matching name, and\n            the numerical rank\n        :rtype: list\n\n        \"\"\"\n        vhost_list = self.parser.get_vhosts()\n        return self._rank_matches_by_name_and_ssl(vhost_list, target_name)\n\n    def _select_best_name_match(self,\n                                matches: Sequence[Mapping[str, Any]]) -> Optional[obj.VirtualHost]:\n        \"\"\"Returns the best name match of a ranked list of vhosts.\n\n        :param list matches: list of dicts containing the vhost, the matching name,\n            and the numerical rank\n        :returns: the most matching vhost\n        :rtype: :class:`~certbot_nginx._internal.obj.VirtualHost`\n\n        \"\"\"\n        if not matches:\n            return None\n        elif matches[0]['rank'] in [START_WILDCARD_RANK, END_WILDCARD_RANK,\n            START_WILDCARD_RANK + NO_SSL_MODIFIER, END_WILDCARD_RANK + NO_SSL_MODIFIER]:\n            # Wildcard match - need to find the longest one\n            rank = matches[0]['rank']\n            wildcards = [x for x in matches if x['rank'] == rank]\n            return cast(obj.VirtualHost, max(wildcards, key=lambda x: len(x['name']))['vhost'])\n        # Exact or regex match\n        return cast(obj.VirtualHost, matches[0]['vhost'])\n\n    def _rank_matches_by_name(self, vhost_list: Iterable[obj.VirtualHost],\n                              target_name: str) -> list[dict[str, Any]]:\n        \"\"\"Returns a ranked list of vhosts from vhost_list that match target_name.\n        This method should always be followed by a call to _select_best_name_match.\n\n        :param list vhost_list: list of vhosts to filter and rank\n        :param str target_name: The name to match\n        :returns: list of dicts containing the vhost, the matching name, and\n            the numerical rank\n        :rtype: list\n\n        \"\"\"\n        # Nginx chooses a matching server name for a request with precedence:\n        # 1. exact name match\n        # 2. longest wildcard name starting with *\n        # 3. longest wildcard name ending with *\n        # 4. first matching regex in order of appearance in the file\n        matches = []\n        for vhost in vhost_list:\n            name_type, name = parser.get_best_match(target_name, vhost.names)\n            if name_type == 'exact':\n                matches.append({'vhost': vhost,\n                                'name': name,\n                                'rank': NAME_RANK})\n            elif name_type == 'wildcard_start':\n                matches.append({'vhost': vhost,\n                                'name': name,\n                                'rank': START_WILDCARD_RANK})\n            elif name_type == 'wildcard_end':\n                matches.append({'vhost': vhost,\n                                'name': name,\n                                'rank': END_WILDCARD_RANK})\n            elif name_type == 'regex':\n                matches.append({'vhost': vhost,\n                                'name': name,\n                                'rank': REGEX_RANK})\n        return sorted(matches, key=lambda x: x['rank'])\n\n    def _rank_matches_by_name_and_ssl(self, vhost_list: Iterable[obj.VirtualHost],\n                                      target_name: str) -> list[dict[str, Any]]:\n        \"\"\"Returns a ranked list of vhosts from vhost_list that match target_name.\n        The ranking gives preference to SSLishness before name match level.\n\n        :param list vhost_list: list of vhosts to filter and rank\n        :param str target_name: The name to match\n        :returns: list of dicts containing the vhost, the matching name, and\n            the numerical rank\n        :rtype: list\n\n        \"\"\"\n        matches = self._rank_matches_by_name(vhost_list, target_name)\n        for match in matches:\n            if not match['vhost'].ssl:\n                match['rank'] += NO_SSL_MODIFIER\n        return sorted(matches, key=lambda x: x['rank'])\n\n    def choose_redirect_vhosts(self, target_name: str, port: str) -> list[obj.VirtualHost]:\n        \"\"\"Chooses a single virtual host for redirect enhancement.\n\n        Chooses the vhost most closely matching target_name that is\n        listening to port without using ssl.\n\n        .. todo:: This should maybe return list if no obvious answer\n            is presented.\n\n        .. todo:: The special name \"$hostname\" corresponds to the machine's\n            hostname. Currently we just ignore this.\n\n        :param str target_name: domain name\n        :param str port: port number\n\n        :returns: vhosts associated with name\n        :rtype: list of :class:`~certbot_nginx._internal.obj.VirtualHost`\n\n        \"\"\"\n        if util.is_wildcard_domain(target_name):\n            # Ask user which VHosts to enhance.\n            vhosts = self._choose_vhosts_wildcard(target_name, prefer_ssl=False,\n                no_ssl_filter_port=port)\n        else:\n            matches = self._get_redirect_ranked_matches(target_name, port)\n            vhosts = [x for x in [self._select_best_name_match(matches)]if x is not None]\n        return vhosts\n\n    def choose_auth_vhosts(self, target_name: str) -> tuple[list[obj.VirtualHost],\n                                                            list[obj.VirtualHost]]:\n        \"\"\"Returns a list of HTTP and HTTPS vhosts with a server_name matching target_name.\n\n        If no HTTP vhost exists, one will be cloned from the default vhost. If that fails, no HTTP\n        vhost will be returned.\n\n        :param str target_name: non-wildcard domain name\n\n        :returns: tuple of HTTP and HTTPS virtualhosts\n        :rtype: tuple of :class:`~certbot_nginx._internal.obj.VirtualHost`\n\n        \"\"\"\n        vhosts = [m['vhost'] for m in self._get_ranked_matches(target_name) if m and 'vhost' in m]\n        http_vhosts = [vh for vh in vhosts if\n                       self._vhost_listening(vh, str(self.config.http01_port), False)]\n        https_vhosts = [vh for vh in vhosts if\n                        self._vhost_listening(vh, str(self.config.https_port), True)]\n\n        # If no HTTP vhost matches, try create one from the default_server on http01_port.\n        if not http_vhosts:\n            try:\n                http_vhosts = [self._vhost_from_duplicated_default(target_name, False,\n                                                                   str(self.config.http01_port))]\n            except errors.MisconfigurationError:\n                http_vhosts = []\n\n        return http_vhosts, https_vhosts\n\n    def _port_matches(self, test_port: str, matching_port: str) -> bool:\n        # test_port is a number, matching is a number or \"\" or None\n        if matching_port == \"\" or matching_port is None:\n            # if no port is specified, Nginx defaults to listening on port 80.\n            return test_port == self.DEFAULT_LISTEN_PORT\n        return test_port == matching_port\n\n    def _vhost_listening(self, vhost: obj.VirtualHost, port: str, ssl: bool) -> bool:\n        \"\"\"Tests whether a vhost has an address listening on a port with SSL enabled or disabled.\n\n        :param `obj.VirtualHost` vhost: The vhost whose addresses will be tested\n        :param port str: The port number as a string that the address should be bound to\n        :param bool ssl: Whether SSL should be enabled or disabled on the address\n\n        :returns: Whether the vhost has an address listening on the port and protocol.\n        :rtype: bool\n\n        \"\"\"\n        assert self.parser is not None # prepare should already have been called here\n\n        # if the 'ssl on' directive is present on the vhost, all its addresses have SSL enabled\n        all_addrs_are_ssl = self.parser.has_ssl_on_directive(vhost)\n\n        # if we want ssl vhosts: either 'ssl on' or 'addr.ssl' should be enabled\n        # if we want plaintext vhosts: neither 'ssl on' nor 'addr.ssl' should be enabled\n        def _ssl_matches(addr: obj.Addr) -> bool:\n            return addr.ssl or all_addrs_are_ssl if ssl else \\\n                   not addr.ssl and not all_addrs_are_ssl\n\n        # if there are no listen directives at all, Nginx defaults to\n        # listening on port 80.\n        if not vhost.addrs:\n            return port == self.DEFAULT_LISTEN_PORT and ssl == all_addrs_are_ssl\n\n        return any(self._port_matches(port, addr.get_port()) and _ssl_matches(addr)\n                   for addr in vhost.addrs)\n\n    def _vhost_listening_on_port_no_ssl(self, vhost: obj.VirtualHost, port: str) -> bool:\n        return self._vhost_listening(vhost, port, False)\n\n    def _get_redirect_ranked_matches(self, target_name: str, port: str) -> list[dict[str, Any]]:\n        \"\"\"Gets a ranked list of plaintextish port-listening vhosts matching target_name\n\n        Filter all hosts for those listening on port without using ssl.\n        Rank by how well these match target_name.\n\n        :param str target_name: The name to match\n        :param str port: port number as a string\n        :returns: list of dicts containing the vhost, the matching name, and\n            the numerical rank\n        :rtype: list\n\n        \"\"\"\n        all_vhosts = self.parser.get_vhosts()\n\n        def _vhost_matches(vhost: obj.VirtualHost, port: str) -> bool:\n            return self._vhost_listening_on_port_no_ssl(vhost, port)\n\n        matching_vhosts = [vhost for vhost in all_vhosts if _vhost_matches(vhost, port)]\n\n        return self._rank_matches_by_name(matching_vhosts, target_name)\n\n    def get_all_names(self) -> set[str]:\n        \"\"\"Returns all names found in the Nginx Configuration.\n\n        :returns: All ServerNames, ServerAliases, and reverse DNS entries for\n                  virtual host addresses\n        :rtype: set\n\n        \"\"\"\n        all_names: set[str] = set()\n\n        for vhost in self.parser.get_vhosts():\n            try:\n                vhost.names.remove(\"$hostname\")\n                vhost.names.add(socket.gethostname())\n            except KeyError:\n                pass\n\n            all_names.update(vhost.names)\n\n            for addr in vhost.addrs:\n                host = addr.get_addr()\n                if common.hostname_regex.match(host):\n                    # If it's a hostname, add it to the names.\n                    all_names.add(host)\n                elif not common.private_ips_regex.match(host):\n                    # If it isn't a private IP, do a reverse DNS lookup\n                    try:\n                        if addr.ipv6:\n                            host = addr.get_ipv6_exploded()\n                            socket.inet_pton(socket.AF_INET6, host)\n                        else:\n                            socket.inet_pton(socket.AF_INET, host)\n                        all_names.add(socket.gethostbyaddr(host)[0])\n                    except (OSError, socket.herror, socket.timeout):\n                        continue\n\n        return util.get_filtered_names(all_names)\n\n    def _make_server_ssl(self, vhost: obj.VirtualHost, key_path: str,\n                         fullchain_path: str) -> None:\n        \"\"\"Make a server SSL.\n\n        Make a server SSL by adding new listen and SSL directives.\n\n        :param vhost: The vhost to add SSL to.\n        :type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost`\n        :param str key_path: key to use for SSL\n        :param str fullchain_path: certificates to use for SSL\n\n        \"\"\"\n        https_port = self.config.https_port\n        http_port = self.config.http01_port\n\n        # no addresses should have ssl turned on here\n        assert not vhost.ssl\n\n        addrs_to_insert: list[obj.Addr] = [\n            obj.Addr.fromstring(f'{addr.get_addr()}:{https_port} ssl')\n            for addr in vhost.addrs\n            if addr.get_port() == str(http_port)\n        ]\n\n        # If the vhost was implicitly listening on the default Nginx port,\n        # have it continue to do so.\n        if not vhost.addrs:\n            listen_block = [['\\n    ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]\n            self.parser.add_server_directives(vhost, listen_block)\n\n        if not addrs_to_insert:\n            # there are no existing addresses listening on 80\n            if vhost.ipv6_enabled():\n                addrs_to_insert += [obj.Addr.fromstring(f'[::]:{https_port} ssl')]\n            if vhost.ipv4_enabled():\n                addrs_to_insert += [obj.Addr.fromstring(f'{https_port} ssl')]\n\n        addr_blocks: list[list[str]] = []\n        ipv6only_set_here: set[tuple[str, str]] = set()\n        for addr in addrs_to_insert:\n            host = addr.get_addr()\n            port = addr.get_port()\n            if addr.ipv6:\n                addr_block = ['\\n    ',\n                              'listen',\n                              ' ',\n                              f'{host}:{port}',\n                              ' ',\n                              'ssl']\n                ipv6only_exists = self.ipv6_info(host, port)[1]\n                if not ipv6only_exists and (host, port) not in ipv6only_set_here:\n                    addr.ipv6only = True # bookkeeping in case we switch output implementation\n                    ipv6only_set_here.add((host, port))\n                    addr_block.append(' ')\n                    addr_block.append('ipv6only=on')\n                addr_blocks.append(addr_block)\n            else:\n                tuple_string = f'{host}:{port}' if host else f'{port}'\n                addr_block = ['\\n    ',\n                              'listen',\n                              ' ',\n                              tuple_string,\n                              ' ',\n                              'ssl']\n                addr_blocks.append(addr_block)\n\n        ssl_block = ([\n            *addr_blocks,\n            ['\\n    ', 'ssl_certificate', ' ', fullchain_path],\n            ['\\n    ', 'ssl_certificate_key', ' ', key_path],\n            ['\\n    ', 'include', ' ', self.mod_ssl_conf],\n            ['\\n    ', 'ssl_dhparam', ' ', self.ssl_dhparams],\n        ])\n\n        self.parser.add_server_directives(\n            vhost, ssl_block)\n\n    ##################################\n    # enhancement methods (Installer)\n    ##################################\n    def supported_enhancements(self) -> list[str]:\n        \"\"\"Returns currently supported enhancements.\"\"\"\n        return ['redirect', 'ensure-http-header', 'staple-ocsp']\n\n    def enhance(self, domain: str, enhancement: str,\n                options: Optional[Union[str, list[str]]] = None) -> None:\n        \"\"\"Enhance configuration.\n\n        :param str domain: domain to enhance\n        :param str enhancement: enhancement type defined in\n            :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n        :param options: options for the enhancement\n            See :const:`~certbot.plugins.enhancements.ENHANCEMENTS`\n            documentation for appropriate parameter.\n\n        \"\"\"\n        try:\n            self._enhance_func[enhancement](domain, options)\n        except (KeyError, ValueError):\n            raise errors.PluginError(\n                \"Unsupported enhancement: {0}\".format(enhancement))\n\n    def _has_certbot_redirect(self, vhost: obj.VirtualHost, domain: str) -> bool:\n        test_redirect_block = _test_block_from_block(_redirect_block_for_domain(domain))\n        return vhost.contains_list(test_redirect_block)\n\n    def _set_http_header(self, domain: str, header_substring: Union[str, list[str], None]) -> None:\n        \"\"\"Enables header identified by header_substring on domain.\n\n        If the vhost is listening plaintextishly, separates out the relevant\n        directives into a new server block, and only add header directive to\n        HTTPS block.\n\n        :param str domain: the domain to enable header for.\n        :param str header_substring: String to uniquely identify a header.\n                        e.g. Strict-Transport-Security, Upgrade-Insecure-Requests\n        :returns: Success\n        :raises .errors.PluginError: If no viable HTTPS host can be created or\n            set with header header_substring.\n        \"\"\"\n        if not isinstance(header_substring, str):\n            raise errors.NotSupportedError(\"Invalid header_substring type \"\n                                           f\"{type(header_substring)}, expected a str.\")\n        if header_substring not in constants.HEADER_ARGS:\n            raise errors.NotSupportedError(\n                f\"{header_substring} is not supported by the nginx plugin.\")\n\n        vhosts = self.choose_vhosts(domain)\n        if not vhosts:\n            raise errors.PluginError(\n                \"Unable to find corresponding HTTPS host for enhancement.\")\n        for vhost in vhosts:\n            if vhost.has_header(header_substring):\n                raise errors.PluginEnhancementAlreadyPresent(\n                    \"Existing %s header\" % (header_substring))\n\n            # if there is no separate SSL block, break the block into two and\n            # choose the SSL block.\n            if vhost.ssl and any(not addr.ssl for addr in vhost.addrs):\n                _, vhost = self._split_block(vhost)\n\n            header_directives = [\n                ['\\n    ', 'add_header', ' ', header_substring, ' '] +\n                    constants.HEADER_ARGS[header_substring],\n                ['\\n']]\n            self.parser.add_server_directives(vhost, header_directives)\n\n    def _add_redirect_block(self, vhost: obj.VirtualHost, domain: str) -> None:\n        \"\"\"Add redirect directive to vhost\n        \"\"\"\n        redirect_block = _redirect_block_for_domain(domain)\n\n        self.parser.add_server_directives(\n            vhost, redirect_block, insert_at_top=True)\n\n    def _split_block(self, vhost: obj.VirtualHost, only_directives: Optional[list[str]] = None\n                     ) -> tuple[obj.VirtualHost, obj.VirtualHost]:\n        \"\"\"Splits this \"virtual host\" (i.e. this nginx server block) into\n        separate HTTP and HTTPS blocks.\n\n        :param vhost: The server block to break up into two.\n        :param list only_directives: If this exists, only duplicate these directives\n            when splitting the block.\n        :type vhost: :class:`~certbot_nginx._internal.obj.VirtualHost`\n        :returns: tuple (http_vhost, https_vhost)\n        :rtype: tuple of type :class:`~certbot_nginx._internal.obj.VirtualHost`\n        \"\"\"\n        http_vhost = self.parser.duplicate_vhost(vhost, only_directives=only_directives)\n\n        def _ssl_match_func(directive: str) -> bool:\n            return 'ssl' in directive\n\n        def _ssl_config_match_func(directive: str) -> bool:\n            return self.mod_ssl_conf in directive\n\n        def _no_ssl_match_func(directive: str) -> bool:\n            return 'ssl' not in directive\n\n        # remove all ssl addresses and related directives from the new block\n        for directive in self.SSL_DIRECTIVES:\n            self.parser.remove_server_directives(http_vhost, directive)\n        self.parser.remove_server_directives(http_vhost, 'listen', match_func=_ssl_match_func)\n        self.parser.remove_server_directives(http_vhost, 'include',\n                                             match_func=_ssl_config_match_func)\n\n        # remove all non-ssl addresses from the existing block\n        self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func)\n        return http_vhost, vhost\n\n    def _enable_redirect(self, domain: str,\n                         unused_options: Optional[Union[str, list[str]]]) -> None:\n        \"\"\"Redirect all equivalent HTTP traffic to ssl_vhost.\n\n        If the vhost is listening plaintextishly, separate out the\n        relevant directives into a new server block and add a rewrite directive.\n\n        .. note:: This function saves the configuration\n\n        :param str domain: domain to enable redirect for\n        :param unused_options: Not currently used\n        :type unused_options: Not Available\n        \"\"\"\n\n        port = self.DEFAULT_LISTEN_PORT\n        # If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT,\n        # choose the most name-matching one.\n\n        vhosts = self.choose_redirect_vhosts(domain, port)\n\n        if not vhosts:\n            logger.info(\"No matching insecure server blocks listening on port %s found.\",\n                self.DEFAULT_LISTEN_PORT)\n            return\n\n        for vhost in vhosts:\n            self._enable_redirect_single(domain, vhost)\n\n    def _enable_redirect_single(self, domain: str, vhost: obj.VirtualHost) -> None:\n        \"\"\"Redirect all equivalent HTTP traffic to ssl_vhost.\n\n        If the vhost is listening plaintextishly, separate out the\n        relevant directives into a new server block and add a rewrite directive.\n\n        .. note:: This function saves the configuration\n\n        :param str domain: domain to enable redirect for\n        :param `~obj.Vhost` vhost: vhost to enable redirect for\n        \"\"\"\n        if vhost.ssl:\n            http_vhost, _ = self._split_block(vhost, ['listen', 'server_name'])\n\n            # Add this at the bottom to get the right order of directives\n            return_404_directive = [['\\n    ', 'return', ' ', '404']]\n            self.parser.add_server_directives(http_vhost, return_404_directive)\n\n            vhost = http_vhost\n\n        if self._has_certbot_redirect(vhost, domain):\n            logger.info(\"Traffic on port %s already redirecting to ssl in %s\",\n                self.DEFAULT_LISTEN_PORT, vhost.filep)\n        else:\n            # Redirect plaintextish host to https\n            self._add_redirect_block(vhost, domain)\n            logger.info(\"Redirecting all traffic on port %s to ssl in %s\",\n                self.DEFAULT_LISTEN_PORT, vhost.filep)\n\n    def _enable_ocsp_stapling(self, domain: str,\n                              chain_path: Optional[Union[str, list[str]]]) -> None:\n        \"\"\"Include OCSP response in TLS handshake\n\n        :param str domain: domain to enable OCSP response for\n        :param chain_path: chain file path\n        :type chain_path: `str` or `None`\n\n        \"\"\"\n        if not isinstance(chain_path, str) and chain_path is not None:\n            raise errors.NotSupportedError(f\"Invalid chain_path type {type(chain_path)}, \"\n                                           \"expected a str or None.\")\n        vhosts = self.choose_vhosts(domain)\n        if not vhosts:\n            raise errors.PluginError(\n                \"Unable to find corresponding HTTPS host for OCSP stapling.\")\n        for vhost in vhosts:\n            self._enable_ocsp_stapling_single(vhost, chain_path)\n\n    def _enable_ocsp_stapling_single(self, vhost: obj.VirtualHost,\n                                     chain_path: Optional[str]) -> None:\n        \"\"\"Include OCSP response in TLS handshake\n\n        :param str vhost: vhost to enable OCSP response for\n        :param chain_path: chain file path\n        :type chain_path: `str` or `None`\n\n        \"\"\"\n        if self.version < (1, 3, 7):\n            raise errors.PluginError(\"Version 1.3.7 or greater of nginx \"\n                                     \"is needed to enable OCSP stapling\")\n\n        if chain_path is None:\n            raise errors.PluginError(\n                \"--chain-path is required to enable \"\n                \"Online Certificate Status Protocol (OCSP) stapling \"\n                \"on nginx >= 1.3.7.\")\n\n        stapling_directives = [\n            ['\\n    ', 'ssl_trusted_certificate', ' ', chain_path],\n            ['\\n    ', 'ssl_stapling', ' ', 'on'],\n            ['\\n    ', 'ssl_stapling_verify', ' ', 'on'], ['\\n']]\n\n        try:\n            self.parser.add_server_directives(vhost,\n                                              stapling_directives)\n        except errors.MisconfigurationError as error:\n            logger.debug(str(error))\n            raise errors.PluginError(\"An error occurred while enabling OCSP \"\n                                     \"stapling for {0}.\".format(vhost.names))\n\n        self.save_notes += (\"OCSP Stapling was enabled \"\n                            \"on SSL Vhost: {0}.\\n\".format(vhost.filep))\n        self.save_notes += \"\\tssl_trusted_certificate {0}\\n\".format(chain_path)\n        self.save_notes += \"\\tssl_stapling on\\n\"\n        self.save_notes += \"\\tssl_stapling_verify on\\n\"\n\n    ######################################\n    # Nginx server management (Installer)\n    ######################################\n    def restart(self) -> None:\n        \"\"\"Restarts nginx server.\n\n        :raises .errors.MisconfigurationError: If either the reload fails.\n\n        \"\"\"\n        nginx_restart(self.conf('ctl'), self.nginx_conf, self.conf('sleep-seconds'))\n\n    def config_test(self) -> None:\n        \"\"\"Check the configuration of Nginx for errors.\n\n        :raises .errors.MisconfigurationError: If config_test fails\n\n        \"\"\"\n        try:\n            util.run_script([self.conf('ctl'), \"-c\", self.nginx_conf, \"-t\"])\n        except errors.SubprocessError as err:\n            raise errors.MisconfigurationError(str(err))\n\n    def _nginx_version(self) -> str:\n        \"\"\"Return results of nginx -V\n\n        :returns: version text\n        :rtype: str\n\n        :raises .PluginError:\n            Unable to run Nginx version command\n        \"\"\"\n        try:\n            proc = subprocess.run(\n                [self.conf('ctl'), \"-c\", self.nginx_conf, \"-V\"],\n                stdout=subprocess.PIPE,\n                stderr=subprocess.PIPE,\n                universal_newlines=True,\n                check=False,\n                env=util.env_no_snap_for_external_calls())\n            text = proc.stderr  # nginx prints output to stderr\n        except (OSError, ValueError) as error:\n            logger.debug(str(error), exc_info=True)\n            raise errors.PluginError(\n                \"Unable to run %s -V\" % self.conf('ctl'))\n        return text\n\n    def get_version(self) -> tuple[int, ...]:\n        \"\"\"Return version of Nginx Server.\n\n        Version is returned as tuple. (ie. 2.4.7 = (2, 4, 7))\n\n        :returns: version\n        :rtype: tuple\n\n        :raises .PluginError:\n            Unable to find Nginx version or version is unsupported\n\n        \"\"\"\n        text = self._nginx_version()\n\n        version_regex = re.compile(r\"nginx version: ([^/]+)/([0-9\\.]*)\", re.IGNORECASE)\n        version_matches = version_regex.findall(text)\n\n        sni_regex = re.compile(r\"TLS SNI support enabled\", re.IGNORECASE)\n        sni_matches = sni_regex.findall(text)\n\n        ssl_regex = re.compile(r\" --with-http_ssl_module\")\n        ssl_matches = ssl_regex.findall(text)\n\n        if not version_matches:\n            raise errors.PluginError(\"Unable to find Nginx version\")\n        if not ssl_matches:\n            raise errors.PluginError(\n                \"Nginx build is missing SSL module (--with-http_ssl_module).\")\n        if not sni_matches:\n            raise errors.PluginError(\"Nginx build doesn't support SNI\")\n\n        product_name, product_version = version_matches[0]\n        if product_name != 'nginx':\n            logger.warning(\"NGINX derivative %s is not officially supported by\"\n                           \" certbot\", product_name)\n\n        nginx_version = tuple(int(i) for i in product_version.split(\".\"))\n\n        # nginx < 0.8.48 uses machine hostname as default server_name instead of\n        # the empty string\n        if nginx_version < (0, 8, 48):\n            raise errors.NotSupportedError(\"Nginx version must be 0.8.48+\")\n\n        return nginx_version\n\n    def _get_openssl_version(self) -> str:\n        \"\"\"Return version of OpenSSL linked to Nginx.\n\n        Version is returned as string. If no version can be found, empty string is returned.\n\n        :returns: openssl_version\n        :rtype: str\n\n        :raises .PluginError:\n            Unable to run Nginx version command\n        \"\"\"\n        text = self._nginx_version()\n\n        matches: list[str] = re.findall(r\"running with OpenSSL ([^ ]+) \", text)\n        if not matches:\n            matches = re.findall(r\"built with OpenSSL ([^ ]+) \", text)\n            if not matches:\n                logger.warning(\"NGINX configured with OpenSSL alternatives is not officially\"\n                    \" supported by Certbot.\")\n                return \"\"\n        return matches[0]\n\n    def more_info(self) -> str:\n        \"\"\"Human-readable string to help understand the module\"\"\"\n        return (\n            \"Configures Nginx to authenticate and install HTTPS.{0}\"\n            \"Server root: {root}{0}\"\n            \"Version: {version}\".format(\n                os.linesep, root=self.parser.config_root,\n                version=\".\".join(str(i) for i in self.version))\n        )\n\n    def auth_hint(self,  # pragma: no cover\n                  failed_achalls: Iterable[achallenges.AnnotatedChallenge]) -> str:\n        return (\n            \"The Certificate Authority failed to verify the temporary nginx configuration changes \"\n            \"made by Certbot. Ensure the listed domains point to this nginx server and that it is \"\n            \"accessible from the internet.\"\n        )\n\n    ###################################################\n    # Wrapper functions for Reverter class (Installer)\n    ###################################################\n    def save(self, title: Optional[str] = None, temporary: bool = False) -> None:\n        \"\"\"Saves all changes to the configuration files.\n\n        :param str title: The title of the save. If a title is given, the\n            configuration will be saved as a new checkpoint and put in a\n            timestamped directory.\n\n        :param bool temporary: Indicates whether the changes made will\n            be quickly reversed in the future (ie. challenges)\n\n        :raises .errors.PluginError: If there was an error in\n            an attempt to save the configuration, or an error creating a\n            checkpoint\n\n        \"\"\"\n        save_files = set(self.parser.parsed.keys())\n        self.add_to_checkpoint(save_files, self.save_notes, temporary)\n        self.save_notes = \"\"\n\n        # Change 'ext' to something else to not override existing conf files\n        self.parser.filedump(ext='')\n        if title and not temporary:\n            self.finalize_checkpoint(title)\n\n    def recovery_routine(self) -> None:\n        \"\"\"Revert all previously modified files.\n\n        Reverts all modified files that have not been saved as a checkpoint\n\n        :raises .errors.PluginError: If unable to recover the configuration\n\n        \"\"\"\n        super().recovery_routine()\n        self.new_vhost = None\n        self.parser.load()\n\n    def revert_challenge_config(self) -> None:\n        \"\"\"Used to cleanup challenge configurations.\n\n        :raises .errors.PluginError: If unable to revert the challenge config.\n\n        \"\"\"\n        self.revert_temporary_config()\n        self.new_vhost = None\n        self.parser.load()\n\n    def rollback_checkpoints(self, rollback: int = 1) -> None:\n        \"\"\"Rollback saved checkpoints.\n\n        :param int rollback: Number of checkpoints to revert\n\n        :raises .errors.PluginError: If there is a problem with the input or\n            the function is unable to correctly revert the configuration\n\n        \"\"\"\n        super().rollback_checkpoints(rollback)\n        self.new_vhost = None\n        self.parser.load()\n\n    ###########################################################################\n    # Challenges Section for Authenticator\n    ###########################################################################\n    def get_chall_pref(self, unused_identifier: str) -> list[type[challenges.Challenge]]:\n        \"\"\"Return list of challenge preferences.\"\"\"\n        return [challenges.HTTP01]\n\n    # Entry point in main.py for performing challenges\n    def perform(self, achalls: list[achallenges.AnnotatedChallenge]\n                ) -> list[challenges.ChallengeResponse]:\n        \"\"\"Perform the configuration related challenge.\n\n        This function currently assumes all challenges will be fulfilled.\n        If this turns out not to be the case in the future. Cleanup and\n        outstanding challenges will have to be designed better.\n\n        \"\"\"\n        self._chall_out += len(achalls)\n        responses: list[Optional[challenges.ChallengeResponse]] = [None] * len(achalls)\n        http_doer = http_01.NginxHttp01(self)\n\n        for i, achall in enumerate(achalls):\n            # Currently also have chall_doer hold associated index of the\n            # challenge. This helps to put all of the responses back together\n            # when they are all complete.\n            if not isinstance(achall, achallenges.KeyAuthorizationAnnotatedChallenge):\n                raise errors.Error(\"Challenge should be an instance \"\n                                   \"of KeyAuthorizationAnnotatedChallenge\")\n            http_doer.add_chall(achall, i)\n\n        http_response = http_doer.perform()\n        # Must restart in order to activate the challenges.\n        # Handled here because we may be able to load up other challenge types\n        self.restart()\n\n        # Go through all of the challenges and assign them to the proper place\n        # in the responses return value. All responses must be in the same order\n        # as the original challenges.\n        for i, resp in enumerate(http_response):\n            responses[http_doer.indices[i]] = resp\n\n        return [response for response in responses if response]\n\n    # called after challenges are performed\n    def cleanup(self, achalls: list[achallenges.AnnotatedChallenge]) -> None:\n        \"\"\"Revert all challenges.\"\"\"\n        self._chall_out -= len(achalls)\n\n        # If all of the challenges have been finished, clean up everything\n        if self._chall_out <= 0:\n            self.revert_challenge_config()\n            self.restart()\n\n\ndef _test_block_from_block(block: list[Any]) -> list[Any]:\n    test_block = nginxparser.UnspacedList(block)\n    parser.comment_directive(test_block, 0)\n    return test_block[:-1]\n\n\ndef _redirect_block_for_domain(domain: str) -> list[Any]:\n    updated_domain = domain\n    match_symbol = '='\n    if util.is_wildcard_domain(domain):\n        match_symbol = '~'\n        updated_domain = updated_domain.replace('.', r'\\.')\n        updated_domain = updated_domain.replace('*', '[^.]+')\n        updated_domain = '^' + updated_domain + '$'\n    redirect_block = [[\n        ['\\n    ', 'if', ' ', '($host', ' ', match_symbol, ' ', '%s)' % updated_domain, ' '],\n        [['\\n        ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],\n        '\\n    ']],\n        ['\\n']]\n    return redirect_block\n\n\ndef nginx_restart(nginx_ctl: str, nginx_conf: str, sleep_duration: int) -> None:\n    \"\"\"Restarts the Nginx Server.\n\n    .. todo:: Nginx restart is fatal if the configuration references\n        non-existent SSL cert/key files. Remove references to /etc/letsencrypt\n        before restart.\n\n    :param str nginx_ctl: Path to the Nginx binary.\n    :param str nginx_conf: Path to the Nginx configuration file.\n    :param int sleep_duration: How long to sleep after sending the reload signal.\n\n    \"\"\"\n    try:\n        reload_output: str = \"\"\n        with tempfile.TemporaryFile() as out:\n            proc = subprocess.run([nginx_ctl, \"-c\", nginx_conf, \"-s\", \"reload\"],\n                                  env=util.env_no_snap_for_external_calls(),\n                                  stdout=out, stderr=out, check=False)\n            out.seek(0)\n            reload_output = out.read().decode(\"utf-8\")\n\n        if proc.returncode != 0:\n            logger.debug(\"nginx reload failed:\\n%s\", reload_output)\n            # Maybe Nginx isn't running - try start it\n            # Write to temporary files instead of piping because of communication issues on Arch\n            # https://github.com/certbot/certbot/issues/4324\n            with tempfile.TemporaryFile() as out:\n                nginx_proc = subprocess.run([nginx_ctl, \"-c\", nginx_conf],\n                    stdout=out, stderr=out, env=util.env_no_snap_for_external_calls(), check=False)\n                if nginx_proc.returncode != 0:\n                    out.seek(0)\n                    # Enter recovery routine...\n                    raise errors.MisconfigurationError(\n                        \"nginx restart failed:\\n%s\" % out.read().decode(\"utf-8\"))\n\n    except (OSError, ValueError):\n        raise errors.MisconfigurationError(\"nginx restart failed\")\n    # Nginx can take a significant duration of time to fully apply a new config, depending\n    # on size and contents (https://github.com/certbot/certbot/issues/7422). Lacking a way\n    # to reliably identify when this process is complete, we provide the user with control\n    # over how long Certbot will sleep after reloading the configuration.\n    if sleep_duration > 0:\n        time.sleep(sleep_duration)\n\n\ndef _determine_default_server_root() -> str:\n    if os.environ.get(\"CERTBOT_DOCS\") == \"1\":\n        default_server_root = (f\"{constants.LINUX_SERVER_ROOT} \"\n                               f\"or {constants.FREEBSD_DARWIN_SERVER_ROOT}\")\n    else:\n        default_server_root = constants.CLI_DEFAULTS[\"server_root\"]\n    return default_server_root\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/constants.py",
    "content": "\"\"\"nginx plugin constants.\"\"\"\nimport platform\nfrom typing import Any\n\nFREEBSD_DARWIN_SERVER_ROOT = \"/usr/local/etc/nginx\"\nLINUX_SERVER_ROOT = \"/etc/nginx\"\nPKGSRC_SERVER_ROOT = \"/usr/pkg/etc/nginx\"\n\nif platform.system() in ('FreeBSD', 'Darwin'):\n    server_root_tmp = FREEBSD_DARWIN_SERVER_ROOT\nelif platform.system() in ('NetBSD',):\n    server_root_tmp = PKGSRC_SERVER_ROOT\nelse:\n    server_root_tmp = LINUX_SERVER_ROOT\n\nCLI_DEFAULTS: dict[str, Any] = {\n    \"server_root\": server_root_tmp,\n    \"ctl\": \"nginx\",\n    \"sleep_seconds\": 1\n}\n\"\"\"CLI defaults.\"\"\"\n\n\nMOD_SSL_CONF_DEST = \"options-ssl-nginx.conf\"\n\"\"\"Name of the mod_ssl config file as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nUPDATED_MOD_SSL_CONF_DIGEST = \".updated-options-ssl-nginx-conf-digest.txt\"\n\"\"\"Name of the hash of the updated or informed mod_ssl_conf as saved\nin `certbot.configuration.NamespaceConfig.config_dir`.\"\"\"\n\nALL_SSL_OPTIONS_HASHES = [\n    '0f81093a1465e3d4eaa8b0c14e77b2a2e93568b0fc1351c2b87893a95f0de87c',\n    '9a7b32c49001fed4cff8ad24353329472a50e86ade1ef9b2b9e43566a619612e',\n    'a6d9f1c7d6b36749b52ba061fff1421f9a0a3d2cfdafbd63c05d06f65b990937',\n    '7f95624dd95cf5afc708b9f967ee83a24b8025dc7c8d9df2b556bbc64256b3ff',\n    '394732f2bbe3e5e637c3fb5c6e980a1f1b90b01e2e8d6b7cff41dde16e2a756d',\n    '4b16fec2bcbcd8a2f3296d886f17f9953ffdcc0af54582452ca1e52f5f776f16',\n    'c052ffff0ad683f43bffe105f7c606b339536163490930e2632a335c8d191cc4',\n    '02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426',\n    '63e2bddebb174a05c9d8a7cf2adf72f7af04349ba59a1a925fe447f73b2f1abf',\n    '2901debc7ecbc10917edd9084c05464c9c5930b463677571eaf8c94bffd11ae2',\n    '30baca73ed9a5b0e9a69ea40e30482241d8b1a7343aa79b49dc5d7db0bf53b6c',\n    '02329eb19930af73c54b3632b3165d84571383b8c8c73361df940cb3894dd426',\n    '108c4555058a087496a3893aea5d9e1cee0f20a3085d44a52dc1a66522299ac3',\n    'd5e021706ecdccc7090111b0ae9a29ef61523e927f020e410caf0a1fd7063981',\n    'ef11e3fb17213e74d3e1816cde0ec37b8b95b4167cf21e7b8ff1eaa9c6f918ee',\n    'af85f6193808a44789a1d293e6cffa249cad9a21135940800958b8e3c72dbc69',\n    'a2a612fd21b02abaa32d9d11ac63d987d6e3054dbfa356de5800eea0d7ce17f3',\n    '2d9648302e3588a172c318e46bff88ade46fc7a16d6afc85322776a04800d473',\n    '5e21cc66989f26ec46116d979421e538131cf8ab33ffff3f682fbfe491b0ace8',\n    'f5615544105c4eee44f02a604e3e9ae55b3d5bad247160bb18731a0ac531af02',\n    '05a799c4db12f8e15e68219c98056824cbd5ae7b05863225318ae112f343880b',\n    'dc81acfd9670f137d5abbccfe3438d9306d4b6a906439b0fbf6a6756272e7cc7',\n    '0175f71721dd8e5315a6d0f3efef703ff54e867d1ab2a4e076791b89a0b3511a',\n    '246b520bedc461fcbd35f4d3efdd75ebf171baccaba5c38f488009566de6d5b3',\n    'dd72286f760c90550f34fbeeceb5a1f1351b09b812e65a18569a0f4a4d7f5847',\n]\n\"\"\"SHA256 hashes of the contents of all versions of MOD_SSL_CONF_SRC\"\"\"\n\n\ndef os_constant(key: str) -> Any:\n    # XXX TODO: In the future, this could return different constants\n    #           based on what OS we are running under.  To see an\n    #           approach to how to handle different OSes, see the\n    #           apache version of this file.  Currently, we do not\n    #           actually have any OS-specific constants on Nginx.\n    \"\"\"\n    Get a constant value for operating system\n\n    :param str key: name of cli constant\n    :return: value of constant for active os\n    \"\"\"\n    return CLI_DEFAULTS[key]\n\n\nHSTS_ARGS = ['\\\"max-age=31536000\\\"', ' ', 'always']\n\nHEADER_ARGS = {'Strict-Transport-Security': HSTS_ARGS}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/display_ops.py",
    "content": "\"\"\"Contains UI methods for Nginx operations.\"\"\"\nimport logging\nfrom typing import Iterable\nfrom typing import Optional\n\nfrom certbot.display import util as display_util\nfrom certbot_nginx._internal.obj import VirtualHost\n\nlogger = logging.getLogger(__name__)\n\n\ndef select_vhost_multiple(vhosts: Optional[Iterable[VirtualHost]]) -> list[VirtualHost]:\n    \"\"\"Select multiple Vhosts to install the certificate for\n    :param vhosts: Available Nginx VirtualHosts\n    :type vhosts: :class:`list` of type `~obj.Vhost`\n    :returns: List of VirtualHosts\n    :rtype: :class:`list`of type `~obj.Vhost`\n    \"\"\"\n    if not vhosts:\n        return []\n    tags_list = [vhost.display_repr()+\"\\n\" for vhost in vhosts]\n    # Remove the extra newline from the last entry\n    if tags_list:\n        tags_list[-1] = tags_list[-1][:-1]\n    code, names = display_util.checklist(\n        \"Which server blocks would you like to modify?\",\n        tags=tags_list, force_interactive=True)\n    if code == display_util.OK:\n        return_vhosts = _reversemap_vhosts(names, vhosts)\n        return return_vhosts\n    return []\n\n\ndef _reversemap_vhosts(names: Iterable[str], vhosts: Iterable[VirtualHost]) -> list[VirtualHost]:\n    \"\"\"Helper function for select_vhost_multiple for mapping string\n    representations back to actual vhost objects\"\"\"\n    return_vhosts = []\n\n    for selection in names:\n        for vhost in vhosts:\n            if vhost.display_repr().strip() == selection.strip():\n                return_vhosts.append(vhost)\n    return return_vhosts\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/http_01.py",
    "content": "\"\"\"A class that performs HTTP-01 challenges for Nginx\"\"\"\n\nimport logging\nfrom typing import Any\nfrom typing import Optional\nfrom typing import TYPE_CHECKING\n\nfrom acme import challenges, messages\nfrom acme.challenges import KeyAuthorizationChallengeResponse\nfrom certbot import errors\nfrom certbot.achallenges import KeyAuthorizationAnnotatedChallenge\nfrom certbot.compat import os\nfrom certbot.plugins import common\nfrom certbot_nginx._internal import nginxparser\nfrom certbot_nginx._internal.obj import Addr\n\nif TYPE_CHECKING:\n    from certbot_nginx._internal.configurator import NginxConfigurator\n\nlogger = logging.getLogger(__name__)\n\n\nclass NginxHttp01(common.ChallengePerformer):\n    \"\"\"HTTP-01 authenticator for Nginx\n\n    :ivar configurator: NginxConfigurator object\n    :type configurator: :class:`~nginx.configurator.NginxConfigurator`\n\n    :ivar list achalls: Annotated\n        class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge`\n        challenges\n\n    :param list indices: Meant to hold indices of challenges in a\n        larger array. NginxHttp01 is capable of solving many challenges\n        at once which causes an indexing issue within NginxConfigurator\n        who must return all responses in order. Imagine\n        NginxConfigurator maintaining state about where all of the\n        challenges, possibly of different types, belong in the response\n        array. This is an optional utility.\n\n    \"\"\"\n\n    def __init__(self, configurator: \"NginxConfigurator\") -> None:\n        super().__init__(configurator)\n        self.configurator: \"NginxConfigurator\"\n        self.challenge_conf = os.path.join(\n            configurator.config.config_dir, \"le_http_01_cert_challenge.conf\")\n\n    def perform(self) -> list[KeyAuthorizationChallengeResponse]:\n        \"\"\"Perform a challenge on Nginx.\n\n        :returns: list of :class:`acme.challenges.KeyAuthorizationChallengeResponse`\n        :rtype: list\n\n        \"\"\"\n        if not self.achalls:\n            return []\n        if any(achall.identifier.typ == messages.IDENTIFIER_IP for achall in self.achalls):\n            raise errors.ConfigurationError(\n                \"nginx authenticator not supported for IP address certificates\")\n\n        responses = [x.response(x.account_key) for x in self.achalls]\n\n        # Set up the configuration\n        self._mod_config()\n\n        # Save reversible changes\n        self.configurator.save(\"HTTP Challenge\", True)\n\n        return responses\n\n    def _mod_config(self) -> None:\n        \"\"\"Modifies Nginx config to include server_names_hash_bucket_size directive\n           and server challenge blocks.\n\n        :raises .MisconfigurationError:\n            Unable to find a suitable HTTP block in which to include\n            authenticator hosts.\n        \"\"\"\n        included = False\n        include_directive = ['\\n', 'include', ' ', self.challenge_conf]\n        http_path = self.configurator.parser.http_path\n\n        bucket_directive = ['\\n', 'server_names_hash_bucket_size', ' ', '128']\n\n        main = self.configurator.parser.parsed[http_path]\n        # insert include directive\n        for line in main:\n            if line[0] == ['http']:\n                body = line[1]\n                if include_directive not in body:\n                    body.insert(0, include_directive)\n                included = True\n                break\n\n        # insert or update the server_names_hash_bucket_size directive\n        # We have several options here.\n        # 1) Only check nginx.conf\n        # 2) Check included files, assuming they've been included inside http already,\n        #     because if they added it outside an http block their config is broken anyway\n        # 3) Add metadata during parsing to note if an include happened inside the http block\n        #\n        # 1 causes bugs; see https://github.com/certbot/certbot/issues/5199\n        # 3 would require a more extensive rewrite and probably isn't necessary anyway\n        # So this code uses option 2.\n        found_bucket = False\n        for file_contents in self.configurator.parser.parsed.values():\n            body = file_contents # already inside http in an included file\n            for line in file_contents:\n                if line[0] == ['http']:\n                    body = line[1] # enter http because this is nginx.conf\n                    break\n\n            for posn, inner_line in enumerate(body):\n                if inner_line[0] == bucket_directive[1]:\n                    if int(inner_line[1]) < int(bucket_directive[3]):\n                        body[posn] = bucket_directive\n                    found_bucket = True\n                    break\n\n            if found_bucket:\n                break\n\n        if not found_bucket:\n            for line in main:\n                if line[0] == ['http']:\n                    body = line[1]\n                    body.insert(0, bucket_directive)\n                    break\n\n        root = self.configurator.parser.config_root\n        if not included:\n            raise errors.MisconfigurationError(\n                'Certbot could not find a block to include '\n                'challenges in %s.' % root)\n        config = [self._make_or_mod_server_block(achall) for achall in self.achalls]\n        config = [x for x in config if x is not None]\n        config = nginxparser.UnspacedList(config)\n        logger.debug(\"Generated server block:\\n%s\", str(config))\n\n        self.configurator.reverter.register_file_creation(\n            True, self.challenge_conf)\n\n        with open(self.challenge_conf, \"w\", encoding=\"utf-8\") as new_conf:\n            nginxparser.dump(config, new_conf)\n\n    def _default_listen_addresses(self) -> list[Addr]:\n        \"\"\"Finds addresses for a challenge block to listen on.\n        :returns: list of :class:`certbot_nginx._internal.obj.Addr` to apply\n        :rtype: list\n        \"\"\"\n        addresses: list[Addr] = []\n        default_addr = \"%s\" % self.configurator.config.http01_port\n        ipv6_addr = \"[::]:{0}\".format(\n            self.configurator.config.http01_port)\n        port = self.configurator.config.http01_port\n\n        ipv6, ipv6only = self.configurator.ipv6_info(\"[::]\", str(port))\n\n        if ipv6:\n            # If IPv6 is active in Nginx configuration\n            if not ipv6only:\n                # If ipv6only=on is not already present in the config\n                ipv6_addr = ipv6_addr + \" ipv6only=on\"\n            addresses = [Addr.fromstring(default_addr),\n                         Addr.fromstring(ipv6_addr)]\n            logger.debug((\"Using default addresses %s and %s for authentication.\"),\n                        default_addr,\n                        ipv6_addr)\n        else:\n            addresses = [Addr.fromstring(default_addr)]\n            logger.debug(\"Using default address %s for authentication.\",\n                        default_addr)\n\n        return addresses\n\n    def _get_validation_path(self, achall: KeyAuthorizationAnnotatedChallenge) -> str:\n        return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode(\"token\"))\n\n    def _make_server_block(self, achall: KeyAuthorizationAnnotatedChallenge) -> list[Any]:\n        \"\"\"Creates a server block for a challenge.\n\n        :param achall: Annotated HTTP-01 challenge\n        :type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`\n\n        :returns: server block for the challenge host\n        :rtype: list\n        \"\"\"\n        addrs = self._default_listen_addresses()\n        block = [['listen', ' ', addr.to_string(include_default=False)] for addr in addrs]\n\n        # Ensure we 404 on any other request by setting a root\n        document_root = os.path.join(\n            self.configurator.config.work_dir, \"http_01_nonexistent\")\n\n        block.extend([['server_name', ' ', achall.identifier.value],\n                      ['root', ' ', document_root],\n                      self._location_directive_for_achall(achall)\n                      ])\n        # TODO: do we want to return something else if they otherwise access this block?\n        return [['server'], block]\n\n    def _location_directive_for_achall(self, achall: KeyAuthorizationAnnotatedChallenge\n                                       ) -> list[Any]:\n        validation = achall.validation(achall.account_key)\n        validation_path = self._get_validation_path(achall)\n\n        location_directive = [['location', ' ', '=', ' ', validation_path],\n                              [['default_type', ' ', 'text/plain'],\n                               ['return', ' ', '200', ' ', validation]]]\n        return location_directive\n\n    def _make_or_mod_server_block(self, achall: KeyAuthorizationAnnotatedChallenge\n                                  ) -> Optional[list[Any]]:\n        \"\"\"Modifies server blocks to respond to a challenge. Returns a new HTTP server block\n           to add to the configuration if an existing one can't be found.\n\n        :param achall: Annotated HTTP-01 challenge\n        :type achall: :class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`\n\n        :returns: new server block to be added, if any\n        :rtype: list\n\n        \"\"\"\n        http_vhosts, https_vhosts = self.configurator.choose_auth_vhosts(achall.identifier.value)\n\n        new_vhost: Optional[list[Any]] = None\n        if not http_vhosts:\n            # Couldn't find either a matching name+port server block\n            # or a port+default_server block, so create a dummy block\n            new_vhost = self._make_server_block(achall)\n\n        # Modify any existing server blocks\n        for vhost in set(http_vhosts + https_vhosts):\n            location_directive = [self._location_directive_for_achall(achall)]\n\n            self.configurator.parser.add_server_directives(vhost, location_directive)\n\n            rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)',\n                                  ' ', '$1', ' ', 'break']]\n            self.configurator.parser.add_server_directives(\n                vhost, rewrite_directive, insert_at_top=True)\n\n        return new_vhost\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/nginxparser.py",
    "content": "\"\"\"Very low-level nginx config parser based on pyparsing.\"\"\"\n# Forked from https://github.com/fatiherikli/nginxparser (MIT Licensed)\nimport copy\nimport logging\nimport operator\nimport typing\nfrom typing import Any\nfrom typing import IO\nfrom typing import Iterable\nfrom typing import Iterator\nfrom typing import overload\nfrom typing import SupportsIndex\nfrom typing import Union\n\nfrom pyparsing import Combine\nfrom pyparsing import Forward\nfrom pyparsing import Group\nfrom pyparsing import Literal\nfrom pyparsing import Optional\nfrom pyparsing import ParseResults\nfrom pyparsing import QuotedString\nfrom pyparsing import Regex\nfrom pyparsing import restOfLine\nfrom pyparsing import stringEnd\nfrom pyparsing import White\nfrom pyparsing import ZeroOrMore\n\nlogger = logging.getLogger(__name__)\n\n\nclass RawNginxParser:\n    # pylint: disable=pointless-statement\n    \"\"\"A class that parses nginx configuration with pyparsing.\"\"\"\n\n    # constants\n    space = Optional(White(ws=' \\t\\r\\n\\u00a0')).leave_whitespace()\n    required_space = White(ws=' \\t\\r\\n\\u00a0').leave_whitespace()\n\n    left_bracket = Literal(\"{\").suppress()\n    right_bracket = space + Literal(\"}\").suppress()\n    semicolon = Literal(\";\").suppress()\n    dquoted = QuotedString('\"', multiline=True, unquote_results=False, esc_char='\\\\')\n    squoted = QuotedString(\"'\", multiline=True, unquote_results=False, esc_char='\\\\')\n    quoted = dquoted | squoted\n    head_tokenchars = Regex(r\"(\\$\\{)|[^{};\\s'\\\"]\") # if (last_space)\n    tail_tokenchars = Regex(r\"(\\$\\{)|[^{;\\s]\") # else\n    tokenchars = Combine(head_tokenchars + ZeroOrMore(tail_tokenchars))\n    paren_quote_extend = Combine(quoted + Literal(')') + ZeroOrMore(tail_tokenchars))\n    # note: ')' allows extension, but then we fall into else, not last_space.\n\n    token = paren_quote_extend | tokenchars | quoted\n\n    whitespace_token_group = space + token + ZeroOrMore(required_space + token) + space\n    assignment = whitespace_token_group + semicolon\n\n    comment = space + Literal('#') + restOfLine\n\n    block = Forward()\n\n    # order matters! see issue 518, and also http { # server { \\n}\n    contents = Group(comment) | Group(block) | Group(assignment)\n\n    block_begin = Group(whitespace_token_group)\n    block_innards = Group(ZeroOrMore(contents) + space).leave_whitespace()\n    block << block_begin + left_bracket + block_innards + right_bracket\n\n    script = ZeroOrMore(contents) + space + stringEnd\n    script.parse_with_tabs().leave_whitespace()\n\n    def __init__(self, source: str) -> None:\n        self.source = source\n\n    def parse(self) -> ParseResults:\n        \"\"\"Returns the parsed tree.\"\"\"\n        return self.script.parse_string(self.source)\n\n    def as_list(self) -> list[Any]:\n        \"\"\"Returns the parsed tree as a list.\"\"\"\n        return self.parse().asList()\n\n\nclass RawNginxDumper:\n    \"\"\"A class that dumps nginx configuration from the provided tree.\"\"\"\n    def __init__(self, blocks: list[Any]) -> None:\n        self.blocks = blocks\n\n    def __iter__(self, blocks: typing.Optional[list[Any]] = None) -> Iterator[str]:\n        \"\"\"Iterates the dumped nginx content.\"\"\"\n        blocks = blocks or self.blocks\n        for b0 in blocks:\n            if isinstance(b0, str):\n                yield b0\n                continue\n            item = copy.deepcopy(b0)\n            if spacey(item[0]):\n                yield item.pop(0) # indentation\n                if not item:\n                    continue\n\n            if isinstance(item[0], list): # block\n                yield \"\".join(item.pop(0)) + '{'\n                for parameter in item.pop(0):\n                    yield from self.__iter__([parameter]) # negate \"for b0 in blocks\"\n                yield '}'\n            else: # not a block - list of strings\n                semicolon = \";\"\n                if isinstance(item[0], str) and item[0].strip() == '#': # comment\n                    semicolon = \"\"\n                yield \"\".join(item) + semicolon\n\n    def __str__(self) -> str:\n        \"\"\"Return the parsed block as a string.\"\"\"\n        return ''.join(self)\n\n\ndef spacey(x: Any) -> bool:\n    \"\"\"Is x an empty string or whitespace?\"\"\"\n    return (isinstance(x, str) and x.isspace()) or x == ''\n\n\nclass UnspacedList(list[Any]):\n    \"\"\"Wrap a list [of lists], making any whitespace entries magically invisible\"\"\"\n\n    def __init__(self, list_source: Iterable[Any]) -> None:\n        # ensure our argument is not a generator, and duplicate any sublists\n        self.spaced = copy.deepcopy(list(list_source))\n        self.dirty = False\n\n        # Turn self into a version of the source list that has spaces removed\n        # and all sub-lists also UnspacedList()ed\n        super().__init__(list_source)\n        for i, entry in reversed(list(enumerate(self))):\n            if isinstance(entry, list):\n                sublist = UnspacedList(entry)\n                super().__setitem__(i, sublist)\n                self.spaced[i] = sublist.spaced\n            elif spacey(entry):\n                # don't delete comments\n                if \"#\" not in self[:i]:\n                    super().__delitem__(i)\n\n    @overload\n    def _coerce(self, inbound: None) -> tuple[None, None]: ...\n\n    @overload\n    def _coerce(self, inbound: str) -> tuple[str, str]: ...\n\n    @overload\n    def _coerce(self, inbound: list[Any]) -> tuple[\"UnspacedList\", list[Any]]: ...\n\n    def _coerce(self, inbound: Any) -> tuple[Any, Any]:\n        \"\"\"\n        Coerce some inbound object to be appropriately usable in this object\n\n        :param inbound: string or None or list or UnspacedList\n        :returns: (coerced UnspacedList or string or None, spaced equivalent)\n        :rtype: tuple\n\n        \"\"\"\n        if not isinstance(inbound, list):  # str or None\n            return inbound, inbound\n        else:\n            if not hasattr(inbound, \"spaced\"):\n                inbound = UnspacedList(inbound)\n            return inbound, inbound.spaced\n\n    def insert(self, i: \"SupportsIndex\", x: Any) -> None:\n        \"\"\"Insert object before index.\"\"\"\n        idx = operator.index(i)\n        item, spaced_item = self._coerce(x)\n        slicepos = self._spaced_position(idx) if idx < len(self) else len(self.spaced)\n        self.spaced.insert(slicepos, spaced_item)\n        if not spacey(item):\n            super().insert(idx, item)\n        self.dirty = True\n\n    def append(self, x: Any) -> None:\n        \"\"\"Append object to the end of the list.\"\"\"\n        item, spaced_item = self._coerce(x)\n        self.spaced.append(spaced_item)\n        if not spacey(item):\n            super().append(item)\n        self.dirty = True\n\n    def extend(self, x: Any) -> None:\n        \"\"\"Extend list by appending elements from the iterable.\"\"\"\n        item, spaced_item = self._coerce(x)\n        self.spaced.extend(spaced_item)\n        super().extend(item)\n        self.dirty = True\n\n    def __add__(self, other: list[Any]) -> \"UnspacedList\":\n        new_list = copy.deepcopy(self)\n        new_list.extend(other)\n        new_list.dirty = True\n        return new_list\n\n    def pop(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Function pop() is not implemented for UnspacedList\"\"\"\n        raise NotImplementedError(\"UnspacedList.pop() not yet implemented\")\n\n    def remove(self, *args: Any, **kwargs: Any) -> None:\n        \"\"\"Function remove() is not implemented for UnspacedList\"\"\"\n        raise NotImplementedError(\"UnspacedList.remove() not yet implemented\")\n\n    def reverse(self) -> None:\n        \"\"\"Function reverse() is not implemented for UnspacedList\"\"\"\n        raise NotImplementedError(\"UnspacedList.reverse() not yet implemented\")\n\n    def sort(self, *_args: Any, **_kwargs: Any) -> None:\n        \"\"\"Function sort() is not implemented for UnspacedList\"\"\"\n        raise NotImplementedError(\"UnspacedList.sort() not yet implemented\")\n\n    def __setslice__(self, *args: Any, **kwargs: Any) -> None:\n        raise NotImplementedError(\"Slice operations on UnspacedLists not yet implemented\")\n\n    def __setitem__(self, i: Union[\"SupportsIndex\", slice], value: Any) -> None:\n        if isinstance(i, slice):\n            raise NotImplementedError(\"Slice operations on UnspacedLists not yet implemented\")\n        item, spaced_item = self._coerce(value)\n        self.spaced.__setitem__(self._spaced_position(i), spaced_item)\n        if not spacey(item):\n            super().__setitem__(i, item)\n        self.dirty = True\n\n    def __delitem__(self, i: Union[\"SupportsIndex\", slice]) -> None:\n        if isinstance(i, slice):\n            raise NotImplementedError(\"Slice operations on UnspacedLists not yet implemented\")\n        self.spaced.__delitem__(self._spaced_position(i))\n        super().__delitem__(i)\n        self.dirty = True\n\n    def __deepcopy__(self, memo: Any) -> \"UnspacedList\":\n        new_spaced = copy.deepcopy(self.spaced, memo=memo)\n        new_list = UnspacedList(new_spaced)\n        new_list.dirty = self.dirty\n        return new_list\n\n    def is_dirty(self) -> bool:\n        \"\"\"Recurse through the parse tree to figure out if any sublists are dirty\"\"\"\n        if self.dirty:\n            return True\n        return any((isinstance(x, UnspacedList) and x.is_dirty() for x in self))\n\n    def _spaced_position(self, idx: \"SupportsIndex\") -> int:\n        \"\"\"Convert from indexes in the unspaced list to positions in the spaced one\"\"\"\n        int_idx = operator.index(idx)\n        pos = spaces = 0\n        # Normalize indexes like list[-1] etc, and save the result\n        if int_idx < 0:\n            int_idx = len(self) + int_idx\n        if not 0 <= int_idx < len(self):\n            raise IndexError(\"list index out of range\")\n        int_idx0 = int_idx\n        # Count the number of spaces in the spaced list before int_idx in the unspaced one\n        while int_idx != -1:\n            if spacey(self.spaced[pos]):\n                spaces += 1\n            else:\n                int_idx -= 1\n            pos += 1\n        return int_idx0 + spaces\n\n\n# Shortcut functions to respect Python's serialization interface\n# (like pyyaml, picker or json)\n\ndef loads(source: str) -> UnspacedList:\n    \"\"\"Parses from a string.\n\n    :param str source: The string to parse\n    :returns: The parsed tree\n    :rtype: list\n\n    \"\"\"\n    return UnspacedList(RawNginxParser(source).as_list())\n\n\ndef load(file_: IO[Any]) -> UnspacedList:\n    \"\"\"Parses from a file.\n\n    :param file file_: The file to parse\n    :returns: The parsed tree\n    :rtype: list\n\n    \"\"\"\n    return loads(file_.read())\n\n\ndef dumps(blocks: UnspacedList) -> str:\n    \"\"\"Dump to a Unicode string.\n\n    :param UnspacedList blocks: The parsed tree\n    :rtype: str\n\n    \"\"\"\n    return str(RawNginxDumper(blocks.spaced))\n\n\ndef dump(blocks: UnspacedList, file_: IO[Any]) -> None:\n    \"\"\"Dump to a file.\n\n    :param UnspacedList blocks: The parsed tree\n    :param IO[Any] file_: The file stream to dump to. It must be opened with\n                          Unicode encoding.\n    :rtype: None\n\n    \"\"\"\n    file_.write(dumps(blocks))\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/obj.py",
    "content": "\"\"\"Module contains classes used by the Nginx Configurator.\"\"\"\nimport re\nfrom typing import Any\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\n\nfrom certbot.plugins import common\n\nADD_HEADER_DIRECTIVE = 'add_header'\n\n\nclass SocketAddrError(Exception):\n    \"\"\"Raised when a UNIX-domain socket address is encountered.\"\"\"\n\n\nclass Addr(common.Addr):\n    r\"\"\"Represents an Nginx address, i.e. what comes after the 'listen'\n    directive.\n\n    According to the `documentation`_, this may be address[:port], port,\n    or unix:path. The latter is ignored here.\n\n    The default value if no directive is specified is \\*:80 (superuser)\n    or \\*:8000 (otherwise). If no port is specified, the default is\n    80. If no address is specified, listen on all addresses.\n\n    .. _documentation:\n       https://nginx.org/en/docs/http/ngx_http_core_module.html#listen\n\n    .. todo:: Old-style nginx configs define SSL vhosts in a separate\n              block instead of using 'ssl' in the listen directive.\n\n    :param str addr: addr part of vhost address, may be hostname, IPv4, IPv6,\n        \"\", or \"\\*\"\n    :param str port: port number or \"\\*\" or \"\"\n    :param bool ssl: Whether the directive includes 'ssl'\n    :param bool default: Whether the directive includes 'default_server'\n    :param bool default: Whether this is an IPv6 address\n    :param bool ipv6only: Whether the directive includes 'ipv6only=on'\n\n    \"\"\"\n    UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')\n    CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]\n\n    def __init__(self, host: str, port: str, ssl: bool, default: bool,\n                 ipv6: bool, ipv6only: bool) -> None:\n        super().__init__((host, port))\n        self.ssl = ssl\n        self.default = default\n        self.ipv6 = ipv6\n        self.ipv6only = ipv6only\n        self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES\n\n    @classmethod\n    def fromstring(cls, str_addr: str) -> \"Addr\":\n        \"\"\"Initialize Addr from string.\n\n        :param str str_addr: nginx address string\n        :returns: parsed nginx address\n        :rtype: Addr\n        :raises SocketAddrError: if a UNIX-domain socket address is given\n\n        \"\"\"\n        parts = str_addr.split(' ')\n        ssl = False\n        default = False\n        ipv6 = False\n        ipv6only = False\n        host = ''\n        port = ''\n\n        # The first part must be the address\n        addr = parts.pop(0)\n\n        # Raise for UNIX-domain sockets\n        if addr.startswith('unix:'):\n            raise SocketAddrError(f'encountered UNIX-domain socket address {str_addr}')\n\n        # IPv6 check\n        ipv6_match = re.match(r'\\[.*\\]', addr)\n        if ipv6_match:\n            ipv6 = True\n            # IPv6 handling\n            host = ipv6_match.group()\n            # The rest of the addr string will be the port, if any\n            port = addr[ipv6_match.end()+1:]\n        else:\n            # IPv4 handling\n            tup = addr.partition(':')\n            if re.match(r'^\\d+$', tup[0]):\n                # This is a bare port, not a hostname. E.g. listen 80\n                host = ''\n                port = tup[0]\n            else:\n                # This is a host-port tuple. E.g. listen 127.0.0.1:*\n                host = tup[0]\n                port = tup[2]\n\n        # The rest of the parts are options; we only care about ssl and default\n        while parts:\n            nextpart = parts.pop()\n            if nextpart == 'ssl':\n                ssl = True\n            elif nextpart == 'default_server':\n                default = True\n            elif nextpart == 'default':\n                default = True\n            elif nextpart == \"ipv6only=on\":\n                ipv6only = True\n\n        return cls(host, port, ssl, default, ipv6, ipv6only)\n\n    def to_string(self, include_default: bool = True) -> str:\n        \"\"\"Return string representation of Addr\"\"\"\n        parts = ''\n        if self.tup[0] and self.tup[1]:\n            parts = \"%s:%s\" % self.tup\n        elif self.tup[0]:\n            parts = self.tup[0]\n        else:\n            parts = self.tup[1]\n\n        if self.default and include_default:\n            parts += ' default_server'\n        if self.ssl:\n            parts += ' ssl'\n\n        return parts\n\n    def __str__(self) -> str:\n        return self.to_string()\n\n    def __repr__(self) -> str:\n        return \"Addr(\" + self.__str__() + \")\"\n\n    def __hash__(self) -> int:  # pylint: disable=useless-super-delegation\n        # Python 3 requires explicit overridden for __hash__\n        # See certbot-apache/src/certbot_apache/_internal/obj.py for more information\n        return super().__hash__()\n\n    def super_eq(self, other: \"Addr\") -> bool:\n        \"\"\"Check ip/port equality, with IPv6 support.\n        \"\"\"\n        # If both addresses got an unspecified address, then make sure the\n        # host representation in each match when doing the comparison.\n        if self.unspecified_address and other.unspecified_address:\n            return common.Addr((self.CANONICAL_UNSPECIFIED_ADDRESS,\n                                self.tup[1]), self.ipv6) == \\\n                   common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,\n                                other.tup[1]), other.ipv6)\n        return super().__eq__(other)\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.super_eq(other) and\n                    self.ssl == other.ssl and\n                    self.default == other.default)\n        return False\n\n\nclass VirtualHost:\n    \"\"\"Represents an Nginx Virtualhost.\n\n    :ivar str filep: file path of VH\n    :ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`)\n    :ivar set names: Server names/aliases of vhost\n        (:class:`list` of :class:`str`)\n    :ivar list raw: The raw form of the parsed server block\n\n    :ivar bool ssl: SSLEngine on in vhost\n    :ivar bool enabled: Virtual host is enabled\n    :ivar list path: The indices into the parsed file used to access\n        the server block defining the vhost\n\n    \"\"\"\n\n    def __init__(self, filep: str, addrs: Sequence[Addr], ssl: bool, enabled: bool,\n                 names: set[str], raw: list[Any], path: list[int]) -> None:\n        \"\"\"Initialize a VH.\"\"\"\n        self.filep = filep\n        self.addrs = addrs\n        self.names = names\n        self.ssl = ssl\n        self.enabled = enabled\n        self.raw = raw\n        self.path = path\n\n    def __str__(self) -> str:\n        addr_str = \", \".join(str(addr) for addr in sorted(self.addrs, key=str))\n        # names might be a set, and it has different representations in Python\n        # 2 and 3. Force it to be a list here for consistent outputs\n        return (\"file: %s\\n\"\n                \"addrs: %s\\n\"\n                \"names: %s\\n\"\n                \"ssl: %s\\n\"\n                \"enabled: %s\" % (self.filep, addr_str,\n                                 list(self.names), self.ssl, self.enabled))\n\n    def __repr__(self) -> str:\n        return \"VirtualHost(\" + self.__str__().replace(\"\\n\", \", \") + \")\\n\"\n\n    def __eq__(self, other: Any) -> bool:\n        if isinstance(other, self.__class__):\n            return (self.filep == other.filep and\n                    sorted(self.addrs, key=str) == sorted(other.addrs, key=str) and\n                    self.names == other.names and\n                    self.ssl == other.ssl and\n                    self.enabled == other.enabled and\n                    self.path == other.path)\n\n        return False\n\n    def __hash__(self) -> int:\n        return hash((self.filep, tuple(self.path),\n                     tuple(self.addrs), tuple(self.names),\n                     self.ssl, self.enabled))\n\n    def has_header(self, header_name: str) -> bool:\n        \"\"\"Determine if this server block has a particular header set.\n        :param str header_name: The name of the header to check for, e.g.\n            'Strict-Transport-Security'\n        \"\"\"\n        found = _find_directive(self.raw, ADD_HEADER_DIRECTIVE, header_name)\n        return found is not None\n\n    def contains_list(self, test: list[Any]) -> bool:\n        \"\"\"Determine if raw server block contains test list at top level\n        \"\"\"\n        for i in range(0, len(self.raw) - len(test) + 1):\n            if self.raw[i:i + len(test)] == test:\n                return True\n        return False\n\n    def ipv6_enabled(self) -> bool:\n        \"\"\"Return true if one or more of the listen directives in vhost supports\n        IPv6\"\"\"\n        for a in self.addrs:\n            if a.ipv6:\n                return True\n        return False\n\n    def ipv4_enabled(self) -> bool:\n        \"\"\"Return true if one or more of the listen directives in vhost are IPv4\n        only\"\"\"\n        if not self.addrs:\n            return True\n        for a in self.addrs:\n            if not a.ipv6:\n                return True\n        return False\n\n    def display_repr(self) -> str:\n        \"\"\"Return a representation of VHost to be used in dialog\"\"\"\n        return (\n            \"File: {filename}\\n\"\n            \"Addresses: {addrs}\\n\"\n            \"Names: {names}\\n\"\n            \"HTTPS: {https}\\n\".format(\n                filename=self.filep,\n                addrs=\", \".join(str(addr) for addr in self.addrs),\n                names=\", \".join(self.names),\n                https=\"Yes\" if self.ssl else \"No\"))\n\n\ndef _find_directive(directives: Optional[Union[str, list[Any]]], directive_name: str,\n                    match_content: Optional[Any] = None) -> Optional[Any]:\n    \"\"\"Find a directive of type directive_name in directives. If match_content is given,\n       Searches for `match_content` in the directive arguments.\n    \"\"\"\n    if not directives or isinstance(directives, str):\n        return None\n\n    # If match_content is None, just match on directive type. Otherwise, match on\n    # both directive type -and- the content!\n    if directives[0] == directive_name and \\\n            (match_content is None or match_content in directives):\n        return directives\n\n    matches = (_find_directive(line, directive_name, match_content) for line in directives)\n    return next((m for m in matches if m is not None), None)\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/parser.py",
    "content": "\"\"\"NginxParser is a member object of the NginxConfigurator class.\"\"\"\nfrom __future__ import annotations\nimport copy\nimport functools\nimport glob\nimport logging\nimport re\nfrom typing import Any\nfrom typing import Callable\nfrom typing import cast\nfrom typing import Iterable\nfrom typing import Mapping\nfrom typing import Optional\nfrom typing import Sequence\nfrom typing import Union\n\nimport pyparsing\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_nginx._internal import nginxparser\nfrom certbot_nginx._internal import obj\nfrom certbot_nginx._internal.nginxparser import UnspacedList\n\nlogger = logging.getLogger(__name__)\n\n\nclass NginxParser:\n    \"\"\"Class handles the fine details of parsing the Nginx Configuration.\n\n    :ivar str root: Normalized absolute path to the server root\n        directory. Without trailing slash.\n    :ivar dict parsed: Mapping of file paths to parsed trees\n\n    \"\"\"\n\n    def __init__(self, root: str) -> None:\n        self.parsed: dict[str, UnspacedList] = {}\n        self.root = os.path.abspath(root)\n        self.config_root = self._find_config_root()\n        self._http_path: str | None = None\n\n        # Parse nginx.conf and included files.\n        # TODO: Check sites-available/ as well. For now, the configurator does\n        # not enable sites from there.\n        self.load()\n\n    def load(self) -> None:\n        \"\"\"Loads Nginx files into a parsed tree.\n\n        \"\"\"\n        self.parsed = {}\n        self._parse_recursively(self.config_root)\n\n    @property\n    def http_path(self) -> str:\n        \"\"\" Filepath of file containing nginx http block. Set in self._parse_recursively\n        \"\"\"\n        if self._http_path is None:\n            raise errors.MisconfigurationError('No nginx http block found')\n        return self._http_path\n\n    def _parse_recursively(self, filepath: str) -> None:\n        \"\"\"Parses nginx config files recursively by looking at 'include'\n        directives inside 'http' and 'server' blocks. Note that this only\n        reads Nginx files that potentially declare a virtual host.\n\n        :param str filepath: The path to the files to parse, as a glob\n\n        \"\"\"\n        # pylint: disable=too-many-nested-blocks\n        filepath = self.abs_path(filepath)\n        trees: dict[str, UnspacedList] = self._parse_files(filepath)\n        for filename, tree in trees.items():\n            for entry in tree:\n                if _is_include_directive(entry):\n                    # Parse the top-level included file\n                    self._parse_recursively(entry[1])\n                elif entry[0] == ['http'] or entry[0] == ['server']:\n                    # Note http block location for http_01.py\n                    if entry[0] == ['http']:\n                        self._http_path = filename\n                    # Look for includes in the top-level 'http'/'server' context\n                    for subentry in entry[1]:\n                        if _is_include_directive(subentry):\n                            self._parse_recursively(subentry[1])\n                        elif entry[0] == ['http'] and subentry[0] == ['server']:\n                            # Look for includes in a 'server' context within\n                            # an 'http' context\n                            for server_entry in subentry[1]:\n                                if _is_include_directive(server_entry):\n                                    self._parse_recursively(server_entry[1])\n\n    def abs_path(self, path: str) -> str:\n        \"\"\"Converts a relative path to an absolute path relative to the root.\n        Does nothing for paths that are already absolute.\n\n        :param str path: The path\n        :returns: The absolute path\n        :rtype: str\n\n        \"\"\"\n        if not os.path.isabs(path):\n            return os.path.normpath(os.path.join(self.root, path))\n        return os.path.normpath(path)\n\n    def _build_addr_to_ssl(self) -> dict[tuple[str, str], bool]:\n        \"\"\"Builds a map from address to whether it listens on ssl in any server block\n        \"\"\"\n        servers = self._get_raw_servers()\n\n        addr_to_ssl: dict[tuple[str, str], bool] = {}\n        for server_list in servers.values():\n            for server, _ in server_list:\n                # Parse the server block to save addr info\n                parsed_server = _parse_server_raw(server)\n                for addr in parsed_server['addrs']:\n                    addr_tuple = addr.normalized_tuple()\n                    if addr_tuple not in addr_to_ssl:\n                        addr_to_ssl[addr_tuple] = addr.ssl\n                    addr_to_ssl[addr_tuple] = addr.ssl or addr_to_ssl[addr_tuple]\n        return addr_to_ssl\n\n    def _get_raw_servers(self) -> dict[str, Union[list[Any], UnspacedList]]:\n        # pylint: disable=cell-var-from-loop\n        \"\"\"Get a map of unparsed all server blocks\n        \"\"\"\n        servers: dict[str, Union[list[Any], nginxparser.UnspacedList]] = {}\n        for filename, tree in self.parsed.items():\n            servers[filename] = []\n            srv = servers[filename]  # workaround undefined loop var in lambdas\n\n            # Find all the server blocks\n            _do_for_subarray(tree, lambda x: len(x) >= 2 and x[0] == ['server'],\n                             lambda x, y: srv.append((x[1], y)))\n\n            # Find 'include' statements in server blocks and append their trees\n            for i, (server, path) in enumerate(servers[filename]):\n                new_server = self._get_included_directives(server)\n                servers[filename][i] = (new_server, path)\n        return servers\n\n    def get_vhosts(self) -> list[obj.VirtualHost]:\n        \"\"\"Gets list of all 'virtual hosts' found in Nginx configuration.\n        Technically this is a misnomer because Nginx does not have virtual\n        hosts, it has 'server blocks'.\n\n        :returns: List of :class:`~certbot_nginx._internal.obj.VirtualHost`\n            objects found in configuration\n        :rtype: list\n\n        \"\"\"\n        enabled = True  # We only look at enabled vhosts for now\n        servers = self._get_raw_servers()\n\n        vhosts = []\n        for filename, server_list in servers.items():\n            for server, path in server_list:\n                # Parse the server block into a VirtualHost object\n\n                parsed_server = _parse_server_raw(server)\n                vhost = obj.VirtualHost(filename,\n                                        parsed_server['addrs'],\n                                        parsed_server['ssl'],\n                                        enabled,\n                                        parsed_server['names'],\n                                        server,\n                                        path)\n                vhosts.append(vhost)\n\n        self._update_vhosts_addrs_ssl(vhosts)\n\n        return vhosts\n\n    def _update_vhosts_addrs_ssl(self, vhosts: Iterable[obj.VirtualHost]) -> None:\n        \"\"\"Update a list of raw parsed vhosts to include global address sslishness\n        \"\"\"\n        addr_to_ssl = self._build_addr_to_ssl()\n        for vhost in vhosts:\n            for addr in vhost.addrs:\n                addr.ssl = addr_to_ssl[addr.normalized_tuple()]\n                if addr.ssl:\n                    vhost.ssl = True\n\n    def _get_included_directives(self, block: UnspacedList) -> UnspacedList:\n        \"\"\"Returns array with the \"include\" directives expanded out by\n        concatenating the contents of the included file to the block.\n\n        :param list block:\n        :rtype: list\n\n        \"\"\"\n        result = copy.deepcopy(block)  # Copy the list to keep self.parsed idempotent\n        for directive in block:\n            if _is_include_directive(directive):\n                included_files = glob.glob(\n                    self.abs_path(directive[1]))\n                for incl in included_files:\n                    try:\n                        result.extend(self.parsed[incl])\n                    except KeyError:\n                        pass\n        return result\n\n    def _parse_files(self, filepath: str, override: bool = False) -> dict[str, UnspacedList]:\n        \"\"\"Parse files from a glob\n\n        :param str filepath: Nginx config file path\n        :param bool override: Whether to parse a file that has been parsed\n        :returns: dict of parsed tree structures indexed by filename\n        :rtype: dict[str, UnspacedList]\n\n        \"\"\"\n        files: list[str] = glob.glob(filepath) # nginx on unix calls glob(3) for this\n                                    # XXX Windows nginx uses FindFirstFile, and\n                                    # should have a narrower call here\n        trees: dict[str, UnspacedList] = {}\n        for filename in files:\n            if filename in self.parsed and not override:\n                continue\n            try:\n                with open(filename, \"r\", encoding=\"utf-8\") as _file:\n                    parsed = nginxparser.load(_file)\n                    self.parsed[filename] = parsed\n                    trees[filename] = parsed\n            except OSError:\n                logger.warning(\"Could not open file: %s\", filename)\n            except UnicodeDecodeError:\n                logger.warning(\"Could not read file: %s due to invalid \"\n                               \"character. Only UTF-8 encoding is \"\n                               \"supported.\", filename)\n            except pyparsing.ParseException:\n                logger.warning(\"Could not parse file: %s. This is usually due to a comment that \"\n                    \"certbot cannot parse, such as between a block's name and definition or \"\n                    \"within a string literal. Moving the comment to another location in the file \"\n                    \"or deleting it may resolve the issue.\", filename)\n        return trees\n\n    def _find_config_root(self) -> str:\n        \"\"\"Return the Nginx Configuration Root file.\"\"\"\n        location = ['nginx.conf']\n\n        for name in location:\n            if os.path.isfile(os.path.join(self.root, name)):\n                return os.path.join(self.root, name)\n\n        raise errors.NoInstallationError(\n            \"Could not find Nginx root configuration file (nginx.conf)\")\n\n    def filedump(self, ext: str = 'tmp', lazy: bool = True) -> None:\n        \"\"\"Dumps parsed configurations into files.\n\n        :param str ext: The file extension to use for the dumped files. If\n            empty, this overrides the existing conf files.\n        :param bool lazy: Only write files that have been modified\n\n        \"\"\"\n        # Best-effort atomicity is enforced above us by reverter.py\n        for filename, tree in self.parsed.items():\n            if ext:\n                filename = filename + os.path.extsep + ext\n            if not isinstance(tree, UnspacedList):\n                raise ValueError(f\"Error tree {tree} is not an UnspacedList\")\n            try:\n                if lazy and not tree.is_dirty():\n                    continue\n                out = nginxparser.dumps(tree)\n                logger.debug('Writing nginx conf tree to %s:\\n%s', filename, out)\n                with open(filename, 'w', encoding='utf-8') as _file:\n                    _file.write(out)\n\n            except OSError:\n                logger.error(\"Could not open file for writing: %s\", filename)\n\n    def parse_server(self, server: UnspacedList) -> dict[str, Any]:\n        \"\"\"Parses a list of server directives, accounting for global address sslishness.\n\n        :param list server: list of directives in a server block\n        :rtype: dict\n        \"\"\"\n        addr_to_ssl = self._build_addr_to_ssl()\n        parsed_server = _parse_server_raw(server)\n        _apply_global_addr_ssl(addr_to_ssl, parsed_server)\n        return parsed_server\n\n    def has_ssl_on_directive(self, vhost: obj.VirtualHost) -> bool:\n        \"\"\"Does vhost have ssl on for all ports?\n\n        :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost in question\n\n        :returns: True if 'ssl on' directive is included\n        :rtype: bool\n\n        \"\"\"\n        server = vhost.raw\n        for directive in server:\n            if not directive:\n                continue\n            if _is_ssl_on_directive(directive):\n                return True\n\n        return False\n\n    def add_server_directives(self, vhost: obj.VirtualHost, directives: list[Any],\n                              insert_at_top: bool = False) -> None:\n        \"\"\"Add directives to the server block identified by vhost.\n\n        This method modifies vhost to be fully consistent with the new directives.\n\n        ..note :: It's an error to try and add a nonrepeatable directive that already\n            exists in the config block with a conflicting value.\n\n        ..todo :: Doesn't match server blocks whose server_name directives are\n            split across multiple conf files.\n\n        :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost\n            whose information we use to match on\n        :param list directives: The directives to add\n        :param bool insert_at_top: True if the directives need to be inserted at the top\n            of the server block instead of the bottom\n\n        \"\"\"\n        self._modify_server_directives(vhost,\n            functools.partial(_add_directives, directives, insert_at_top))\n\n    def update_or_add_server_directives(self, vhost: obj.VirtualHost, directives: list[Any],\n                                        insert_at_top: bool = False) -> None:\n        \"\"\"Add or replace directives in the server block identified by vhost.\n\n        This method modifies vhost to be fully consistent with the new directives.\n\n        ..note :: When a directive with the same name already exists in the\n        config block, the first instance will be replaced. Otherwise, the directive\n        will be appended/prepended to the config block as in add_server_directives.\n\n        ..todo :: Doesn't match server blocks whose server_name directives are\n            split across multiple conf files.\n\n        :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost\n            whose information we use to match on\n        :param list directives: The directives to add\n        :param bool insert_at_top: True if the directives need to be inserted at the top\n            of the server block instead of the bottom\n\n        \"\"\"\n        self._modify_server_directives(vhost,\n            functools.partial(_update_or_add_directives, directives, insert_at_top))\n\n    def remove_server_directives(self, vhost: obj.VirtualHost, directive_name: str,\n                                 match_func: Optional[Callable[[Any], bool]] = None) -> None:\n        \"\"\"Remove all directives of type directive_name.\n\n        :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost: The vhost\n            to remove directives from\n        :param string directive_name: The directive type to remove\n        :param callable match_func: Function of the directive that returns true for directives\n            to be deleted.\n        \"\"\"\n        self._modify_server_directives(vhost,\n            functools.partial(_remove_directives, directive_name, match_func))\n\n    def _update_vhost_based_on_new_directives(self, vhost: obj.VirtualHost,\n                                              directives_list: UnspacedList) -> None:\n        new_server = self._get_included_directives(directives_list)\n        parsed_server = self.parse_server(new_server)\n        vhost.addrs = parsed_server['addrs']\n        vhost.ssl = parsed_server['ssl']\n        vhost.names = parsed_server['names']\n        vhost.raw = new_server\n\n    def _modify_server_directives(self, vhost: obj.VirtualHost,\n                                  block_func: Callable[[list[Any]], None]) -> None:\n        filename = vhost.filep\n        try:\n            result = self.parsed[filename]\n            for index in vhost.path:\n                result = result[index]\n            if not isinstance(result, list) or len(result) != 2:\n                raise errors.MisconfigurationError(\"Not a server block.\")\n            result = result[1]\n            block_func(result)\n\n            self._update_vhost_based_on_new_directives(vhost, result)\n        except errors.MisconfigurationError as err:\n            raise errors.MisconfigurationError(\"Problem in %s: %s\" % (filename, str(err)))\n\n    def duplicate_vhost(self, vhost_template: obj.VirtualHost,\n                        remove_singleton_listen_params: bool = False,\n                        only_directives: Optional[list[Any]] = None) -> obj.VirtualHost:\n        \"\"\"Duplicate the vhost in the configuration files.\n\n        :param :class:`~certbot_nginx._internal.obj.VirtualHost` vhost_template: The vhost\n            whose information we copy\n        :param bool remove_singleton_listen_params: If we should remove parameters\n            from listen directives in the block that can only be used once per address\n        :param list only_directives: If it exists, only duplicate the named directives. Only\n            looks at first level of depth; does not expand includes.\n\n        :returns: A vhost object for the newly created vhost\n        :rtype: :class:`~certbot_nginx._internal.obj.VirtualHost`\n        \"\"\"\n        # TODO: https://github.com/certbot/certbot/issues/5185\n        # put it in the same file as the template, at the same level\n        new_vhost = copy.deepcopy(vhost_template)\n\n        enclosing_block = self.parsed[vhost_template.filep]\n        for index in vhost_template.path[:-1]:\n            enclosing_block = enclosing_block[index]\n        raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]])\n\n        if only_directives is not None:\n            new_directives = nginxparser.UnspacedList([])\n            for directive in raw_in_parsed[1]:\n                if directive and directive[0] in only_directives:\n                    new_directives.append(directive)\n            raw_in_parsed[1] = new_directives\n\n            self._update_vhost_based_on_new_directives(new_vhost, new_directives)\n\n        enclosing_block.append(raw_in_parsed)\n        new_vhost.path[-1] = len(enclosing_block) - 1\n        if remove_singleton_listen_params:\n            for addr in new_vhost.addrs:\n                addr.default = False\n                addr.ipv6only = False\n            for directive in enclosing_block[new_vhost.path[-1]][1]:\n                if directive and directive[0] == 'listen':\n                    # Exclude one-time use parameters which will cause an error if repeated.\n                    # https://nginx.org/en/docs/http/ngx_http_core_module.html#listen\n                    exclude = {'default_server', 'default', 'setfib', 'fastopen', 'backlog',\n                                   'rcvbuf', 'sndbuf', 'accept_filter', 'deferred', 'bind',\n                                   'ipv6only', 'reuseport', 'so_keepalive'}\n\n                    for param in exclude:\n                        # See: github.com/certbot/certbot/pull/6223#pullrequestreview-143019225\n                        keys = [x.split('=')[0] for x in directive]\n                        if param in keys:\n                            del directive[keys.index(param)]\n        return new_vhost\n\n\ndef _parse_ssl_options(ssl_options: Optional[str]) -> list[UnspacedList]:\n    if ssl_options is not None:\n        try:\n            with open(ssl_options, \"r\", encoding=\"utf-8\") as _file:\n                return nginxparser.load(_file)\n        except OSError:\n            logger.warning(\"Missing NGINX TLS options file: %s\", ssl_options)\n        except UnicodeDecodeError:\n            logger.warning(\"Could not read file: %s due to invalid character. \"\n                           \"Only UTF-8 encoding is supported.\", ssl_options)\n        except pyparsing.ParseBaseException as err:\n            logger.warning(\"Could not parse file: %s due to %s\", ssl_options, err)\n    return UnspacedList([])\n\n\ndef _do_for_subarray(entry: list[Any], condition: Callable[[list[Any]], bool],\n                     func: Callable[[list[Any], list[int]], None],\n                     path: Optional[list[int]] = None) -> None:\n    \"\"\"Executes a function for a subarray of a nested array if it matches\n    the given condition.\n\n    :param list entry: The list to iterate over\n    :param function condition: Returns true iff func should be executed on item\n    :param function func: The function to call for each matching item\n\n    \"\"\"\n    if path is None:\n        path = []\n    if isinstance(entry, list):\n        if condition(entry):\n            func(entry, path)\n        else:\n            for index, item in enumerate(entry):\n                _do_for_subarray(item, condition, func, path + [index])\n\n\ndef get_best_match(target_name: str, names: Iterable[str]) -> tuple[Optional[str], Optional[str]]:\n    \"\"\"Finds the best match for target_name out of names using the Nginx\n    name-matching rules (exact > longest wildcard starting with * >\n    longest wildcard ending with * > regex).\n\n    :param str target_name: The name to match\n    :param set names: The candidate server names\n    :returns: Tuple of (type of match, the name that matched)\n    :rtype: tuple\n\n    \"\"\"\n    exact = []\n    wildcard_start = []\n    wildcard_end = []\n    regex = []\n\n    for name in names:\n        if _exact_match(target_name, name):\n            exact.append(name)\n        elif _wildcard_match(target_name, name, True):\n            wildcard_start.append(name)\n        elif _wildcard_match(target_name, name, False):\n            wildcard_end.append(name)\n        elif _regex_match(target_name, name):\n            regex.append(name)\n\n    if exact:\n        # There can be more than one exact match; e.g. eff.org, .eff.org\n        match = min(exact, key=len)\n        return 'exact', match\n    if wildcard_start:\n        # Return the longest wildcard\n        match = max(wildcard_start, key=len)\n        return 'wildcard_start', match\n    if wildcard_end:\n        # Return the longest wildcard\n        match = max(wildcard_end, key=len)\n        return 'wildcard_end', match\n    if regex:\n        # Just return the first one for now\n        match = regex[0]\n        return 'regex', match\n\n    return None, None\n\n\ndef _exact_match(target_name: str, name: str) -> bool:\n    target_lower = target_name.lower()\n    return name.lower() in (target_lower, '.' + target_lower)\n\n\ndef _wildcard_match(target_name: str, name: str, start: bool) -> bool:\n    # Degenerate case\n    if name == '*':\n        return True\n\n    parts = target_name.split('.')\n    match_parts = name.split('.')\n\n    # If the domain ends in a wildcard, do the match procedure in reverse\n    if not start:\n        parts.reverse()\n        match_parts.reverse()\n\n    # The first part must be a wildcard or blank, e.g. '.eff.org'\n    first = match_parts.pop(0)\n    if first not in ('*', ''):\n        return False\n\n    target_name_lower = '.'.join(parts).lower()\n    name_lower = '.'.join(match_parts).lower()\n\n    # Ex: www.eff.org matches *.eff.org, eff.org does not match *.eff.org\n    return target_name_lower.endswith('.' + name_lower)\n\n\ndef _regex_match(target_name: str, name: str) -> bool:\n    # Must start with a tilde\n    if len(name) < 2 or name[0] != '~':\n        return False\n\n    # After tilde is a perl-compatible regex\n    try:\n        regex = re.compile(name[1:])\n        return bool(re.match(regex, target_name))\n    except re.error:  # pragma: no cover\n        # perl-compatible regexes are sometimes not recognized by python\n        return False\n\n\ndef _is_include_directive(entry: Any) -> bool:\n    \"\"\"Checks if an nginx parsed entry is an 'include' directive.\n\n    :param list entry: the parsed entry\n    :returns: Whether it's an 'include' directive\n    :rtype: bool\n\n    \"\"\"\n    return (isinstance(entry, list) and\n            len(entry) == 2 and entry[0] == 'include' and\n            isinstance(entry[1], str))\n\n\ndef _is_ssl_on_directive(entry: Any) -> bool:\n    \"\"\"Checks if an nginx parsed entry is an 'ssl on' directive.\n\n    :param list entry: the parsed entry\n    :returns: Whether it's an 'ssl on' directive\n    :rtype: bool\n\n    \"\"\"\n    return (isinstance(entry, list) and\n            len(entry) == 2 and entry[0] == 'ssl' and\n            entry[1] == 'on')\n\n\ndef _add_directives(directives: list[Any], insert_at_top: bool,\n                    block: UnspacedList) -> None:\n    \"\"\"Adds directives to a config block.\"\"\"\n    for directive in directives:\n        _add_directive(block, directive, insert_at_top)\n    if block and '\\n' not in block[-1]:  # could be \"   \\n  \" or [\"\\n\"] !\n        block.append(nginxparser.UnspacedList('\\n'))\n\n\ndef _update_or_add_directives(directives: list[Any], insert_at_top: bool,\n                              block: UnspacedList) -> None:\n    \"\"\"Adds or replaces directives in a config block.\"\"\"\n    for directive in directives:\n        _update_or_add_directive(block, directive, insert_at_top)\n    if block and '\\n' not in block[-1]:  # could be \"   \\n  \" or [\"\\n\"] !\n        block.append(nginxparser.UnspacedList('\\n'))\n\n\nINCLUDE = 'include'\nREPEATABLE_DIRECTIVES = {'server_name', 'listen', INCLUDE, 'rewrite', 'add_header'}\nCOMMENT = ' managed by Certbot'\nCOMMENT_BLOCK = [' ', '#', COMMENT]\n\n\ndef comment_directive(block: UnspacedList, location: int) -> None:\n    \"\"\"Add a ``#managed by Certbot`` comment to the end of the line at location.\n\n    :param list block: The block containing the directive to be commented\n    :param int location: The location within ``block`` of the directive to be commented\n    \"\"\"\n    next_entry = block[location + 1] if location + 1 < len(block) else None\n    if isinstance(next_entry, list) and next_entry:\n        if len(next_entry) >= 2 and next_entry[-2] == \"#\" and COMMENT in next_entry[-1]:\n            return\n        if isinstance(next_entry, nginxparser.UnspacedList):\n            next_entry = next_entry.spaced[0]\n        else:\n            next_entry = next_entry[0]\n\n    block.insert(location + 1, COMMENT_BLOCK[:])\n    if next_entry is not None and \"\\n\" not in next_entry:\n        block.insert(location + 2, '\\n')\n\n\ndef _comment_out_directive(block: UnspacedList, location: int, include_location: str) -> None:\n    \"\"\"Comment out the line at location, with a note of explanation.\"\"\"\n    comment_message = ' duplicated in {0}'.format(include_location)\n    # add the end comment\n    # create a dumpable object out of block[location] (so it includes the ;)\n    directive = block[location]\n    new_dir_block = nginxparser.UnspacedList([])  # just a wrapper\n    new_dir_block.append(directive)\n    dumped = nginxparser.dumps(new_dir_block)\n    commented = dumped + ' #' + comment_message  # add the comment directly to the one-line string\n    new_dir = nginxparser.loads(commented)  # reload into UnspacedList\n\n    # add the beginning comment\n    insert_location = 0\n    if new_dir[0].spaced[0] != new_dir[0][0]:  # if there's whitespace at the beginning\n        insert_location = 1\n    new_dir[0].spaced.insert(insert_location, \"# \")  # comment out the line\n    new_dir[0].spaced.append(\";\")  # directly add in the ;, because now dumping won't work properly\n    dumped = nginxparser.dumps(new_dir)\n    new_dir = nginxparser.loads(dumped)  # reload into an UnspacedList\n\n    block[location] = new_dir[0] # set the now-single-line-comment directive back in place\n\n\ndef _find_location(block: UnspacedList, directive_name: str,\n                   match_func: Optional[Callable[[Any], bool]] = None) -> Optional[int]:\n    \"\"\"Finds the index of the first instance of directive_name in block.\n       If no line exists, use None.\"\"\"\n    return next((index for index, line in enumerate(block) if (\n        line and line[0] == directive_name and (match_func is None or match_func(line)))), None)\n\n\ndef _is_whitespace_or_comment(directive: Sequence[Any]) -> bool:\n    \"\"\"Is this directive either a whitespace or comment directive?\"\"\"\n    return len(directive) == 0 or directive[0] == '#'\n\n\ndef _add_directive(block: UnspacedList, directive: Sequence[Any], insert_at_top: bool) -> None:\n    if not isinstance(directive, nginxparser.UnspacedList):\n        directive = nginxparser.UnspacedList(directive)\n    if _is_whitespace_or_comment(directive):\n        # whitespace or comment\n        block.append(directive)\n        return\n\n    location = _find_location(block, directive[0])\n\n    # Append or prepend directive. Fail if the name is not a repeatable directive name,\n    # and there is already a copy of that directive with a different value\n    # in the config file.\n\n    # handle flat include files\n\n    directive_name = directive[0]\n\n    def can_append(loc: Optional[int], dir_name: str) -> bool:\n        \"\"\" Can we append this directive to the block? \"\"\"\n        return loc is None or (isinstance(dir_name, str)\n                               and dir_name in REPEATABLE_DIRECTIVES)\n\n    err_fmt = 'tried to insert directive \"{0}\" but found conflicting \"{1}\".'\n\n    # Give a better error message about the specific directive than Nginx's \"fail to restart\"\n    if directive_name == INCLUDE:\n        # in theory, we might want to do this recursively, but in practice, that's really not\n        # necessary because we know what file we're talking about (and if we don't recurse, we\n        # just give a worse error message)\n        included_directives = _parse_ssl_options(directive[1])\n\n        for included_directive in included_directives:\n            included_dir_loc = _find_location(block, included_directive[0])\n            included_dir_name = included_directive[0]\n            if (not _is_whitespace_or_comment(included_directive)\n                    and not can_append(included_dir_loc, included_dir_name)):\n\n                # By construction of can_append(), included_dir_loc cannot be None at that point\n                resolved_included_dir_loc = cast(int, included_dir_loc)\n\n                if block[resolved_included_dir_loc] != included_directive:\n                    raise errors.MisconfigurationError(err_fmt.format(\n                        included_directive, block[resolved_included_dir_loc]))\n                _comment_out_directive(block, resolved_included_dir_loc, directive[1])\n\n    if can_append(location, directive_name):\n        if insert_at_top:\n            # Add a newline so the comment doesn't comment\n            # out existing directives\n            block.insert(0, nginxparser.UnspacedList('\\n'))\n            block.insert(0, directive)\n            comment_directive(block, 0)\n        else:\n            block.append(directive)\n            comment_directive(block, len(block) - 1)\n        return\n\n    # By construction of can_append(), location cannot be None at that point\n    resolved_location = cast(int, location)\n\n    if block[resolved_location] != directive:\n        raise errors.MisconfigurationError(err_fmt.format(directive, block[resolved_location]))\n\n\ndef _update_directive(block: UnspacedList, directive: Sequence[Any], location: int) -> None:\n    block[location] = directive\n    comment_directive(block, location)\n\n\ndef _update_or_add_directive(block: UnspacedList, directive: Sequence[Any],\n                             insert_at_top: bool) -> None:\n    if not isinstance(directive, nginxparser.UnspacedList):\n        directive = nginxparser.UnspacedList(directive)\n    if _is_whitespace_or_comment(directive):\n        # whitespace or comment\n        block.append(directive)\n        return\n\n    location = _find_location(block, directive[0])\n\n    # we can update directive\n    if location is not None:\n        _update_directive(block, directive, location)\n        return\n\n    _add_directive(block, directive, insert_at_top)\n\n\ndef _is_certbot_comment(directive: Sequence[Any]) -> bool:\n    return '#' in directive and COMMENT in directive\n\n\ndef _remove_directives(directive_name: str, match_func: Callable[[Any], bool],\n                       block: UnspacedList) -> None:\n    \"\"\"Removes directives of name directive_name from a config block if match_func matches.\n    \"\"\"\n    while True:\n        location = _find_location(block, directive_name, match_func=match_func)\n        if location is None:\n            return\n        # if the directive was made by us, remove the comment following\n        if location + 1 < len(block) and _is_certbot_comment(block[location + 1]):\n            del block[location + 1]\n        del block[location]\n\n\ndef _apply_global_addr_ssl(addr_to_ssl: Mapping[tuple[str, str], bool],\n                           parsed_server: dict[str, Any]) -> None:\n    \"\"\"Apply global sslishness information to the parsed server block\n    \"\"\"\n    for addr in parsed_server['addrs']:\n        addr.ssl = addr_to_ssl[addr.normalized_tuple()]\n        if addr.ssl:\n            parsed_server['ssl'] = True\n\n\ndef _parse_server_raw(server: UnspacedList) -> dict[str, Any]:\n    \"\"\"Parses a list of server directives.\n\n    :param list server: list of directives in a server block\n    :rtype: dict\n\n    \"\"\"\n    addrs: set[obj.Addr] = set()\n    ssl: bool = False\n    names: set[str] = set()\n\n    apply_ssl_to_all_addrs = False\n\n    for directive in server:\n        if not directive:\n            continue\n        if directive[0] == 'listen':\n            try:\n                addr = obj.Addr.fromstring(\" \".join(directive[1:]))\n            except obj.SocketAddrError:\n                # Ignore UNIX-domain socket addresses\n                continue\n            addrs.add(addr)\n            if addr.ssl:\n                ssl = True\n        elif directive[0] == 'server_name':\n            params = directive[1:]\n            while '#' in params:\n                end_index = [i for i, param in enumerate(params) if param.startswith('\\n')][0]\n                params = params[:params.index('#')] + params[end_index+1:]\n            names.update(x.strip('\"\\'') for x in params)\n        elif _is_ssl_on_directive(directive):\n            ssl = True\n            apply_ssl_to_all_addrs = True\n\n    if apply_ssl_to_all_addrs:\n        for addr in addrs:\n            addr.ssl = True\n\n    return {\n        'addrs': addrs,\n        'ssl': ssl,\n        'names': names\n    }\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/parser_obj.py",
    "content": "# type: ignore\n# This module is not used for now, so we just skip type checking for the sake\n# of simplicity.\n\"\"\" This file contains parsing routines and object classes to help derive meaning from\nraw lists of tokens from pyparsing. \"\"\"\n\nimport abc\nimport logging\nfrom typing import Any\nfrom typing import Callable\nfrom typing import Iterator\nfrom typing import Optional\nfrom typing import Sequence\n\nfrom certbot import errors\n\nlogger = logging.getLogger(__name__)\nCOMMENT = \" managed by Certbot\"\nCOMMENT_BLOCK = [\"#\", COMMENT]\n\n\nclass Parsable:\n    \"\"\" Abstract base class for \"Parsable\" objects whose underlying representation\n    is a tree of lists.\n\n    :param .Parsable parent: This object's parsed parent in the tree\n    \"\"\"\n\n    __metaclass__ = abc.ABCMeta\n\n    def __init__(self, parent: Optional[\"Parsable\"] = None):\n        self._data: list[Any] = []\n        self._tabs = None\n        self.parent = parent\n\n    @classmethod\n    def parsing_hooks(cls) -> tuple[type[\"Block\"], type[\"Sentence\"], type[\"Statements\"]]:\n        \"\"\"Returns object types that this class should be able to `parse` recursively.\n        The order of the objects indicates the order in which the parser should\n        try to parse each subitem.\n        :returns: A list of Parsable classes.\n        :rtype list:\n        \"\"\"\n        return Block, Sentence, Statements\n\n    @staticmethod\n    @abc.abstractmethod\n    def should_parse(lists: Any) -> bool:\n        \"\"\" Returns whether the contents of `lists` can be parsed into this object.\n\n        :returns: Whether `lists` can be parsed as this object.\n        :rtype bool:\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def parse(self, raw_list: list[Any], add_spaces: bool = False) -> None:\n        \"\"\" Loads information into this object from underlying raw_list structure.\n        Each Parsable object might make different assumptions about the structure of\n        raw_list.\n\n        :param list raw_list: A list or sublist of tokens from pyparsing, containing whitespace\n            as separate tokens.\n        :param bool add_spaces: If set, the method can and should manipulate and insert spacing\n            between non-whitespace tokens and lists to delimit them.\n        :raises .errors.MisconfigurationError: when the assumptions about the structure of\n            raw_list are not met.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def iterate(self, expanded: bool = False,\n                match: Optional[Callable[[\"Parsable\"], bool]] = None) -> Iterator[Any]:\n        \"\"\" Iterates across this object. If this object is a leaf object, only yields\n        itself. If it contains references other parsing objects, and `expanded` is set,\n        this function should first yield itself, then recursively iterate across all of them.\n        :param bool expanded: Whether to recursively iterate on possible children.\n        :param callable match: If provided, an object is only iterated if this callable\n            returns True when called on that object.\n\n        :returns: Iterator over desired objects.\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def get_tabs(self) -> str:\n        \"\"\" Guess at the tabbing style of this parsed object, based on whitespace.\n\n        If this object is a leaf, it deducts the tabbing based on its own contents.\n        Other objects may guess by calling `get_tabs` recursively on child objects.\n\n        :returns: Guess at tabbing for this object. Should only return whitespace strings\n            that does not contain newlines.\n        :rtype str:\n        \"\"\"\n        raise NotImplementedError()\n\n    @abc.abstractmethod\n    def set_tabs(self, tabs: str = \"    \") -> None:\n        \"\"\"This tries to set and alter the tabbing of the current object to a desired\n        whitespace string. Primarily meant for objects that were constructed, so they\n        can conform to surrounding whitespace.\n\n        :param str tabs: A whitespace string (not containing newlines).\n        \"\"\"\n        raise NotImplementedError()\n\n    def dump(self, include_spaces: bool = False) -> list[Any]:\n        \"\"\" Dumps back to pyparsing-like list tree. The opposite of `parse`.\n\n        Note: if this object has not been modified, `dump` with `include_spaces=True`\n        should always return the original input of `parse`.\n\n        :param bool include_spaces: If set to False, magically hides whitespace tokens from\n            dumped output.\n\n        :returns: Pyparsing-like list tree.\n        :rtype list:\n        \"\"\"\n        return [elem.dump(include_spaces) for elem in self._data]\n\n\nclass Statements(Parsable):\n    \"\"\" A group or list of \"Statements\". A Statement is either a Block or a Sentence.\n\n    The underlying representation is simply a list of these Statement objects, with\n    an extra `_trailing_whitespace` string to keep track of the whitespace that does not\n    precede any more statements.\n    \"\"\"\n    def __init__(self, parent: Optional[Parsable] = None):\n        super().__init__(parent)\n        self._trailing_whitespace = None\n\n    # ======== Begin overridden functions\n\n    @staticmethod\n    def should_parse(lists: Any) -> bool:\n        return isinstance(lists, list)\n\n    def set_tabs(self, tabs: str = \"    \") -> None:\n        \"\"\" Sets the tabbing for this set of statements. Does this by calling `set_tabs`\n        on each of the child statements.\n\n        Then, if a parent is present, sets trailing whitespace to parent tabbing. This\n        is so that the trailing } of any Block that contains Statements lines up\n        with parent tabbing.\n        \"\"\"\n        for statement in self._data:\n            statement.set_tabs(tabs)\n        if self.parent is not None:\n            self._trailing_whitespace = \"\\n\" + self.parent.get_tabs()\n\n    def parse(self, raw_list: list[Any], add_spaces: bool = False) -> None:\n        \"\"\" Parses a list of statements.\n        Expects all elements in `raw_list` to be parseable by `type(self).parsing_hooks`,\n        with an optional whitespace string at the last index of `raw_list`.\n        \"\"\"\n        if not isinstance(raw_list, list):\n            raise errors.MisconfigurationError(\"Statements parsing expects a list!\")\n        # If there's a trailing whitespace in the list of statements, keep track of it.\n        if raw_list and isinstance(raw_list[-1], str) and raw_list[-1].isspace():\n            self._trailing_whitespace = raw_list[-1]\n            raw_list = raw_list[:-1]\n        self._data = [parse_raw(elem, self, add_spaces) for elem in raw_list]\n\n    def get_tabs(self) -> str:\n        \"\"\" Takes a guess at the tabbing of all contained Statements by retrieving the\n        tabbing of the first Statement.\"\"\"\n        if self._data:\n            return self._data[0].get_tabs()\n        return \"\"\n\n    def dump(self, include_spaces: bool = False) -> list[Any]:\n        \"\"\" Dumps this object by first dumping each statement, then appending its\n        trailing whitespace (if `include_spaces` is set) \"\"\"\n        data = super().dump(include_spaces)\n        if include_spaces and self._trailing_whitespace is not None:\n            return data + [self._trailing_whitespace]\n        return data\n\n    def iterate(self, expanded: bool = False,\n                match: Optional[Callable[[\"Parsable\"], bool]] = None) -> Iterator[Any]:\n        \"\"\" Combines each statement's iterator.  \"\"\"\n        for elem in self._data:\n            yield from elem.iterate(expanded, match)\n\n    # ======== End overridden functions\n\n\ndef _space_list(list_: Sequence[Any]) -> list[str]:\n    \"\"\" Inserts whitespace between adjacent non-whitespace tokens. \"\"\"\n    spaced_statement: list[str] = []\n    for i in reversed(range(len(list_))):\n        spaced_statement.insert(0, list_[i])\n        if i > 0 and not list_[i].isspace() and not list_[i-1].isspace():\n            spaced_statement.insert(0, \" \")\n    return spaced_statement\n\n\nclass Sentence(Parsable):\n    \"\"\" A list of words. Non-whitespace words are typically separated with whitespace tokens. \"\"\"\n\n    # ======== Begin overridden functions\n\n    @staticmethod\n    def should_parse(lists: Any) -> bool:\n        \"\"\" Returns True if `lists` can be parseable as a `Sentence`-- that is,\n        every element is a string type.\n\n        :param list lists: The raw unparsed list to check.\n\n        :returns: whether this lists is parseable by `Sentence`.\n        \"\"\"\n        return (isinstance(lists, list) and len(lists) > 0 and\n                all(isinstance(elem, str) for elem in lists))\n\n    def parse(self, raw_list: list[Any], add_spaces: bool = False) -> None:\n        \"\"\" Parses a list of string types into this object.\n        If add_spaces is set, adds whitespace tokens between adjacent non-whitespace tokens.\"\"\"\n        if add_spaces:\n            raw_list = _space_list(raw_list)\n        if (not isinstance(raw_list, list)\n                or any(not isinstance(elem, str) for elem in raw_list)):\n            raise errors.MisconfigurationError(\"Sentence parsing expects a list of string types.\")\n        self._data = raw_list\n\n    def iterate(self, expanded: bool = False,\n                match: Optional[Callable[[Parsable], bool]] = None) -> Iterator[Any]:\n        \"\"\" Simply yields itself. \"\"\"\n        if match is None or match(self):\n            yield self\n\n    def set_tabs(self, tabs: str = \"    \") -> None:\n        \"\"\" Sets the tabbing on this sentence. Inserts a newline and `tabs` at the\n        beginning of `self._data`. \"\"\"\n        if self._data[0].isspace():\n            return\n        self._data.insert(0, \"\\n\" + tabs)\n\n    def dump(self, include_spaces: bool = False) -> list[Any]:\n        \"\"\" Dumps this sentence. If include_spaces is set, includes whitespace tokens.\"\"\"\n        if not include_spaces:\n            return self.words\n        return self._data\n\n    def get_tabs(self) -> str:\n        \"\"\" Guesses at the tabbing of this sentence. If the first element is whitespace,\n        returns the whitespace after the rightmost newline in the string. \"\"\"\n        first = self._data[0]\n        if not first.isspace():\n            return \"\"\n        rindex = first.rfind(\"\\n\")\n        return first[rindex+1:]\n\n    # ======== End overridden functions\n\n    @property\n    def words(self) -> list[str]:\n        \"\"\" Iterates over words, but without spaces. Like Unspaced List. \"\"\"\n        return [word.strip(\"\\\"\\'\") for word in self._data if not word.isspace()]\n\n    def __getitem__(self, index: int) -> str:\n        return self.words[index]\n\n    def __contains__(self, word: str) -> bool:\n        return word in self.words\n\n\nclass Block(Parsable):\n    \"\"\" Any sort of block, denoted by a block name and curly braces, like so:\n    The parsed block:\n        block name {\n            content 1;\n            content 2;\n        }\n    might be represented with the list [names, contents], where\n        names = [\"block\", \" \", \"name\", \" \"]\n        contents = [[\"\\n    \", \"content\", \" \", \"1\"], [\"\\n    \", \"content\", \" \", \"2\"], \"\\n\"]\n    \"\"\"\n    def __init__(self, parent: Optional[Parsable] = None) -> None:\n        super().__init__(parent)\n        self.names: Optional[Sentence] = None\n        self.contents: Optional[Block] = None\n\n    @staticmethod\n    def should_parse(lists: Any) -> bool:\n        \"\"\" Returns True if `lists` can be parseable as a `Block`-- that is,\n        it's got a length of 2, the first element is a `Sentence` and the second can be\n        a `Statements`.\n\n        :param list lists: The raw unparsed list to check.\n\n        :returns: whether this lists is parseable by `Block`. \"\"\"\n        return isinstance(lists, list) and len(lists) == 2 and \\\n               Sentence.should_parse(lists[0]) and isinstance(lists[1], list)\n\n    def set_tabs(self, tabs: str = \"    \") -> None:\n        \"\"\" Sets tabs by setting equivalent tabbing on names, then adding tabbing\n        to contents.\"\"\"\n        self.names.set_tabs(tabs)\n        self.contents.set_tabs(tabs + \"    \")\n\n    def iterate(self, expanded: bool = False,\n                match: Optional[Callable[[Parsable], bool]] = None) -> Iterator[Any]:\n        \"\"\" Iterator over self, and if expanded is set, over its contents. \"\"\"\n        if match is None or match(self):\n            yield self\n        if expanded:\n            yield from self.contents.iterate(expanded, match)\n\n    def parse(self, raw_list: list[Any], add_spaces: bool = False) -> None:\n        \"\"\" Parses a list that resembles a block.\n\n        The assumptions that this routine makes are:\n            1. the first element of `raw_list` is a valid Sentence.\n            2. the second element of `raw_list` is a valid Statement.\n        If add_spaces is set, we call it recursively on `names` and `contents`, and\n        add an extra trailing space to `names` (to separate the block's opening bracket\n        and the block name).\n        \"\"\"\n        if not Block.should_parse(raw_list):\n            raise errors.MisconfigurationError(\"Block parsing expects a list of length 2. \"\n                \"First element should be a list of string types (the block names), \"\n                \"and second should be another list of statements (the block content).\")\n        self.names = Sentence(self)\n        if add_spaces:\n            raw_list[0].append(\" \")\n        self.names.parse(raw_list[0], add_spaces)\n        self.contents = Statements(self)\n        self.contents.parse(raw_list[1], add_spaces)\n        self._data = [self.names, self.contents]\n\n    def get_tabs(self) -> str:\n        \"\"\" Guesses tabbing by retrieving tabbing guess of self.names. \"\"\"\n        return self.names.get_tabs()\n\n\ndef _is_comment(parsed_obj: Parsable) -> bool:\n    \"\"\" Checks whether parsed_obj is a comment.\n\n    :param .Parsable parsed_obj:\n\n    :returns: whether parsed_obj represents a comment sentence.\n    :rtype bool:\n    \"\"\"\n    if not isinstance(parsed_obj, Sentence):\n        return False\n    return parsed_obj.words[0] == \"#\"\n\n\ndef _is_certbot_comment(parsed_obj: Parsable) -> bool:\n    \"\"\" Checks whether parsed_obj is a \"managed by Certbot\" comment.\n\n    :param .Parsable parsed_obj:\n\n    :returns: whether parsed_obj is a \"managed by Certbot\" comment.\n    :rtype bool:\n    \"\"\"\n    if not _is_comment(parsed_obj):\n        return False\n    if len(parsed_obj.words) != len(COMMENT_BLOCK):\n        return False\n    for i, word in enumerate(parsed_obj.words):\n        if word != COMMENT_BLOCK[i]:\n            return False\n    return True\n\n\ndef _certbot_comment(parent: Parsable, preceding_spaces: int = 4) -> Sentence:\n    \"\"\" A \"Managed by Certbot\" comment.\n    :param int preceding_spaces: Number of spaces between the end of the previous\n        statement and the comment.\n    :returns: Sentence containing the comment.\n    :rtype: .Sentence\n    \"\"\"\n    result = Sentence(parent)\n    result.parse([\" \" * preceding_spaces] + COMMENT_BLOCK)\n    return result\n\n\ndef _choose_parser(parent: Parsable, list_: Any) -> Parsable:\n    \"\"\" Choose a parser from type(parent).parsing_hooks, depending on whichever hook\n    returns True first. \"\"\"\n    hooks = Parsable.parsing_hooks()\n    if parent:\n        hooks = type(parent).parsing_hooks()\n    for type_ in hooks:\n        if type_.should_parse(list_):\n            return type_(parent)\n    raise errors.MisconfigurationError(\n        \"None of the parsing hooks succeeded, so we don't know how to parse this set of lists.\")\n\n\ndef parse_raw(lists_: Any, parent: Optional[Parsable] = None, add_spaces: bool = False) -> Parsable:\n    \"\"\" Primary parsing factory function.\n\n    :param list lists_: raw lists from pyparsing to parse.\n    :param .Parent parent: The parent containing this object.\n    :param bool add_spaces: Whether to pass add_spaces to the parser.\n\n    :returns .Parsable: The parsed object.\n\n    :raises errors.MisconfigurationError: If no parsing hook passes, and we can't\n        determine which type to parse the raw lists into.\n    \"\"\"\n    parser = _choose_parser(parent, lists_)\n    parser.parse(lists_, add_spaces)\n    return parser\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/__init__.py",
    "content": "\"\"\"certbot-nginx tests\"\"\"\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/configurator_test.py",
    "content": "\"\"\"Test for certbot_nginx._internal.configurator.\"\"\"\nimport sys\nfrom unittest import mock\n\nimport pytest\n\nfrom acme import challenges\nfrom acme import messages\nfrom certbot import achallenges\nfrom certbot import crypto_util\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot.tests import util as certbot_test_util\nfrom certbot_nginx._internal import obj\nfrom certbot_nginx._internal import parser\nfrom certbot_nginx._internal.configurator import _redirect_block_for_domain\nfrom certbot_nginx._internal.nginxparser import UnspacedList\nfrom certbot_nginx._internal.tests import test_util as util\n\n\nclass NginxConfiguratorTest(util.NginxTest):\n    \"\"\"Test a semi complex vhost configuration.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config = self.get_nginx_configurator(\n            self.config_path, self.config_dir, self.work_dir, self.logs_dir)\n\n        patch = mock.patch('certbot_nginx._internal.configurator.display_util.notify')\n        self.mock_notify = patch.start()\n        self.addCleanup(patch.stop)\n\n    @mock.patch(\"certbot_nginx._internal.configurator.util.exe_exists\")\n    def test_prepare_no_install(self, mock_exe_exists):\n        mock_exe_exists.return_value = False\n        with pytest.raises(errors.NoInstallationError):\n            self.config.prepare()\n\n    def test_prepare(self):\n        assert (1, 6, 2) == self.config.version\n        assert 16 == len(self.config.parser.parsed)\n\n    @mock.patch(\"certbot_nginx._internal.configurator.util.exe_exists\")\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    def test_prepare_initializes_version(self, mock_run, mock_exe_exists):\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/1.6.2\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"TLS SNI support enabled\",\n                           \"configure arguments: --prefix=/usr/local/Cellar/\"\n                           \"nginx/1.6.2 --with-http_ssl_module\"])\n\n        mock_exe_exists.return_value = True\n\n        self.config.version = None\n        self.config.config_test = mock.Mock()\n        self.config.prepare()\n        assert (1, 6, 2) == self.config.version\n\n    def test_prepare_locked(self):\n        # It is important to test that server_root is locked during the call to\n        # prepare (as opposed to somewhere else during plugin execution) to\n        # ensure that this lock will be acquired after the Certbot package\n        # acquires all of its locks. (Tests that Certbot calls prepare after\n        # acquiring its locks are part of the Certbot package's tests.) Not\n        # doing this could result in deadlock from two versions of Certbot that\n        # acquire its locks in a different order.\n        server_root = self.config.conf(\"server-root\")\n\n        from certbot import util as certbot_util\n        certbot_util._LOCKS[server_root].release()  # pylint: disable=protected-access\n\n        self.config.config_test = mock.Mock()\n        certbot_test_util.lock_and_call(self._test_prepare_locked, server_root)\n\n    @mock.patch(\"certbot_nginx._internal.configurator.util.exe_exists\")\n    def _test_prepare_locked(self, unused_exe_exists):\n        try:\n            self.config.prepare()\n        except errors.PluginError as err:\n            err_msg = str(err)\n            assert \"lock\" in err_msg\n            assert self.config.conf(\"server-root\") in err_msg\n        else:  # pragma: no cover\n            self.fail(\"Exception wasn't raised!\")\n\n    @mock.patch(\"certbot_nginx._internal.configurator.socket.gethostname\")\n    @mock.patch(\"certbot_nginx._internal.configurator.socket.gethostbyaddr\")\n    def test_get_all_names(self, mock_gethostbyaddr, mock_gethostname):\n        mock_gethostbyaddr.return_value = ('155.225.50.69.nephoscale.net', [], [])\n        mock_gethostname.return_value = ('example.net')\n        names = self.config.get_all_names()\n        assert names == {\n            \"155.225.50.69.nephoscale.net\", \"www.example.org\", \"another.alias\",\n             \"migration.com\", \"summer.com\", \"geese.com\", \"sslon.com\",\n             \"globalssl.com\", \"globalsslsetssl.com\", \"ipv6.com\", \"ipv6ssl.com\",\n             \"headers.com\", \"example.net\", \"ssl.both.com\", \"no-listens.com\",\n             \"addr-80.com\"}\n\n    def test_supported_enhancements(self):\n        assert ['redirect', 'ensure-http-header', 'staple-ocsp'] == \\\n                         self.config.supported_enhancements()\n\n    def test_enhance(self):\n        with pytest.raises(errors.PluginError):\n            self.config.enhance('myhost', 'unknown_enhancement')\n\n    def test_get_chall_pref(self):\n        assert [challenges.HTTP01] == \\\n                         self.config.get_chall_pref('myhost')\n\n    def test_save(self):\n        filep = self.config.parser.abs_path('sites-enabled/example.com')\n        mock_vhost = obj.VirtualHost(filep,\n                                     None, None, None,\n                                     {'.example.com', 'example.*'},\n                                     None, [0])\n        self.config.parser.add_server_directives(\n            mock_vhost,\n            [['listen', ' ', '5001', ' ', 'ssl']])\n        self.config.save()\n\n        # pylint: disable=protected-access\n        parsed = self.config.parser._parse_files(filep, override=True)\n        assert [[['server'],\n                           [['listen', '69.50.225.155:9000'],\n                            ['listen', '127.0.0.1'],\n                            ['server_name', '.example.com'],\n                            ['server_name', 'example.*'],\n                            ['listen', '5001', 'ssl'],\n                            ['#', parser.COMMENT]]]] == \\\n                         parsed[filep]\n\n    def test_choose_or_make_vhosts_alias(self):\n        self._test_choose_or_make_vhosts_common('alias', 'server_conf')\n\n    def test_choose_or_make_vhosts_example_com(self):\n        self._test_choose_or_make_vhosts_common('example.com', 'example_conf')\n\n    def test_choose_or_make_vhosts_localhost(self):\n        self._test_choose_or_make_vhosts_common('localhost', 'localhost_conf')\n\n    def test_choose_or_make_vhosts_example_com_uk_test(self):\n        self._test_choose_or_make_vhosts_common('example.com.uk.test', 'example_conf')\n\n    def test_choose_or_make_vhosts_www_example_com(self):\n        self._test_choose_or_make_vhosts_common('www.example.com', 'example_conf')\n\n    def test_choose_or_make_vhosts_test_www_example_com(self):\n        self._test_choose_or_make_vhosts_common('test.www.example.com', 'foo_conf')\n\n    def test_choose_or_make_vhosts_abc_www_foo_com(self):\n        self._test_choose_or_make_vhosts_common('abc.www.foo.com', 'foo_conf')\n\n    def test_choose_or_make_vhosts_www_bar_co_uk(self):\n        self._test_choose_or_make_vhosts_common('www.bar.co.uk', 'localhost_conf')\n\n    def test_choose_or_make_vhosts_ipv6_com(self):\n        self._test_choose_or_make_vhosts_common('ipv6.com', 'ipv6_conf')\n\n    def _test_choose_or_make_vhosts_common(self, name, conf):\n        conf_names = {'localhost_conf': {'localhost', r'~^(www\\.)?(example|bar)\\.'},\n                 'server_conf': {'somename', 'another.alias', 'alias'},\n                 'example_conf': {'.example.com', 'example.*'},\n                 'foo_conf': {'*.www.foo.com', '*.www.example.com'},\n                 'ipv6_conf': {'ipv6.com'}}\n\n        conf_path = {'localhost': \"etc_nginx/nginx.conf\",\n                   'alias': \"etc_nginx/nginx.conf\",\n                   'example.com': \"etc_nginx/sites-enabled/example.com\",\n                   'example.com.uk.test': \"etc_nginx/sites-enabled/example.com\",\n                   'www.example.com': \"etc_nginx/sites-enabled/example.com\",\n                   'test.www.example.com': \"etc_nginx/foo.conf\",\n                   'abc.www.foo.com': \"etc_nginx/foo.conf\",\n                   'www.bar.co.uk': \"etc_nginx/nginx.conf\",\n                   'ipv6.com': \"etc_nginx/sites-enabled/ipv6.com\"}\n        conf_path = {key: os.path.normpath(value) for key, value in conf_path.items()}\n\n        vhost = self.config.choose_or_make_vhosts(name, 'key.pem', 'fullchain.pem')[0]\n        path = os.path.relpath(vhost.filep, self.temp_dir)\n\n        assert conf_names[conf] == vhost.names\n        assert conf_path[name] == path\n        # IPv6 specific checks\n        if name == \"ipv6.com\":\n            assert vhost.ipv6_enabled()\n            # Make sure that we have SSL enabled also for IPv6 addr\n            assert any(True for x in vhost.addrs if x.ssl and x.ipv6)\n\n    def test_choose_or_make_vhosts_bad(self):\n        bad_results = ['www.foo.com', 'example', 't.www.bar.co',\n                       '69.255.225.155']\n\n        for name in bad_results:\n            with self.subTest(name=name):\n                with pytest.raises(errors.MisconfigurationError):\n                    self.config.choose_or_make_vhosts(name, 'key.pem', 'fullchain.pem')\n\n    def test_choose_or_make_vhosts_keep_ip_address(self):\n        # let's use a simple helper function to set key and fullchain values\n        def choose_or_make_vhosts(domain):\n            return self.config.choose_or_make_vhosts(domain, 'key.pem', 'fullchain.pem')\n\n        # no listen on port 80\n        # listen       69.50.225.155:9000;\n        # listen       127.0.0.1;\n        vhost = choose_or_make_vhosts('example.com')[0]\n        assert obj.Addr.fromstring(\"5001 ssl\") in vhost.addrs\n\n        # no listens at all\n        vhost = choose_or_make_vhosts('no-listens.com')[0]\n        assert obj.Addr.fromstring(\"5001 ssl\") in vhost.addrs\n        assert obj.Addr.fromstring(\"80\") in vhost.addrs\n\n        # blank addr listen on 80 should result in blank addr ssl\n        # listen 80;\n        # listen [::]:80;\n        vhost = choose_or_make_vhosts('ipv6.com')[0]\n        assert obj.Addr.fromstring(\"5001 ssl\") in vhost.addrs\n        assert obj.Addr.fromstring(\"[::]:5001 ssl\") in vhost.addrs\n\n        # listen on 80 with ip address should result in copied addr\n        # listen 1.2.3.4:80;\n        # listen [1:20::300]:80;\n        vhost = choose_or_make_vhosts('addr-80.com')[0]\n        assert obj.Addr.fromstring(\"1.2.3.4:5001 ssl\") in vhost.addrs\n        assert obj.Addr.fromstring(\"[1:20::300]:5001 ssl ipv6only=on\") in vhost.addrs\n\n    def test_choose_or_make_vhost_ssl_directives(self):\n        conf_path = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.choose_or_make_vhosts('example.com', 'my-key.pem', 'my-fullchain.pem')\n        self.config.save()\n        self.config.parser.load()\n        parsed_conf = util.filter_comments(self.config.parser.parsed[conf_path])\n\n        expected_directives = [\n            ['ssl_certificate', 'my-fullchain.pem'],\n            ['ssl_certificate_key', 'my-key.pem'],\n        ]\n        for directive in expected_directives:\n            assert util.contains_at_depth(parsed_conf, directive, 2)\n\n    def test_ipv6only(self):\n        # ipv6_info: (ipv6_active, ipv6only_present)\n        assert (True, False) == self.config.ipv6_info(\"[::]\", \"80\")\n        # Port 443 has ipv6only=on because of ipv6ssl.com vhost\n        assert (True, True) == self.config.ipv6_info(\"[::]\", \"443\")\n\n    def test_ipv6only_detection(self):\n        self.config.version = (1, 3, 1)\n\n        vhost = self.config.choose_or_make_vhosts(\"ipv6.com\", \"key.pem\", \"fullchain.pem\")[0]\n        for addr in vhost.addrs:\n            assert not addr.ipv6only\n\n    def test_more_info(self):\n        assert 'nginx.conf' in self.config.more_info()\n\n    def test_deploy_cert_requires_fullchain_path(self):\n        self.config.version = (1, 3, 1)\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\"www.example.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            None)\n\n    @mock.patch('certbot_nginx._internal.parser.NginxParser.update_or_add_server_directives')\n    def test_deploy_cert_raise_on_add_error(self, mock_update_or_add_server_directives):\n        mock_update_or_add_server_directives.side_effect = errors.MisconfigurationError()\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\"migration.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n\n    def test_deploy_cert(self):\n        server_conf = self.config.parser.abs_path('server.conf')\n        nginx_conf = self.config.parser.abs_path('nginx.conf')\n        example_conf = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.version = (1, 3, 1)\n\n        # Get the default SSL vhost\n        self.config.deploy_cert(\n            \"www.example.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.deploy_cert(\n            \"another.alias\",\n            \"/etc/nginx/cert.pem\",\n            \"/etc/nginx/key.pem\",\n            \"/etc/nginx/chain.pem\",\n            \"/etc/nginx/fullchain.pem\")\n        self.config.save()\n\n        self.config.parser.load()\n\n        parsed_example_conf = util.filter_comments(self.config.parser.parsed[example_conf])\n        parsed_server_conf = util.filter_comments(self.config.parser.parsed[server_conf])\n        parsed_nginx_conf = util.filter_comments(self.config.parser.parsed[nginx_conf])\n\n        assert [[['server'],\n                           [\n                            ['listen', '69.50.225.155:9000'],\n                            ['listen', '127.0.0.1'],\n                            ['server_name', '.example.com'],\n                            ['server_name', 'example.*'],\n\n                            ['listen', '5001', 'ssl'],\n                            ['ssl_certificate', 'example/fullchain.pem'],\n                            ['ssl_certificate_key', 'example/key.pem'],\n                            ['include', self.config.mod_ssl_conf],\n                            ['ssl_dhparam', self.config.ssl_dhparams],\n                            ]]] == \\\n                         parsed_example_conf\n        assert [['server_name', 'somename', 'alias', 'another.alias']] == \\\n                         parsed_server_conf\n        assert util.contains_at_depth(\n            parsed_nginx_conf,\n            [['server'],\n             [\n              ['listen', '8000'],\n              ['listen', 'somename:8080'],\n              ['include', 'server.conf'],\n              [['location', '/'],\n               [['root', 'html'],\n                ['index', 'index.html', 'index.htm']]],\n              ['listen', '5001', 'ssl'],\n              ['ssl_certificate', '/etc/nginx/fullchain.pem'],\n              ['ssl_certificate_key', '/etc/nginx/key.pem'],\n              ['include', self.config.mod_ssl_conf],\n              ['ssl_dhparam', self.config.ssl_dhparams],\n            ]],\n            2)\n\n    def test_deploy_cert_add_explicit_listen(self):\n        migration_conf = self.config.parser.abs_path('sites-enabled/migration.com')\n        self.config.deploy_cert(\n            \"summer.com\",\n            \"summer/cert.pem\",\n            \"summer/key.pem\",\n            \"summer/chain.pem\",\n            \"summer/fullchain.pem\")\n        self.config.save()\n        self.config.parser.load()\n        parsed_migration_conf = util.filter_comments(self.config.parser.parsed[migration_conf])\n        assert [['server'],\n                          [\n                           ['server_name', 'migration.com'],\n                           ['server_name', 'summer.com'],\n\n                           ['listen', '80'],\n                           ['listen', '5001', 'ssl'],\n                           ['ssl_certificate', 'summer/fullchain.pem'],\n                           ['ssl_certificate_key', 'summer/key.pem'],\n                           ['include', self.config.mod_ssl_conf],\n                           ['ssl_dhparam', self.config.ssl_dhparams],\n                           ]] == \\\n                         parsed_migration_conf[0]\n\n    @mock.patch(\"certbot_nginx._internal.configurator.http_01.NginxHttp01.perform\")\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.restart\")\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.revert_challenge_config\")\n    def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_http_perform):\n        # Only tests functionality specific to configurator.perform\n        # Note: As more challenges are offered this will have to be expanded\n        achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=messages.ChallengeBody(\n                chall=challenges.HTTP01(token=b\"m8TdO1qik4JVFtgPPurJmg\"),\n                uri=\"https://ca.org/chall1_uri\",\n                status=messages.Status(\"pending\"),\n            ),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"example.com\"),\n            account_key=self.rsa512jwk)\n\n        expected = [\n            achall.response(self.rsa512jwk),\n        ]\n\n        mock_http_perform.return_value = expected[:]\n        responses = self.config.perform([achall])\n\n        assert mock_http_perform.call_count == 1\n        assert responses == expected\n\n        self.config.cleanup([achall])\n        assert 0 == self.config._chall_out # pylint: disable=protected-access\n        assert mock_revert.call_count == 1\n        assert mock_restart.call_count == 2\n\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    def test_get_version(self, mock_run):\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/1.4.2\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"TLS SNI support enabled\",\n                           \"configure arguments: --prefix=/usr/local/Cellar/\"\n                           \"nginx/1.6.2 --with-http_ssl_module\"])\n        assert self.config.get_version() == (1, 4, 2)\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/0.9\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"TLS SNI support enabled\",\n                           \"configure arguments: --with-http_ssl_module\"])\n        assert self.config.get_version() == (0, 9)\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"blah 0.0.1\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"TLS SNI support enabled\",\n                           \"configure arguments: --with-http_ssl_module\"])\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/1.4.2\",\n                           \"TLS SNI support enabled\"])\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/1.4.2\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"configure arguments: --with-http_ssl_module\"])\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\\n\".join(\n                          [\"nginx version: nginx/0.8.1\",\n                           \"built by clang 6.0 (clang-600.0.56)\"\n                           \" (based on LLVM 3.5svn)\",\n                           \"TLS SNI support enabled\",\n                           \"configure arguments: --with-http_ssl_module\"])\n        with pytest.raises(errors.NotSupportedError):\n            self.config.get_version()\n\n        mock_run.side_effect = OSError(\"Can't find program\")\n        with pytest.raises(errors.PluginError):\n            self.config.get_version()\n\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    def test_get_openssl_version(self, mock_run):\n        # pylint: disable=protected-access\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                built with OpenSSL 1.0.2g  1 Mar 2016\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"1.0.2g\"\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                built with OpenSSL 1.0.2-beta1  1 Mar 2016\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"1.0.2-beta1\"\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                built with OpenSSL 1.0.2  1 Mar 2016\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"1.0.2\"\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                built with OpenSSL 1.0.2g  1 Mar 2016 (running with OpenSSL 1.0.2a  1 Mar 2016)\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"1.0.2a\"\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                built with LibreSSL 2.2.2\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"\"\n\n        mock_run.return_value.stdout = \"\"\n        mock_run.return_value.stderr = \"\"\"\n                nginx version: nginx/1.15.5\n                built by gcc 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)\n                TLS SNI support enabled\n                configure arguments:\n            \"\"\"\n        assert self.config._get_openssl_version() == \"\"\n\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    @mock.patch(\"certbot_nginx._internal.configurator.time\")\n    def test_nginx_restart(self, mock_time, mock_run):\n        mocked = mock_run.return_value\n        mocked.stdout = ''\n        mocked.stderr = ''\n        mocked.returncode = 0\n        self.config.restart()\n        assert mock_run.call_count == 1\n        mock_time.sleep.assert_called_once_with(0.1234)\n\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    @mock.patch(\"certbot_nginx._internal.configurator.logger.debug\")\n    def test_nginx_restart_fail(self, mock_log_debug, mock_run):\n        mocked = mock_run.return_value\n        mocked.stdout = ''\n        mocked.stderr = ''\n        mocked.returncode = 1\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.restart()\n        assert mock_run.call_count == 2\n        mock_log_debug.assert_called_once_with(\"nginx reload failed:\\n%s\", \"\")\n\n    @mock.patch(\"certbot_nginx._internal.configurator.subprocess.run\")\n    def test_no_nginx_start(self, mock_run):\n        mock_run.side_effect = OSError(\"Can't find program\")\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.restart()\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_config_test_bad_process(self, mock_run_script):\n        mock_run_script.side_effect = errors.SubprocessError\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.config_test()\n\n    @mock.patch(\"certbot.util.run_script\")\n    def test_config_test(self, _):\n        self.config.config_test()\n\n    @mock.patch(\"certbot.reverter.Reverter.recovery_routine\")\n    def test_recovery_routine_throws_error_from_reverter(self, mock_recovery_routine):\n        mock_recovery_routine.side_effect = errors.ReverterError(\"foo\")\n        with pytest.raises(errors.PluginError):\n            self.config.recovery_routine()\n\n    @mock.patch(\"certbot.reverter.Reverter.rollback_checkpoints\")\n    def test_rollback_checkpoints_throws_error_from_reverter(self, mock_rollback_checkpoints):\n        mock_rollback_checkpoints.side_effect = errors.ReverterError(\"foo\")\n        with pytest.raises(errors.PluginError):\n            self.config.rollback_checkpoints()\n\n    @mock.patch(\"certbot.reverter.Reverter.revert_temporary_config\")\n    def test_revert_challenge_config_throws_error_from_reverter(self, mock_revert_temporary_config):\n        mock_revert_temporary_config.side_effect = errors.ReverterError(\"foo\")\n        with pytest.raises(errors.PluginError):\n            self.config.revert_challenge_config()\n\n    @mock.patch(\"certbot.reverter.Reverter.add_to_checkpoint\")\n    def test_save_throws_error_from_reverter(self, mock_add_to_checkpoint):\n        mock_add_to_checkpoint.side_effect = errors.ReverterError(\"foo\")\n        with pytest.raises(errors.PluginError):\n            self.config.save()\n\n    def test_redirect_enhance(self):\n        # Test that we successfully add a redirect when there is\n        # a listen directive\n        expected = UnspacedList(_redirect_block_for_domain(\"www.example.com\"))[0]\n\n        example_conf = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.enhance(\"www.example.com\", \"redirect\")\n\n        generated_conf = self.config.parser.parsed[example_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2) is True\n\n        # Test that we successfully add a redirect when there is\n        # no listen directive\n        migration_conf = self.config.parser.abs_path('sites-enabled/migration.com')\n        self.config.enhance(\"migration.com\", \"redirect\")\n\n        expected = UnspacedList(_redirect_block_for_domain(\"migration.com\"))[0]\n\n        generated_conf = self.config.parser.parsed[migration_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2) is True\n\n    def test_split_for_redirect(self):\n        example_conf = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.deploy_cert(\n            \"example.org\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.enhance(\"www.example.com\", \"redirect\")\n        generated_conf = self.config.parser.parsed[example_conf]\n        assert [[['server'], [\n               ['server_name', '.example.com'],\n               ['server_name', 'example.*'],\n               ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],\n               ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],\n               ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],\n               ['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'],\n               ['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'],\n               [], []]],\n             [['server'], [\n               [['if', '($host', '=', 'www.example.com)'], [\n                 ['return', '301', 'https://$host$request_uri']]],\n               ['#', ' managed by Certbot'], [],\n               ['listen', '69.50.225.155:9000'],\n               ['listen', '127.0.0.1'],\n               ['server_name', '.example.com'],\n               ['server_name', 'example.*'],\n               ['return', '404'], ['#', ' managed by Certbot'], [], [], []]]] == \\\n            generated_conf\n\n    def test_split_for_headers(self):\n        example_conf = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.deploy_cert(\n            \"example.org\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.enhance(\"www.example.com\", \"ensure-http-header\", \"Strict-Transport-Security\")\n        generated_conf = self.config.parser.parsed[example_conf]\n        assert [[['server'], [\n               ['server_name', '.example.com'],\n               ['server_name', 'example.*'],\n               ['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],\n               ['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],\n               ['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],\n               ['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'],\n               ['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'],\n               [], [],\n               ['add_header', 'Strict-Transport-Security', '\"max-age=31536000\"', 'always'],\n               ['#', ' managed by Certbot'],\n               [], []]],\n             [['server'], [\n               ['listen', '69.50.225.155:9000'],\n               ['listen', '127.0.0.1'],\n               ['server_name', '.example.com'],\n               ['server_name', 'example.*'],\n               [], []]]] == \\\n            generated_conf\n\n    def test_http_header_hsts(self):\n        # we need to select an SSL enabled vhost here for the HSTS enhancement\n        example_conf = self.config.parser.abs_path('sites-enabled/migration.com')\n        self.config.enhance(\"migration.com\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n        expected = ['add_header', 'Strict-Transport-Security', '\"max-age=31536000\"', 'always']\n        generated_conf = self.config.parser.parsed[example_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2) is True\n\n    def test_multiple_headers_hsts(self):\n        # we need to select an SSL enabled vhost here for the HSTS enhancement\n        headers_conf = self.config.parser.abs_path('sites-enabled/headers.com')\n        self.config.enhance(\"headers.com\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n        expected = ['add_header', 'Strict-Transport-Security', '\"max-age=31536000\"', 'always']\n        generated_conf = self.config.parser.parsed[headers_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2) is True\n\n    def test_http_header_hsts_twice(self):\n        # we need to select an SSL enabled vhost here for the HSTS enhancement\n        self.config.enhance(\"migration.com\", \"ensure-http-header\",\n                            \"Strict-Transport-Security\")\n        with pytest.raises(errors.PluginEnhancementAlreadyPresent):\n            self.config.enhance(\"migration.com\",\n            \"ensure-http-header\", \"Strict-Transport-Security\")\n\n    @mock.patch('certbot_nginx._internal.obj.VirtualHost.contains_list')\n    def test_certbot_redirect_exists(self, mock_contains_list):\n        # Test that we add no redirect statement if there is already a\n        # redirect in the block that is managed by certbot\n        # Has a certbot redirect\n        mock_contains_list.return_value = True\n        with mock.patch(\"certbot_nginx._internal.configurator.logger\") as mock_logger:\n            self.config.enhance(\"www.example.com\", \"redirect\")\n            assert mock_logger.info.call_args[0][0] == \\\n                \"Traffic on port %s already redirecting to ssl in %s\"\n\n    def test_redirect_dont_enhance(self):\n        # Test that we don't accidentally add redirect to ssl-only block\n        with mock.patch(\"certbot_nginx._internal.configurator.logger\") as mock_logger:\n            self.config.enhance(\"geese.com\", \"redirect\")\n        assert mock_logger.info.call_args[0][0] == \\\n                'No matching insecure server blocks listening on port %s found.'\n\n    def test_double_redirect(self):\n        # Test that we add one redirect for each domain\n        example_conf = self.config.parser.abs_path('sites-enabled/example.com')\n        self.config.enhance(\"example.com\", \"redirect\")\n        self.config.enhance(\"example.org\", \"redirect\")\n\n        expected1 = UnspacedList(_redirect_block_for_domain(\"example.com\"))[0]\n        expected2 = UnspacedList(_redirect_block_for_domain(\"example.org\"))[0]\n\n        generated_conf = self.config.parser.parsed[example_conf]\n        assert util.contains_at_depth(generated_conf, expected1, 2)\n        assert util.contains_at_depth(generated_conf, expected2, 2)\n\n    def test_staple_ocsp_bad_version(self):\n        self.config.version = (1, 3, 1)\n        with pytest.raises(errors.PluginError):\n            self.config.enhance(\"www.example.com\", \"staple-ocsp\", \"chain_path\")\n\n    def test_staple_ocsp_no_chain_path(self):\n        with pytest.raises(errors.PluginError):\n            self.config.enhance(\"www.example.com\", \"staple-ocsp\", None)\n\n    def test_staple_ocsp_internal_error(self):\n        # we need to select an SSL enabled vhost here for the OCSP stapling\n        # enhancement\n        self.config.enhance(\"sslon.com\", \"staple-ocsp\", \"chain_path\")\n        # error is raised because the server block has conflicting directives\n        with pytest.raises(errors.PluginError):\n            self.config.enhance(\"sslon.com\", \"staple-ocsp\", \"different_path\")\n\n    def test_staple_ocsp(self):\n        chain_path = \"example/chain.pem\"\n        # we need to select an SSL enabled vhost here for the OCSP stapling\n        # enhancement\n        self.config.enhance(\"sslon.com\", \"staple-ocsp\", chain_path)\n\n        example_conf = self.config.parser.abs_path('sites-enabled/sslon.com')\n        generated_conf = self.config.parser.parsed[example_conf]\n\n        assert util.contains_at_depth(\n            generated_conf,\n            ['ssl_trusted_certificate', 'example/chain.pem'], 2)\n        assert util.contains_at_depth(\n            generated_conf, ['ssl_stapling', 'on'], 2)\n        assert util.contains_at_depth(\n            generated_conf, ['ssl_stapling_verify', 'on'], 2)\n\n    def test_deploy_no_match_default_set(self):\n        default_conf = self.config.parser.abs_path('sites-enabled/default')\n        foo_conf = self.config.parser.abs_path('foo.conf')\n        del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server\n        self.config.version = (1, 3, 1)\n\n        self.config.deploy_cert(\n            \"www.nomatch.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.save()\n\n        self.config.parser.load()\n\n        parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf])\n\n        assert [[['server'],\n                           [['listen', 'myhost', 'default_server'],\n                            ['listen', 'otherhost', 'default_server'],\n                            ['server_name', '\"www.example.org\"'],\n                            [['location', '/'],\n                             [['root', 'html'],\n                              ['index', 'index.html', 'index.htm']]]]],\n                          [['server'],\n                           [['listen', 'myhost'],\n                            ['listen', 'otherhost'],\n                            ['server_name', 'www.nomatch.com'],\n                            [['location', '/'],\n                             [['root', 'html'],\n                              ['index', 'index.html', 'index.htm']]],\n                            ['listen', '5001', 'ssl'],\n                            ['ssl_certificate', 'example/fullchain.pem'],\n                            ['ssl_certificate_key', 'example/key.pem'],\n                            ['include', self.config.mod_ssl_conf],\n                            ['ssl_dhparam', self.config.ssl_dhparams]]]] == \\\n                         parsed_default_conf\n\n        self.config.deploy_cert(\n            \"nomatch.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.save()\n\n        self.config.parser.load()\n\n        parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf])\n\n        assert util.contains_at_depth(parsed_default_conf, \"nomatch.com\", 3)\n\n    def test_deploy_no_match_default_set_multi_level_path(self):\n        default_conf = self.config.parser.abs_path('sites-enabled/default')\n        foo_conf = self.config.parser.abs_path('foo.conf')\n        del self.config.parser.parsed[default_conf][0][1][0]\n        del self.config.parser.parsed[default_conf][0][1][0]\n        self.config.version = (1, 3, 1)\n\n        self.config.deploy_cert(\n            \"www.nomatch.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n        self.config.save()\n\n        self.config.parser.load()\n\n        parsed_foo_conf = util.filter_comments(self.config.parser.parsed[foo_conf])\n\n        assert [['server'],\n                          [['listen', '*:80', 'ssl'],\n                          ['server_name', 'www.nomatch.com'],\n                          ['root', '/home/ubuntu/sites/foo/'],\n                          [['location', '/status'], [[['types'], [['image/jpeg', 'jpg']]]]],\n                          [['location', '~', 'case_sensitive\\\\.php$'], [['index', 'index.php'],\n                           ['root', '/var/root']]],\n                          [['location', '~*', 'case_insensitive\\\\.php$'], []],\n                          [['location', '=', 'exact_match\\\\.php$'], []],\n                          [['location', '^~', 'ignore_regex\\\\.php$'], []],\n                          ['ssl_certificate', 'example/fullchain.pem'],\n                          ['ssl_certificate_key', 'example/key.pem']]] == \\\n                         parsed_foo_conf[1][1][1]\n\n    def test_deploy_no_match_no_default_set(self):\n        default_conf = self.config.parser.abs_path('sites-enabled/default')\n        foo_conf = self.config.parser.abs_path('foo.conf')\n        del self.config.parser.parsed[default_conf][0][1][0]\n        del self.config.parser.parsed[default_conf][0][1][0]\n        del self.config.parser.parsed[foo_conf][2][1][0][1][0]\n        self.config.version = (1, 3, 1)\n\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.deploy_cert(\"www.nomatch.com\", \"example/cert.pem\", \"example/key.pem\",\n            \"example/chain.pem\", \"example/fullchain.pem\")\n\n    def test_deploy_no_match_fail_multiple_defaults(self):\n        self.config.version = (1, 3, 1)\n        with pytest.raises(errors.MisconfigurationError):\n            self.config.deploy_cert(\"www.nomatch.com\", \"example/cert.pem\", \"example/key.pem\",\n            \"example/chain.pem\", \"example/fullchain.pem\")\n\n    def test_deploy_no_match_multiple_defaults_ok(self):\n        foo_conf = self.config.parser.abs_path('foo.conf')\n        self.config.parser.parsed[foo_conf][2][1][0][1][0][1] = '*:5001'\n        self.config.version = (1, 3, 1)\n        self.config.deploy_cert(\"www.nomatch.com\", \"example/cert.pem\", \"example/key.pem\",\n            \"example/chain.pem\", \"example/fullchain.pem\")\n\n    def test_deploy_no_match_add_redirect(self):\n        default_conf = self.config.parser.abs_path('sites-enabled/default')\n        foo_conf = self.config.parser.abs_path('foo.conf')\n        del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server\n        self.config.version = (1, 3, 1)\n\n        self.config.deploy_cert(\n            \"www.nomatch.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n\n        self.config.deploy_cert(\n            \"nomatch.com\",\n            \"example/cert.pem\",\n            \"example/key.pem\",\n            \"example/chain.pem\",\n            \"example/fullchain.pem\")\n\n        self.config.enhance(\"www.nomatch.com\", \"redirect\")\n\n        self.config.save()\n\n        self.config.parser.load()\n\n        expected = UnspacedList(_redirect_block_for_domain(\"www.nomatch.com\"))[0]\n\n        generated_conf = self.config.parser.parsed[default_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2)\n\n    @mock.patch('certbot.reverter.logger')\n    @mock.patch('certbot_nginx._internal.parser.NginxParser.load')\n    def test_parser_reload_after_config_changes(self, mock_parser_load, unused_mock_logger):\n        self.config.recovery_routine()\n        self.config.revert_challenge_config()\n        self.config.rollback_checkpoints()\n        assert mock_parser_load.call_count == 3\n\n    def test_choose_vhosts_wildcard(self):\n        # pylint: disable=protected-access\n        mock_path = \"certbot_nginx._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            vhost = [x for x in self.config.parser.get_vhosts()\n              if 'summer.com' in x.names][0]\n            mock_select_vhs.return_value = [vhost]\n            vhs = self.config._choose_vhosts_wildcard(\"*.com\",\n                                                     prefer_ssl=True)\n            # Check that the dialog was called with migration.com\n            assert vhost in mock_select_vhs.call_args[0][0]\n\n            # And the actual returned values\n            assert len(vhs) == 1\n            assert vhs[0] == vhost\n\n    def test_choose_vhosts_wildcard_redirect(self):\n        # pylint: disable=protected-access\n        mock_path = \"certbot_nginx._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            vhost = [x for x in self.config.parser.get_vhosts()\n              if 'summer.com' in x.names][0]\n            mock_select_vhs.return_value = [vhost]\n            vhs = self.config._choose_vhosts_wildcard(\"*.com\",\n                                                     prefer_ssl=False)\n            # Check that the dialog was called with migration.com\n            assert vhost in mock_select_vhs.call_args[0][0]\n\n            # And the actual returned values\n            assert len(vhs) == 1\n            assert vhs[0] == vhost\n\n    def test_deploy_cert_wildcard(self):\n        # pylint: disable=protected-access\n        mock_choose_vhosts = mock.MagicMock()\n        vhost = [x for x in self.config.parser.get_vhosts()\n            if 'geese.com' in x.names][0]\n        mock_choose_vhosts.return_value = [vhost]\n        self.config._choose_vhosts_wildcard = mock_choose_vhosts\n        mock_d = \"certbot_nginx._internal.configurator.NginxConfigurator._deploy_cert\"\n        with mock.patch(mock_d) as mock_dep:\n            self.config.deploy_cert(\"*.com\", \"/tmp/path\",\n                                    \"/tmp/path\", \"/tmp/path\", \"/tmp/path\")\n            assert mock_dep.called\n            assert len(mock_dep.call_args_list) == 1\n            assert vhost == mock_dep.call_args_list[0][0][0]\n\n    @mock.patch(\"certbot_nginx._internal.display_ops.select_vhost_multiple\")\n    def test_deploy_cert_wildcard_no_vhosts(self, mock_dialog):\n        # pylint: disable=protected-access\n        mock_dialog.return_value = []\n        with pytest.raises(errors.PluginError):\n            self.config.deploy_cert(\"*.wild.cat\", \"/tmp/path\", \"/tmp/path\",\n                           \"/tmp/path\", \"/tmp/path\")\n\n    @mock.patch(\"certbot_nginx._internal.display_ops.select_vhost_multiple\")\n    def test_enhance_wildcard_ocsp_after_install(self, mock_dialog):\n        # pylint: disable=protected-access\n        vhost = [x for x in self.config.parser.get_vhosts()\n            if 'geese.com' in x.names][0]\n        self.config._wildcard_vhosts[\"*.com\"] = [vhost]\n        self.config.enhance(\"*.com\", \"staple-ocsp\", \"example/chain.pem\")\n        assert not mock_dialog.called\n\n    @mock.patch(\"certbot_nginx._internal.display_ops.select_vhost_multiple\")\n    def test_enhance_wildcard_redirect_or_ocsp_no_install(self, mock_dialog):\n        # we need to select an SSL enabled vhost here for the OCSP stapling\n        # enhancement\n        vhost = [x for x in self.config.parser.get_vhosts()\n                 if 'geese.com' in x.names][0]\n        mock_dialog.return_value = [vhost]\n        self.config.enhance(\"*.com\", \"staple-ocsp\", \"example/chain.pem\")\n        assert mock_dialog.called is True\n\n    @mock.patch(\"certbot_nginx._internal.display_ops.select_vhost_multiple\")\n    def test_enhance_wildcard_double_redirect(self, mock_dialog):\n      # pylint: disable=protected-access\n        vhost = [x for x in self.config.parser.get_vhosts()\n            if 'summer.com' in x.names][0]\n        self.config._wildcard_redirect_vhosts[\"*.com\"] = [vhost]\n        self.config.enhance(\"*.com\", \"redirect\")\n        assert not mock_dialog.called\n\n    def test_choose_vhosts_wildcard_no_ssl_filter_port(self):\n        # pylint: disable=protected-access\n        mock_path = \"certbot_nginx._internal.display_ops.select_vhost_multiple\"\n        with mock.patch(mock_path) as mock_select_vhs:\n            mock_select_vhs.return_value = []\n            self.config._choose_vhosts_wildcard(\"*.com\",\n                                                prefer_ssl=False,\n                                                no_ssl_filter_port='80')\n            # Check that the dialog was called with only port 80 vhosts\n            assert len(mock_select_vhs.call_args[0][0]) == 9\n\n    def test_choose_auth_vhosts(self):\n        \"\"\"choose_auth_vhosts correctly selects duplicative and HTTP/HTTPS vhosts\"\"\"\n        http, https = self.config.choose_auth_vhosts('ssl.both.com')\n        assert len(http) == 4\n        assert len(https) == 2\n        assert http[0].names == {'ssl.both.com'}\n        assert http[1].names == {'ssl.both.com'}\n        assert http[2].names == {'ssl.both.com'}\n        assert http[3].names == {'*.both.com'}\n        assert https[0].names == {'ssl.both.com'}\n        assert https[1].names == {'*.both.com'}\n\n\nclass InstallSslOptionsConfTest(util.NginxTest):\n    \"\"\"Test that the options-ssl-nginx.conf file is installed and updated properly.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n\n        self.config = self.get_nginx_configurator(\n            self.config_path, self.config_dir, self.work_dir, self.logs_dir)\n\n    def _call(self):\n        self.config.install_ssl_options_conf(self.config.mod_ssl_conf,\n            self.config.updated_mod_ssl_conf_digest)\n\n    def _current_ssl_options_hash(self):\n        return crypto_util.sha256sum(self.config.mod_ssl_conf_src)\n\n    def _assert_current_file(self):\n        assert os.path.isfile(self.config.mod_ssl_conf)\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf) == \\\n            self._current_ssl_options_hash()\n\n    def test_no_file(self):\n        # prepare should have placed a file there\n        self._assert_current_file()\n        os.remove(self.config.mod_ssl_conf)\n        assert not os.path.isfile(self.config.mod_ssl_conf)\n        self._call()\n        self._assert_current_file()\n\n    def test_current_file(self):\n        self._assert_current_file()\n        self._call()\n        self._assert_current_file()\n\n    def _mock_hash_except_ssl_conf_src(self, fake_hash):\n        # Write a bad file in place so that update tests fail if no update occurs.\n        # We're going to pretend this file (the currently installed conf file)\n        # actually hashes to `fake_hash` for the update tests.\n        with open(self.config.mod_ssl_conf, \"w\") as f:\n            f.write(\"bogus\")\n        sha256 = crypto_util.sha256sum\n        def _hash(filename):\n            return sha256(filename) if filename == self.config.mod_ssl_conf_src else fake_hash\n        return _hash\n\n    def test_prev_file_updates_to_current(self):\n        from certbot_nginx._internal.constants import ALL_SSL_OPTIONS_HASHES\n        with mock.patch('certbot.crypto_util.sha256sum',\n                new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])):\n            self._call()\n        self._assert_current_file()\n\n    def test_prev_file_updates_to_current_old_nginx(self):\n        from certbot_nginx._internal.constants import ALL_SSL_OPTIONS_HASHES\n        self.config.version = (1, 5, 8)\n        with mock.patch('certbot.crypto_util.sha256sum',\n                new=self._mock_hash_except_ssl_conf_src(ALL_SSL_OPTIONS_HASHES[0])):\n            self._call()\n        self._assert_current_file()\n\n    def test_manually_modified_current_file_does_not_update(self):\n        with open(self.config.mod_ssl_conf, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert not mock_logger.warning.called\n        assert os.path.isfile(self.config.mod_ssl_conf)\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf_src) == \\\n            self._current_ssl_options_hash()\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf) != \\\n            self._current_ssl_options_hash()\n\n    def test_manually_modified_past_file_warns(self):\n        with open(self.config.mod_ssl_conf, \"a\") as mod_ssl_conf:\n            mod_ssl_conf.write(\"a new line for the wrong hash\\n\")\n        with open(self.config.updated_mod_ssl_conf_digest, \"w\") as f:\n            f.write(\"hashofanoldversion\")\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert mock_logger.warning.call_args[0][0] == \\\n                \"%s has been manually modified; updated file \" \\\n                \"saved to %s. We recommend updating %s for security purposes.\"\n        assert crypto_util.sha256sum(self.config.mod_ssl_conf_src) == \\\n            self._current_ssl_options_hash()\n        # only print warning once\n        with mock.patch(\"certbot.plugins.common.logger\") as mock_logger:\n            self._call()\n            assert not mock_logger.warning.called\n\n    def test_current_file_hash_in_all_hashes(self):\n        from certbot_nginx._internal.constants import ALL_SSL_OPTIONS_HASHES\n        assert self._current_ssl_options_hash() in ALL_SSL_OPTIONS_HASHES, \\\n            \"Constants.ALL_SSL_OPTIONS_HASHES must be appended\" \\\n            \" with the sha256 hash of self.config.mod_ssl_conf when it is updated.\"\n\n    def test_ssl_config_files_hash_in_all_hashes(self):\n        \"\"\"\n        It is really critical that all TLS Nginx config files have their SHA256 hash registered in\n        constants.ALL_SSL_OPTIONS_HASHES. Otherwise Certbot will mistakenly assume that the config\n        file has been manually edited by the user, and will refuse to update it.\n        This test ensures that all necessary hashes are present.\n        \"\"\"\n        import importlib.resources\n\n        from certbot_nginx._internal.constants import ALL_SSL_OPTIONS_HASHES\n\n        tls_configs_ref = importlib.resources.files(\"certbot_nginx\").joinpath(\n            \"_internal\", \"tls_configs\")\n        with importlib.resources.as_file(tls_configs_ref) as tls_configs_dir:\n            for tls_config_file in os.listdir(tls_configs_dir):\n                file_hash = crypto_util.sha256sum(os.path.join(tls_configs_dir, tls_config_file))\n                assert file_hash in ALL_SSL_OPTIONS_HASHES, \\\n                    f\"Constants.ALL_SSL_OPTIONS_HASHES must be appended with the sha256 \" \\\n                    f\"hash of {tls_config_file} when it is updated.\"\n\n    def test_nginx_version_uses_correct_config(self):\n        self.config.version = (1, 5, 8)\n        self.config.openssl_version = \"1.0.2g\" # shouldn't matter\n        assert os.path.basename(self.config.mod_ssl_conf_src) == \\\n                         \"options-ssl-nginx-old.conf\"\n        self._call()\n        self._assert_current_file()\n        self.config.version = (1, 5, 9)\n        self.config.openssl_version = \"1.0.2l\"\n        assert os.path.basename(self.config.mod_ssl_conf_src) == \\\n                         \"options-ssl-nginx-tls12-only.conf\"\n        self._call()\n        self._assert_current_file()\n        self.config.version = (1, 13, 0)\n        assert os.path.basename(self.config.mod_ssl_conf_src) == \\\n                         \"options-ssl-nginx.conf\"\n        self._call()\n        self._assert_current_file()\n        self.config.version = (1, 13, 0)\n        self.config.openssl_version = \"1.0.2k\"\n        assert os.path.basename(self.config.mod_ssl_conf_src) == \\\n                         \"options-ssl-nginx-tls13-session-tix-on.conf\"\n\n\nclass DetermineDefaultServerRootTest(certbot_test_util.ConfigTestCase):\n    \"\"\"Tests for certbot_nginx._internal.configurator._determine_default_server_root.\"\"\"\n\n    def _call(self):\n        from certbot_nginx._internal.configurator import _determine_default_server_root\n        return _determine_default_server_root()\n\n    @mock.patch.dict(os.environ, {\"CERTBOT_DOCS\": \"1\"})\n    def test_docs_value(self):\n        self._test(expect_both_values=True)\n\n    @mock.patch.dict(os.environ, {})\n    def test_real_values(self):\n        self._test(expect_both_values=False)\n\n    def _test(self, expect_both_values):\n        server_root = self._call()\n\n        if expect_both_values:\n            assert \"/usr/local/etc/nginx\" in server_root\n            assert \"/etc/nginx\" in server_root\n        else:\n            assert server_root in (\"/etc/nginx\", \"/usr/local/etc/nginx\")\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/display_ops_test.py",
    "content": "\"\"\"Test certbot_nginx._internal.display_ops.\"\"\"\nimport sys\n\nimport pytest\n\nfrom certbot.display import util as display_util\nfrom certbot.tests import util as certbot_util\nfrom certbot_nginx._internal import parser\nfrom certbot_nginx._internal.display_ops import select_vhost_multiple\nfrom certbot_nginx._internal.tests import test_util as util\n\n\nclass SelectVhostMultiTest(util.NginxTest):\n    \"\"\"Tests for certbot_nginx._internal.display_ops.select_vhost_multiple.\"\"\"\n\n    def setUp(self):\n        super().setUp()\n        nparser = parser.NginxParser(self.config_path)\n        self.vhosts = nparser.get_vhosts()\n\n    def test_select_no_input(self):\n        assert not select_vhost_multiple([])\n\n    @certbot_util.patch_display_util()\n    def test_select_correct(self, mock_util):\n        mock_util().checklist.return_value = (\n            display_util.OK, [self.vhosts[3].display_repr(),\n                              self.vhosts[2].display_repr()])\n        vhs = select_vhost_multiple([self.vhosts[3],\n                                     self.vhosts[2],\n                                     self.vhosts[1]])\n        assert self.vhosts[2] in vhs\n        assert self.vhosts[3] in vhs\n        assert self.vhosts[1] not in vhs\n\n    @certbot_util.patch_display_util()\n    def test_select_cancel(self, mock_util):\n        mock_util().checklist.return_value = (display_util.CANCEL, \"whatever\")\n        vhs = select_vhost_multiple([self.vhosts[2], self.vhosts[3]])\n        assert len(vhs) == 0\n        assert vhs == []\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/http_01_test.py",
    "content": "\"\"\"Tests for certbot_nginx._internal.http_01\"\"\"\nimport sys\nfrom unittest import mock\n\nimport josepy as jose\nimport pytest\n\nfrom acme import challenges, messages\nfrom certbot import achallenges\nfrom certbot.tests import acme_util\nfrom certbot.tests import util as test_util\nfrom certbot_nginx._internal.obj import Addr\nfrom certbot_nginx._internal.tests import test_util as util\n\nAUTH_KEY = jose.JWKRSA.load(test_util.load_vector(\"rsa512_key.pem\"))\n\n\nclass HttpPerformTest(util.NginxTest):\n    \"\"\"Test the NginxHttp01 challenge.\"\"\"\n\n    account_key = AUTH_KEY\n    achalls = [\n        achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=b\"kNdwjwOeX0I_A8DXt9Msmg\"), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"www.example.com\"),\n            account_key=account_key),\n        achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(\n                    token=b\"\\xba\\xa9\\xda?<m\\xaewmx\\xea\\xad\\xadv\\xf4\\x02\\xc9y\"\n                          b\"\\x80\\xe2_X\\t\\xe7\\xc7\\xa4\\t\\xca\\xf7&\\x945\"\n                ), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"ipv6.com\"),\n            account_key=account_key),\n        achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(\n                    token=b\"\\x8c\\x8a\\xbf_-f\\\\cw\\xee\\xd6\\xf8/\\xa5\\xe3\\xfd\"\n                          b\"\\xeb9\\xf1\\xf5\\xb9\\xefVM\\xc9w\\xa4u\\x9c\\xe1\\x87\\xb4\"\n                ), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"www.example.org\"),\n            account_key=account_key),\n        achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=b\"kNdwjxOeX0I_A8DXt9Msmg\"), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"migration.com\"),\n            account_key=account_key),\n        achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=b\"kNdwjxOeX0I_A8DXt9Msmg\"), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"ipv6ssl.com\"),\n            account_key=account_key),\n    ]\n\n    def setUp(self):\n        super().setUp()\n\n        config = self.get_nginx_configurator(\n            self.config_path, self.config_dir, self.work_dir, self.logs_dir)\n\n        from certbot_nginx._internal import http_01\n        self.http01 = http_01.NginxHttp01(config)\n\n    def test_perform0(self):\n        responses = self.http01.perform()\n        assert [] == responses\n\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.save\")\n    def test_perform1(self, mock_save):\n        self.http01.add_chall(self.achalls[0])\n        response = self.achalls[0].response(self.account_key)\n\n        responses = self.http01.perform()\n\n        assert [response] == responses\n        assert mock_save.call_count == 1\n\n    def test_perform2(self):\n        acme_responses = []\n        for achall in self.achalls:\n            self.http01.add_chall(achall)\n            acme_responses.append(achall.response(self.account_key))\n\n        http_responses = self.http01.perform()\n\n        assert len(http_responses) == 5\n        for i in range(5):\n            assert http_responses[i] == acme_responses[i]\n\n    def test_mod_config(self):\n        self.http01.add_chall(self.achalls[0])\n        self.http01.add_chall(self.achalls[2])\n\n        self.http01._mod_config()  # pylint: disable=protected-access\n\n        self.http01.configurator.save()\n\n        self.http01.configurator.parser.load()\n\n        # vhosts = self.http01.configurator.parser.get_vhosts()\n\n        # for vhost in vhosts:\n        #     pass\n            # if the name matches\n            # check that the location block is in there and is correct\n\n            # if vhost.addrs == set(v_addr1):\n            #     response = self.achalls[0].response(self.account_key)\n            # else:\n            #     response = self.achalls[2].response(self.account_key)\n            #     self.assertEqual(vhost.addrs, set(v_addr2_print))\n            # self.assertEqual(vhost.names, set([response.z_domain.decode('ascii')]))\n\n    @mock.patch('certbot_nginx._internal.parser.NginxParser.add_server_directives')\n    def test_mod_config_http_and_https(self, mock_add_server_directives):\n        \"\"\"A server_name with both HTTP and HTTPS vhosts should get modded in both vhosts\"\"\"\n        self.configuration.https_port = 443\n        self.http01.add_chall(self.achalls[3]) # migration.com\n        self.http01._mod_config()  # pylint: disable=protected-access\n\n        # Domain has an HTTP and HTTPS vhost\n        # 2 * 'rewrite' + 2 * 'return 200 keyauthz' = 4\n        assert mock_add_server_directives.call_count == 4\n\n    @mock.patch('certbot_nginx._internal.parser.nginxparser.dump')\n    @mock.patch('certbot_nginx._internal.parser.NginxParser.add_server_directives')\n    def test_mod_config_only_https(self, mock_add_server_directives, mock_dump):\n        \"\"\"A server_name with only an HTTPS vhost should get modded\"\"\"\n        self.http01.add_chall(self.achalls[4]) # ipv6ssl.com\n        self.http01._mod_config() # pylint: disable=protected-access\n\n        # It should modify the existing HTTPS vhost\n        assert mock_add_server_directives.call_count == 2\n        # since there was no suitable HTTP vhost or default HTTP vhost, a non-empty one\n        # should have been created and written to the challenge conf file\n        assert mock_dump.call_args[0][0] != []\n\n    @mock.patch('certbot_nginx._internal.parser.NginxParser.add_server_directives')\n    def test_mod_config_deduplicate(self, mock_add_server_directives):\n        \"\"\"A vhost that appears in both HTTP and HTTPS vhosts only gets modded once\"\"\"\n        achall = achallenges.KeyAuthorizationAnnotatedChallenge(\n            challb=acme_util.chall_to_challb(\n                challenges.HTTP01(token=b\"kNdwjxOeX0I_A8DXt9Msmg\"), messages.STATUS_PENDING),\n            identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=\"ssl.both.com\"),\n            account_key=AUTH_KEY)\n        self.http01.add_chall(achall)\n        self.http01._mod_config() # pylint: disable=protected-access\n\n        # Should only get called 5 times, rather than 6, because two vhosts are the same\n        assert mock_add_server_directives.call_count == 5*2\n\n    def test_mod_config_insert_bucket_directive(self):\n        nginx_conf = self.http01.configurator.parser.abs_path('nginx.conf')\n\n        expected = ['server_names_hash_bucket_size', '128']\n        original_conf = self.http01.configurator.parser.parsed[nginx_conf]\n        assert not util.contains_at_depth(original_conf, expected, 2)\n\n        self.http01.add_chall(self.achalls[0])\n        self.http01._mod_config()  # pylint: disable=protected-access\n        self.http01.configurator.save()\n        self.http01.configurator.parser.load()\n\n        generated_conf = self.http01.configurator.parser.parsed[nginx_conf]\n        assert util.contains_at_depth(generated_conf, expected, 2)\n\n    def test_mod_config_update_bucket_directive_in_included_file(self):\n        # save old example.com config\n        example_com_loc = self.http01.configurator.parser.abs_path('sites-enabled/example.com')\n        with open(example_com_loc) as f:\n            original_example_com = f.read()\n\n        # modify example.com config\n        modified_example_com = 'server_names_hash_bucket_size 64;\\n' + original_example_com\n        with open(example_com_loc, 'w') as f:\n            f.write(modified_example_com)\n        self.http01.configurator.parser.load()\n\n        # run change\n        self.http01.add_chall(self.achalls[0])\n        self.http01._mod_config()  # pylint: disable=protected-access\n        self.http01.configurator.save()\n        self.http01.configurator.parser.load()\n\n        # not in nginx.conf\n        expected = ['server_names_hash_bucket_size', '128']\n        nginx_conf_loc = self.http01.configurator.parser.abs_path('nginx.conf')\n        nginx_conf = self.http01.configurator.parser.parsed[nginx_conf_loc]\n        assert not util.contains_at_depth(nginx_conf, expected, 2)\n\n        # is updated in example.com conf\n        generated_conf = self.http01.configurator.parser.parsed[example_com_loc]\n        assert util.contains_at_depth(generated_conf, expected, 0)\n\n        # put back example.com config\n        with open(example_com_loc, 'w') as f:\n            f.write(original_example_com)\n        self.http01.configurator.parser.load()\n\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.ipv6_info\")\n    def test_default_listen_addresses_no_memoization(self, ipv6_info):\n        # pylint: disable=protected-access\n        ipv6_info.return_value = (True, True)\n        self.http01._default_listen_addresses()\n        assert ipv6_info.call_count == 1\n        ipv6_info.return_value = (False, False)\n        self.http01._default_listen_addresses()\n        assert ipv6_info.call_count == 2\n\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.ipv6_info\")\n    def test_default_listen_addresses_t_t(self, ipv6_info):\n        # pylint: disable=protected-access\n        ipv6_info.return_value = (True, True)\n        addrs = self.http01._default_listen_addresses()\n        http_addr = Addr.fromstring(\"80\")\n        http_ipv6_addr = Addr.fromstring(\"[::]:80\")\n        assert addrs == [http_addr, http_ipv6_addr]\n\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.ipv6_info\")\n    def test_default_listen_addresses_t_f(self, ipv6_info):\n        # pylint: disable=protected-access\n        ipv6_info.return_value = (True, False)\n        addrs = self.http01._default_listen_addresses()\n        http_addr = Addr.fromstring(\"80\")\n        http_ipv6_addr = Addr.fromstring(\"[::]:80 ipv6only=on\")\n        assert addrs == [http_addr, http_ipv6_addr]\n\n    @mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.ipv6_info\")\n    def test_default_listen_addresses_f_f(self, ipv6_info):\n        # pylint: disable=protected-access\n        ipv6_info.return_value = (False, False)\n        addrs = self.http01._default_listen_addresses()\n        http_addr = Addr.fromstring(\"80\")\n        assert addrs == [http_addr]\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/nginxparser_test.py",
    "content": "\"\"\"Test for certbot_nginx._internal.nginxparser.\"\"\"\nimport copy\nimport operator\nimport sys\nimport tempfile\nimport unittest\n\nfrom pyparsing import ParseException\nimport pytest\n\nfrom certbot_nginx._internal.nginxparser import dump\nfrom certbot_nginx._internal.nginxparser import dumps\nfrom certbot_nginx._internal.nginxparser import load\nfrom certbot_nginx._internal.nginxparser import loads\nfrom certbot_nginx._internal.nginxparser import RawNginxParser\nfrom certbot_nginx._internal.nginxparser import UnspacedList\nfrom certbot_nginx._internal.tests import test_util as util\n\nFIRST = operator.itemgetter(0)\n\n\nclass TestRawNginxParser(unittest.TestCase):\n    \"\"\"Test the raw low-level Nginx config parser.\"\"\"\n\n    def test_assignments(self):\n        parsed = RawNginxParser.assignment.parse_string('root /test;').asList()\n        assert parsed == ['root', ' ', '/test']\n        parsed = RawNginxParser.assignment.parse_string('root /test;foo bar;').asList()\n        assert parsed == ['root', ' ', '/test'], ['foo', ' ', 'bar']\n\n    def test_blocks(self):\n        parsed = RawNginxParser.block.parse_string('foo {}').asList()\n        assert parsed == [['foo', ' '], []]\n        parsed = RawNginxParser.block.parse_string('location /foo{}').asList()\n        assert parsed == [['location', ' ', '/foo'], []]\n        parsed = RawNginxParser.block.parse_string('foo { bar foo ; }').asList()\n        assert parsed == [['foo', ' '], [[' ', 'bar', ' ', 'foo', ' '], ' ']]\n\n    def test_nested_blocks(self):\n        parsed = RawNginxParser.block.parse_string('foo { bar {} }').asList()\n        block, content = parsed\n        assert FIRST(content) == [[' ', 'bar', ' '], []]\n        assert FIRST(block) == 'foo'\n\n    def test_dump_as_string(self):\n        dumped = dumps(UnspacedList([\n            ['user', ' ', 'www-data'],\n            [['\\n', 'server', ' '], [\n                ['\\n    ', 'listen', ' ', '80'],\n                ['\\n    ', 'server_name', ' ', 'foo.com'],\n                ['\\n    ', 'root', ' ', '/home/ubuntu/sites/foo/'],\n                [['\\n\\n    ', 'location', ' ', '/status', ' '], [\n                    ['\\n        ', 'check_status', ''],\n                    [['\\n\\n        ', 'types', ' '],\n                    [['\\n            ', 'image/jpeg', ' ', 'jpg']]],\n                ]]\n            ]]]))\n\n        assert dumped.split('\\n') == \\\n                         'user www-data;\\n' \\\n                         'server {\\n' \\\n                         '    listen 80;\\n' \\\n                         '    server_name foo.com;\\n' \\\n                         '    root /home/ubuntu/sites/foo/;\\n' \\\n                         '\\n' \\\n                         '    location /status {\\n' \\\n                         '        check_status;\\n' \\\n                         '\\n' \\\n                         '        types {\\n' \\\n                         '            image/jpeg jpg;}}}'.split('\\n')\n\n    def test_parse_from_file(self):\n        with util.get_data_filename('foo.conf') as path:\n            with open(path) as handle:\n                parsed = util.filter_comments(load(handle))\n        assert parsed == \\\n            [['user', 'www-data'],\n            [['http'],\n            [[['server'], [\n                ['listen', '*:80', 'default_server', 'ssl'],\n                ['server_name', '*.www.foo.com', '*.www.example.com'],\n                ['root', '/home/ubuntu/sites/foo/'],\n                [['location', '/status'], [\n                    [['types'], [['image/jpeg', 'jpg']]],\n                ]],\n                [['location', '~', r'case_sensitive\\.php$'], [\n                    ['index', 'index.php'],\n                    ['root', '/var/root'],\n                ]],\n                [['location', '~*', r'case_insensitive\\.php$'], []],\n                [['location', '=', r'exact_match\\.php$'], []],\n                [['location', '^~', r'ignore_regex\\.php$'], []]\n            ]]]]]\n\n    def test_parse_from_file2(self):\n        with util.get_data_filename('edge_cases.conf') as path:\n            with open(path) as handle:\n                parsed = util.filter_comments(load(handle))\n        assert parsed == \\\n            [[['server'], [['server_name', 'simple']]],\n            [['server'],\n            [['server_name', 'with.if'],\n            [['location', '~', '^/services/.+$'],\n                [[['if', '($request_filename', '~*', '\\\\.(ttf|woff)$)'],\n                [['add_header', 'Access-Control-Allow-Origin', '\"*\"']]]]]]],\n            [['server'],\n            [['server_name', 'with.complicated.headers'],\n            [['location', '~*', '\\\\.(?:gif|jpe?g|png)$'],\n                [['add_header', 'Pragma', 'public'],\n                ['add_header',\n                'Cache-Control', '\\'public, must-revalidate, proxy-revalidate\\'',\n                '\"test,;{}\"', 'foo'],\n                ['blah', '\"hello;world\"'],\n                ['try_files', '$uri', '@rewrites']]]]]]\n\n    def test_parse_from_file3(self):\n        with util.get_data_filename('multiline_quotes.conf') as path:\n            with open(path) as handle:\n                parsed = util.filter_comments(load(handle))\n        assert parsed == \\\n            [[['http'],\n                [[['server'],\n                    [['listen', '*:443'],\n                    [['location', '/'],\n                        [['body_filter_by_lua',\n                          '\\'ngx.ctx.buffered = (ngx.ctx.buffered or \"\")'\n                          ' .. string.sub(ngx.arg[1], 1, 1000)\\n'\n                          '                            '\n                          'if ngx.arg[2] then\\n'\n                          '                              '\n                          'ngx.var.resp_body = ngx.ctx.buffered\\n'\n                          '                            end\\'']]]]]]]]\n\n    def test_abort_on_parse_failure(self):\n        with util.get_data_filename('broken.conf') as path:\n            with open(path) as handle:\n                with pytest.raises(ParseException):\n                    load(handle)\n\n    def test_dump_as_file(self):\n        with util.get_data_filename('nginx.conf') as path:\n            with open(path) as handle:\n                parsed = load(handle)\n        parsed[-1][-1].append(UnspacedList([['server'],\n                               [['listen', ' ', '443', ' ', 'ssl'],\n                                ['server_name', ' ', 'localhost'],\n                                ['ssl_certificate', ' ', 'cert.pem'],\n                                ['ssl_certificate_key', ' ', 'cert.key'],\n                                ['ssl_session_cache', ' ', 'shared:SSL:1m'],\n                                ['ssl_session_timeout', ' ', '5m'],\n                                ['ssl_ciphers', ' ', 'HIGH:!aNULL:!MD5'],\n                                [['location', ' ', '/'],\n                                 [['root', ' ', 'html'],\n                                  ['index', ' ', 'index.html', ' ', 'index.htm']]]]]))\n\n        with tempfile.TemporaryFile(mode='w+t') as f:\n            dump(parsed, f)\n            f.seek(0)\n            parsed_new = load(f)\n        assert parsed == parsed_new\n\n    def test_comments(self):\n        with util.get_data_filename('minimalistic_comments.conf') as path:\n            with open(path) as handle:\n                parsed = load(handle)\n\n        with tempfile.TemporaryFile(mode='w+t') as f:\n            dump(parsed, f)\n            f.seek(0)\n            parsed_new = load(f)\n\n        assert parsed == parsed_new\n        assert parsed_new == [\n            ['#', \" Use bar.conf when it's a full moon!\"],\n            ['include', 'foo.conf'],\n            ['#', ' Kilroy was here'],\n            ['check_status'],\n            [['server'],\n             [['#', ''],\n              ['#', \" Don't forget to open up your firewall!\"],\n              ['#', ''],\n              ['listen', '1234'],\n              ['#', ' listen 80;']]],\n        ]\n\n    def test_issue_518(self):\n        parsed = loads('if ($http_accept ~* \"webp\") { set $webp \"true\"; }')\n\n        assert parsed == [\n            [['if', '($http_accept', '~*', '\"webp\")'],\n             [['set', '$webp', '\"true\"']]]\n        ]\n\n    def test_comment_in_block(self):\n        parsed = loads(\"\"\"http {\n          # server{\n          }\"\"\")\n\n        assert parsed == [\n            [['http'],\n             [['#', ' server{']]]\n        ]\n\n    def test_access_log(self):\n        # see issue #3798\n        parsed = loads('access_log syslog:server=unix:/dev/log,facility=auth,'\n            'tag=nginx_post,severity=info custom;')\n\n        assert parsed == [\n            ['access_log',\n             'syslog:server=unix:/dev/log,facility=auth,tag=nginx_post,severity=info',\n             'custom']\n        ]\n\n    def test_add_header(self):\n        # see issue #3798\n        parsed = loads('add_header Cache-Control no-cache,no-store,must-revalidate,max-age=0;')\n\n        assert parsed == [\n            ['add_header', 'Cache-Control', 'no-cache,no-store,must-revalidate,max-age=0']\n        ]\n\n    def test_map_then_assignment_in_block(self):\n        # see issue #3798\n        test_str = \"\"\"http {\n            map $http_upgrade $connection_upgrade {\n              default upgrade;\n              ''      close;\n              \"~Opera Mini\" 1;\n              *.example.com 1;\n            }\n            one;\n        }\"\"\"\n        parsed = loads(test_str)\n        assert parsed == [\n            [['http'], [\n                [['map', '$http_upgrade', '$connection_upgrade'], [\n                    ['default', 'upgrade'],\n                    [\"''\", 'close'],\n                    ['\"~Opera Mini\"', '1'],\n                    ['*.example.com', '1']\n                ]],\n                ['one']\n            ]]\n        ]\n\n    def test_variable_name(self):\n        parsed = loads('try_files /typo3temp/tx_ncstaticfilecache/'\n            '$host${request_uri}index.html @nocache;')\n\n        assert parsed == [\n            ['try_files',\n             '/typo3temp/tx_ncstaticfilecache/$host${request_uri}index.html',\n             '@nocache']\n        ]\n\n    def test_weird_blocks(self):\n        test = r\"\"\"\n            if ($http_user_agent ~ MSIE) {\n                rewrite ^(.*)$ /msie/$1 break;\n            }\n\n            if ($http_cookie ~* \"id=([^;]+)(?:;|$)\") {\n               set $id $1;\n            }\n\n            if ($request_method = POST) {\n               return 405;\n            }\n\n            if ($request_method) {\n               return 403;\n            }\n\n            if ($args ~ post=140){\n              rewrite ^ http://example.com/;\n            }\n\n            location ~ ^/users/(.+\\.(?:gif|jpe?g|png))$ {\n              alias /data/w3/images/$1;\n            }\n\n            proxy_set_header X-Origin-URI ${scheme}://${http_host}/$request_uri;\n        \"\"\"\n        parsed = loads(test)\n        assert parsed == [[['if', '($http_user_agent', '~', 'MSIE)'],\n            [['rewrite', '^(.*)$', '/msie/$1', 'break']]],\n            [['if', '($http_cookie', '~*', '\"id=([^;]+)(?:;|$)\")'], [['set', '$id', '$1']]],\n            [['if', '($request_method', '=', 'POST)'], [['return', '405']]],\n            [['if', '($request_method)'],\n            [['return', '403']]], [['if', '($args', '~', 'post=140)'],\n            [['rewrite', '^', 'http://example.com/']]],\n            [['location', '~', '^/users/(.+\\\\.(?:gif|jpe?g|png))$'],\n            [['alias', '/data/w3/images/$1']]],\n            ['proxy_set_header', 'X-Origin-URI', '${scheme}://${http_host}/$request_uri']]\n\n    def test_edge_cases(self):\n        # quotes\n        parsed = loads(r'\"hello\\\"\"; # blah \"heh heh\"')\n        assert parsed == [['\"hello\\\\\"\"'], ['#', ' blah \"heh heh\"']]\n\n        # if with comment\n        parsed = loads(\"\"\"if ($http_cookie ~* \"id=([^;]+)(?:;|$)\") { # blah )\n            }\"\"\")\n        assert parsed == [[['if', '($http_cookie', '~*', '\"id=([^;]+)(?:;|$)\")'],\n            [['#', ' blah )']]]]\n\n        # end paren\n        test = \"\"\"\n            one\"test\";\n            (\"two\");\n            \"test\")red;\n            \"test\")\"blue\";\n            \"test\")\"three;\n            (one\"test\")one;\n            one\";\n            one\"test;\n            one\"test\"one;\n        \"\"\"\n        parsed = loads(test)\n        assert parsed == [\n            ['one\"test\"'],\n            ['(\"two\")'],\n            ['\"test\")red'],\n            ['\"test\")\"blue\"'],\n            ['\"test\")\"three'],\n            ['(one\"test\")one'],\n            ['one\"'],\n            ['one\"test'],\n            ['one\"test\"one']\n        ]\n        with pytest.raises(ParseException):\n            loads(r'\"test\"one;') # fails\n        with pytest.raises(ParseException):\n            loads(r'\"test;') # fails\n\n        # newlines\n        test = \"\"\"\n            server_name foo.example.com bar.example.com \\\n                        baz.example.com qux.example.com;\n            server_name foo.example.com bar.example.com\n                        baz.example.com qux.example.com;\n        \"\"\"\n        parsed = loads(test)\n        assert parsed == [\n            ['server_name', 'foo.example.com', 'bar.example.com',\n                'baz.example.com', 'qux.example.com'],\n            ['server_name', 'foo.example.com', 'bar.example.com',\n                'baz.example.com', 'qux.example.com']\n        ]\n\n        # variable weirdness\n        parsed = loads(\"directive $var ${var} $ ${};\")\n        assert parsed == [['directive', '$var', '${var}', '$', '${}']]\n        with pytest.raises(ParseException):\n            loads(\"server {server_name test.com};\")\n        assert loads(\"blag${dfgdfg};\") == [['blag${dfgdfg}']]\n        with pytest.raises(ParseException):\n            loads(\"blag${dfgdf{g};\")\n\n        # empty file\n        parsed = loads(\"\")\n        assert parsed == []\n\n    def test_non_breaking_spaces(self):\n        # non-breaking spaces\n        test = u'\\u00a0'\n        loads(test)\n        test = \"\"\"\n        map $http_upgrade $connection_upgrade {\n            default upgrade;\n            ''      close;\n        }\n        \"\"\"\n        loads(test)\n\n    def test_location_comment_issue(self):\n        # See discussion at https://github.com/certbot/certbot/issues/10264\n        already_good = '''\n        location = /resume\n        # x\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        '''\n        loads(already_good)\n        already_good = '''\n        location = /resume\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        # {\n        '''\n        loads(already_good)\n        needs_fixing = '''\n        location = /resume\n        # {\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        '''\n        with pytest.raises(ParseException):\n            loads(needs_fixing) # fails\n        needs_fixing = '''\n        location = /resume\n        # x{\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        '''\n        with pytest.raises(ParseException):\n            loads(needs_fixing) # fails\n        needs_fixing = '''\n        location = /resume\n        #{\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        '''\n        with pytest.raises(ParseException):\n            loads(needs_fixing) # fails\n        needs_fixing = '''\n        location = /resume\n        # {x\n        { rewrite .* /Files/Adam_Lein_resume.pdf redirect; }\n        '''\n        with pytest.raises(ParseException):\n            loads(needs_fixing) # fails\n\n\nclass TestUnspacedList(unittest.TestCase):\n    \"\"\"Test the UnspacedList data structure\"\"\"\n    def setUp(self):\n        self.a = [\"\\n    \", \"things\", \" \", \"quirk\"]\n        self.b = [\"y\", \" \"]\n        self.l = self.a[:]\n        self.l2 = self.b[:]\n        self.ul = UnspacedList(self.l)\n        self.ul2 = UnspacedList(self.l2)\n\n    def test_construction(self):\n        assert self.ul == [\"things\", \"quirk\"]\n        assert self.ul2 == [\"y\"]\n\n    def test_append(self):\n        ul3 = copy.deepcopy(self.ul)\n        ul3.append(\"wise\")\n        assert ul3 == [\"things\", \"quirk\", \"wise\"]\n        assert ul3.spaced == self.a + [\"wise\"]\n\n    def test_add(self):\n        ul3 = self.ul + self.ul2\n        assert ul3 == [\"things\", \"quirk\", \"y\"]\n        assert ul3.spaced == self.a + self.b\n        assert self.ul.spaced == self.a\n        ul3 = self.ul + self.l2\n        assert ul3 == [\"things\", \"quirk\", \"y\"]\n        assert ul3.spaced == self.a + self.b\n\n    def test_extend(self):\n        ul3 = copy.deepcopy(self.ul)\n        ul3.extend(self.ul2)\n        assert ul3 == [\"things\", \"quirk\", \"y\"]\n        assert ul3.spaced == self.a + self.b\n        assert self.ul.spaced == self.a\n\n    def test_set(self):\n        ul3 = copy.deepcopy(self.ul)\n        ul3[0] = \"zither\"\n        l = [\"\\n \", \"zather\", \"zest\"]\n        ul3[1] = UnspacedList(l)\n        assert ul3 == [\"zither\", [\"zather\", \"zest\"]]\n        assert ul3.spaced == [self.a[0], \"zither\", \" \", l]\n\n    def test_get(self):\n        with pytest.raises(IndexError):\n            self.ul2.__getitem__(2)\n        with pytest.raises(IndexError):\n            self.ul2.__getitem__(-3)\n\n    def test_insert(self):\n        x = UnspacedList(\n                [['\\n    ', 'listen', '       ', '69.50.225.155:9000'],\n                ['\\n    ', 'listen', '       ', '127.0.0.1'],\n                ['\\n    ', 'server_name', ' ', '.example.com'],\n                ['\\n    ', 'server_name', ' ', 'example.*'], '\\n',\n                ['listen', ' ', '5001', ' ', 'ssl']])\n        x.insert(5, \"FROGZ\")\n        assert x == \\\n            [['listen', '69.50.225.155:9000'], ['listen', '127.0.0.1'],\n            ['server_name', '.example.com'], ['server_name', 'example.*'],\n            ['listen', '5001', 'ssl'], 'FROGZ']\n        assert x.spaced == \\\n            [['\\n    ', 'listen', '       ', '69.50.225.155:9000'],\n            ['\\n    ', 'listen', '       ', '127.0.0.1'],\n            ['\\n    ', 'server_name', ' ', '.example.com'],\n            ['\\n    ', 'server_name', ' ', 'example.*'], '\\n',\n            ['listen', ' ', '5001', ' ', 'ssl'],\n            'FROGZ']\n\n    def test_rawlists(self):\n        ul3 = copy.deepcopy(self.ul)\n        ul3.insert(0, \"some\")\n        ul3.append(\"why\")\n        ul3.extend([\"did\", \"whether\"])\n        del ul3[2]\n        assert ul3 == [\"some\", \"things\", \"why\", \"did\", \"whether\"]\n\n    def test_is_dirty(self):\n        assert self.ul2.is_dirty() is False\n        ul3 = UnspacedList([])\n        ul3.append(self.ul)\n        assert self.ul.is_dirty() is False\n        assert ul3.is_dirty() is True\n        ul4 = UnspacedList([[1], [2, 3, 4]])\n        assert ul4.is_dirty() is False\n        ul4[1][2] = 5\n        assert ul4.is_dirty() is True\n\n\nif __name__ == '__main__':\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/obj_test.py",
    "content": "\"\"\"Test the helper objects in certbot_nginx._internal.obj.\"\"\"\nimport itertools\nimport sys\nimport unittest\n\nimport pytest\n\n\nclass AddrTest(unittest.TestCase):\n    \"\"\"Test the Addr class.\"\"\"\n    def setUp(self):\n        from certbot_nginx._internal.obj import Addr\n        self.addr1 = Addr.fromstring(\"192.168.1.1\")\n        self.addr2 = Addr.fromstring(\"192.168.1.1:* ssl\")\n        self.addr3 = Addr.fromstring(\"192.168.1.1:80\")\n        self.addr4 = Addr.fromstring(\"*:80 default_server ssl\")\n        self.addr5 = Addr.fromstring(\"myhost\")\n        self.addr6 = Addr.fromstring(\"80 default_server spdy\")\n        self.addr7 = Addr.fromstring(\"*:80 default ssl\")\n\n    def test_fromstring(self):\n        assert self.addr1.get_addr() == \"192.168.1.1\"\n        assert self.addr1.get_port() == \"\"\n        assert self.addr1.ssl is False\n        assert self.addr1.default is False\n\n        assert self.addr2.get_addr() == \"192.168.1.1\"\n        assert self.addr2.get_port() == \"*\"\n        assert self.addr2.ssl is True\n        assert self.addr2.default is False\n\n        assert self.addr3.get_addr() == \"192.168.1.1\"\n        assert self.addr3.get_port() == \"80\"\n        assert self.addr3.ssl is False\n        assert self.addr3.default is False\n\n        assert self.addr4.get_addr() == \"*\"\n        assert self.addr4.get_port() == \"80\"\n        assert self.addr4.ssl is True\n        assert self.addr4.default is True\n\n        assert self.addr5.get_addr() == \"myhost\"\n        assert self.addr5.get_port() == \"\"\n        assert self.addr5.ssl is False\n        assert self.addr5.default is False\n\n        assert self.addr6.get_addr() == \"\"\n        assert self.addr6.get_port() == \"80\"\n        assert self.addr6.ssl is False\n        assert self.addr6.default is True\n\n        assert self.addr7.default is True\n\n    def test_fromstring_socket(self):\n        from certbot_nginx._internal.obj import Addr, SocketAddrError\n        socket_string = r\"unix:/var/run/nginx.sock\"\n        with pytest.raises(SocketAddrError, match=socket_string):\n            Addr.fromstring(socket_string)\n\n    def test_str(self):\n        assert str(self.addr1) == \"192.168.1.1\"\n        assert str(self.addr2) == \"192.168.1.1:* ssl\"\n        assert str(self.addr3) == \"192.168.1.1:80\"\n        assert str(self.addr4) == \"*:80 default_server ssl\"\n        assert str(self.addr5) == \"myhost\"\n        assert str(self.addr6) == \"80 default_server\"\n        assert str(self.addr7) == \"*:80 default_server ssl\"\n\n    def test_to_string(self):\n        assert self.addr1.to_string() == \"192.168.1.1\"\n        assert self.addr2.to_string() == \"192.168.1.1:* ssl\"\n        assert self.addr3.to_string() == \"192.168.1.1:80\"\n        assert self.addr4.to_string() == \"*:80 default_server ssl\"\n        assert self.addr4.to_string(include_default=False) == \"*:80 ssl\"\n        assert self.addr5.to_string() == \"myhost\"\n        assert self.addr6.to_string() == \"80 default_server\"\n        assert self.addr6.to_string(include_default=False) == \"80\"\n\n    def test_eq(self):\n        from certbot_nginx._internal.obj import Addr\n        new_addr1 = Addr.fromstring(\"192.168.1.1 spdy\")\n        assert self.addr1 == new_addr1\n        assert self.addr1 != self.addr2\n        assert self.addr1 != 3333\n\n    def test_equivalent_any_addresses(self):\n        from certbot_nginx._internal.obj import Addr\n        any_addresses = (\"0.0.0.0:80 default_server ssl\",\n                         \"80 default_server ssl\",\n                         \"*:80 default_server ssl\",\n                         \"80 default ssl\")\n        for first, second in itertools.combinations(any_addresses, 2):\n            assert Addr.fromstring(first) == Addr.fromstring(second)\n\n        # Also, make sure ports are checked.\n        assert Addr.fromstring(any_addresses[0]) != \\\n                            Addr.fromstring(\"0.0.0.0:443 default_server ssl\")\n\n        # And they aren't equivalent to a specified address.\n        for any_address in any_addresses:\n            assert Addr.fromstring(\"192.168.1.2:80 default_server ssl\") != \\\n                Addr.fromstring(any_address)\n\n    def test_set_inclusion(self):\n        from certbot_nginx._internal.obj import Addr\n        set_a = {self.addr1, self.addr2}\n        addr1b = Addr.fromstring(\"192.168.1.1\")\n        addr2b = Addr.fromstring(\"192.168.1.1:* ssl\")\n        set_b = {addr1b, addr2b}\n\n        assert set_a == set_b\n\n\nclass VirtualHostTest(unittest.TestCase):\n    \"\"\"Test the VirtualHost class.\"\"\"\n    def setUp(self):\n        from certbot_nginx._internal.obj import Addr\n        from certbot_nginx._internal.obj import VirtualHost\n        raw1 = [\n            ['listen', '69.50.225.155:9000'],\n            [['if', '($scheme', '!=', '\"https\") '],\n                [['return', '301', 'https://$host$request_uri']]\n            ],\n            ['#', ' managed by Certbot']\n        ]\n        self.vhost1 = VirtualHost(\n            \"filep\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, raw1, [])\n        raw2 = [\n            ['listen', '69.50.225.155:9000'],\n            [['if', '($scheme', '!=', '\"https\") '],\n                [['return', '301', 'https://$host$request_uri']]\n            ]\n        ]\n        self.vhost2 = VirtualHost(\n            \"filep\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, raw2, [])\n        raw3 = [\n            ['listen', '69.50.225.155:9000'],\n            ['rewrite', '^(.*)$', '$scheme://www.domain.com$1', 'permanent']\n        ]\n        self.vhost3 = VirtualHost(\n            \"filep\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, raw3, [])\n        raw4 = [\n            ['listen', '69.50.225.155:9000'],\n            ['server_name', 'return.com']\n        ]\n        self.vhost4 = VirtualHost(\n            \"filp\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, raw4, [])\n        raw_has_hsts = [\n            ['listen', '69.50.225.155:9000'],\n            ['server_name', 'return.com'],\n            ['add_header', 'always', 'set', 'Strict-Transport-Security', '\\\"max-age=31536000\\\"'],\n        ]\n        self.vhost_has_hsts = VirtualHost(\n            \"filep\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, raw_has_hsts, [])\n\n    def test_eq(self):\n        from certbot_nginx._internal.obj import Addr\n        from certbot_nginx._internal.obj import VirtualHost\n        vhost1b = VirtualHost(\n            \"filep\",\n            {Addr.fromstring(\"localhost blah\")}, False, False,\n            {'localhost'}, [], [])\n\n        assert vhost1b == self.vhost1\n        assert str(vhost1b) == str(self.vhost1)\n        assert vhost1b != 1234\n\n    def test_str(self):\n        stringified = '\\n'.join(['file: filep', 'addrs: localhost',\n                                 \"names: ['localhost']\", 'ssl: False',\n                                 'enabled: False'])\n        assert stringified == str(self.vhost1)\n\n    def test_has_header(self):\n        assert self.vhost_has_hsts.has_header('Strict-Transport-Security') is True\n        assert self.vhost_has_hsts.has_header('Bogus-Header') is False\n        assert self.vhost1.has_header('Strict-Transport-Security') is False\n        assert self.vhost1.has_header('Bogus-Header') is False\n\n    def test_contains_list(self):\n        from certbot_nginx._internal.configurator import _test_block_from_block\n        from certbot_nginx._internal.obj import Addr\n        from certbot_nginx._internal.obj import VirtualHost\n        test_block = [\n            ['\\n    ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],\n            ['\\n']\n        ]\n        test_needle = _test_block_from_block(test_block)\n        test_haystack = [['listen', '80'], ['root', '/var/www/html'],\n            ['index', 'index.html index.htm index.nginx-debian.html'],\n            ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],\n            ['#', ' managed by Certbot'],\n            ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],\n            ['#', ' managed by Certbot'],\n            ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],\n            ['#', ' managed by Certbot'],\n            ['return', '301', 'https://$host$request_uri'],\n            ['#', ' managed by Certbot'], []]\n        vhost_haystack = VirtualHost(\n            \"filp\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, test_haystack, [])\n        test_bad_haystack = [['listen', '80'], ['root', '/var/www/html'],\n            ['index', 'index.html index.htm index.nginx-debian.html'],\n            ['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],\n            ['#', ' managed by Certbot'],\n            ['ssl_certificate', '/etc/letsencrypt/live/two.functorkitten.xyz/fullchain.pem'],\n            ['#', ' managed by Certbot'],\n            ['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],\n            ['#', ' managed by Certbot'],\n            [['if', '($scheme', '!=', '\"https\")'],\n             [['return', '302', 'https://$host$request_uri']]\n            ],\n            ['#', ' managed by Certbot'], []]\n        vhost_bad_haystack = VirtualHost(\n            \"filp\",\n            {Addr.fromstring(\"localhost\")}, False, False,\n            {'localhost'}, test_bad_haystack, [])\n        assert vhost_haystack.contains_list(test_needle)\n        assert not vhost_bad_haystack.contains_list(test_needle)\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/parser_obj_test.py",
    "content": "# type: ignore\n# As done in parser_obj.py, this module is not used for now, so we just skip\n# type checking for the sake of simplicity.\n\"\"\" Tests for functions and classes in parser_obj.py \"\"\"\n\nimport sys\nimport unittest\nfrom unittest import mock\n\nimport pytest\n\nfrom certbot_nginx._internal.parser_obj import COMMENT_BLOCK\nfrom certbot_nginx._internal.parser_obj import parse_raw\n\n\nclass CommentHelpersTest(unittest.TestCase):\n    def test_is_comment(self):\n        from certbot_nginx._internal.parser_obj import _is_comment\n        assert _is_comment(parse_raw(['#']))\n        assert _is_comment(parse_raw(['#', ' literally anything else']))\n        assert not _is_comment(parse_raw(['not', 'even', 'a', 'comment']))\n\n    def test_is_certbot_comment(self):\n        from certbot_nginx._internal.parser_obj import _is_certbot_comment\n        assert _is_certbot_comment(\n            parse_raw(COMMENT_BLOCK))\n        assert not _is_certbot_comment(\n            parse_raw(['#', ' not a certbot comment']))\n        assert not _is_certbot_comment(\n            parse_raw(['#', ' managed by Certbot', ' also not a certbot comment']))\n        assert not _is_certbot_comment(\n            parse_raw(['not', 'even', 'a', 'comment']))\n\n    def test_certbot_comment(self):\n        from certbot_nginx._internal.parser_obj import _certbot_comment\n        from certbot_nginx._internal.parser_obj import _is_certbot_comment\n        comment = _certbot_comment(None)\n        assert _is_certbot_comment(comment)\n        assert comment.dump() == COMMENT_BLOCK\n        assert comment.dump(True) == ['    '] + COMMENT_BLOCK\n        assert _certbot_comment(None, 2).dump(True) == ['  '] + COMMENT_BLOCK\n\n\nclass ParsingHooksTest(unittest.TestCase):\n    def test_is_sentence(self):\n        from certbot_nginx._internal.parser_obj import Sentence\n        assert not Sentence.should_parse([])\n        assert Sentence.should_parse([''])\n        assert Sentence.should_parse(['word'])\n        assert Sentence.should_parse(['two', 'words'])\n        assert not Sentence.should_parse([[]])\n        assert not Sentence.should_parse(['word', []])\n\n    def test_is_block(self):\n        from certbot_nginx._internal.parser_obj import Block\n        assert not Block.should_parse([])\n        assert not Block.should_parse([''])\n        assert not Block.should_parse(['two', 'words'])\n        assert not Block.should_parse([[[]], []])\n        assert not Block.should_parse([['block_name'], ['hi', []], []])\n        assert not Block.should_parse([['block_name'], 'lol'])\n        assert Block.should_parse([['block_name'], ['hi', []]])\n        assert Block.should_parse([['hello'], []])\n        assert Block.should_parse([['block_name'], [['many'], ['statements'], 'here']])\n        assert Block.should_parse([['if', ' ', '(whatever)'], ['hi']])\n\n    @mock.patch(\"certbot_nginx._internal.parser_obj.Parsable.parsing_hooks\")\n    def test_parse_raw(self, parsing_hooks):\n        fake_parser1 = mock.Mock()\n        fake_parser1.should_parse = lambda x: True\n        fake_parser2 = mock.Mock()\n        fake_parser2.should_parse = lambda x: True\n        parsing_hooks.return_value = (fake_parser1, fake_parser2,)\n        # First encountered \"match\" should parse.\n        parse_raw([])\n        fake_parser1().parse.assert_called_once()\n        fake_parser2().parse.assert_not_called()\n        fake_parser1.reset_mock()\n        # \"match\" that returns False shouldn't parse.\n        fake_parser1.should_parse = lambda x: False\n        parse_raw([])\n        fake_parser1().parse.assert_not_called()\n        fake_parser2().parse.assert_called_once()\n\n    @mock.patch(\"certbot_nginx._internal.parser_obj.Parsable.parsing_hooks\")\n    def test_parse_raw_no_match(self, parsing_hooks):\n        from certbot import errors\n        fake_parser1 = mock.Mock()\n        fake_parser1.should_parse = lambda x: False\n        parsing_hooks.return_value = (fake_parser1,)\n        with pytest.raises(errors.MisconfigurationError):\n            parse_raw([])\n        parsing_hooks.return_value = ()\n        with pytest.raises(errors.MisconfigurationError):\n            parse_raw([])\n\n    @mock.patch(\"certbot_nginx._internal.parser_obj.Parsable.parsing_hooks\")\n    def test_parse_raw_passes_add_spaces(self, parsing_hooks):\n        fake_parser1 = mock.Mock()\n        fake_parser1.should_parse = lambda x: True\n        parsing_hooks.return_value = (fake_parser1,)\n        parse_raw([])\n        fake_parser1().parse.assert_called_with([], False)\n        parse_raw([], add_spaces=True)\n        fake_parser1().parse.assert_called_with([], True)\n\n\nclass SentenceTest(unittest.TestCase):\n    def setUp(self):\n        from certbot_nginx._internal.parser_obj import Sentence\n        self.sentence = Sentence(None)\n\n    def test_parse_bad_sentence_raises_error(self):\n        from certbot import errors\n        with pytest.raises(errors.MisconfigurationError):\n            self.sentence.parse('lol')\n        with pytest.raises(errors.MisconfigurationError):\n            self.sentence.parse([[]])\n        with pytest.raises(errors.MisconfigurationError):\n            self.sentence.parse([5])\n\n    def test_parse_sentence_words_hides_spaces(self):\n        og_sentence = ['\\r\\n', 'hello', ' ', ' ', '\\t\\n  ', 'lol', ' ', 'spaces']\n        self.sentence.parse(og_sentence)\n        assert self.sentence.words == ['hello', 'lol', 'spaces']\n        assert self.sentence.dump() == ['hello', 'lol', 'spaces']\n        assert self.sentence.dump(True) == og_sentence\n\n    def test_parse_sentence_with_add_spaces(self):\n        self.sentence.parse(['hi', 'there'], add_spaces=True)\n        assert self.sentence.dump(True) == ['hi', ' ', 'there']\n        self.sentence.parse(['one', ' ', 'space', 'none'], add_spaces=True)\n        assert self.sentence.dump(True) == ['one', ' ', 'space', ' ', 'none']\n\n    def test_iterate(self):\n        expected = [['1', '2', '3']]\n        self.sentence.parse(['1', ' ', '2', ' ', '3'])\n        for i, sentence in enumerate(self.sentence.iterate()):\n            assert sentence.dump() == expected[i]\n\n    def test_set_tabs(self):\n        self.sentence.parse(['tabs', 'pls'], add_spaces=True)\n        self.sentence.set_tabs()\n        assert self.sentence.dump(True)[0] == '\\n    '\n        self.sentence.parse(['tabs', 'pls'], add_spaces=True)\n\n    def test_get_tabs(self):\n        self.sentence.parse(['no', 'tabs'])\n        assert self.sentence.get_tabs() == ''\n        self.sentence.parse(['\\n \\n  ', 'tabs'])\n        assert self.sentence.get_tabs() == '  '\n        self.sentence.parse(['\\n\\t  ', 'tabs'])\n        assert self.sentence.get_tabs() == '\\t  '\n        self.sentence.parse(['\\n\\t \\n', 'tabs'])\n        assert self.sentence.get_tabs() == ''\n\n\nclass BlockTest(unittest.TestCase):\n    def setUp(self):\n        from certbot_nginx._internal.parser_obj import Block\n        self.bloc = Block(None)\n        self.name = ['server', 'name']\n        self.contents = [['thing', '1'], ['thing', '2'], ['another', 'one']]\n        self.bloc.parse([self.name, self.contents])\n\n    def test_iterate(self):\n        # Iterates itself normally\n        assert self.bloc == next(self.bloc.iterate())\n        # Iterates contents while expanded\n        expected = [self.bloc.dump()] + self.contents\n        for i, elem in enumerate(self.bloc.iterate(expanded=True)):\n            assert expected[i] == elem.dump()\n\n    def test_iterate_match(self):\n        # can match on contents while expanded\n        from certbot_nginx._internal.parser_obj import Block\n        from certbot_nginx._internal.parser_obj import Sentence\n        expected = [['thing', '1'], ['thing', '2']]\n        for i, elem in enumerate(self.bloc.iterate(expanded=True,\n            match=lambda x: isinstance(x, Sentence) and 'thing' in x.words)):\n            assert expected[i] == elem.dump()\n        # can match on self\n        assert self.bloc == next(self.bloc.iterate(\n            expanded=True,\n            match=lambda x: isinstance(x, Block) and 'server' in x.names))\n\n    def test_parse_with_added_spaces(self):\n        import copy\n        self.bloc.parse([copy.copy(self.name), self.contents], add_spaces=True)\n        assert self.bloc.dump() == [self.name, self.contents]\n        assert self.bloc.dump(True) == [\n            ['server', ' ', 'name', ' '],\n            [['thing', ' ', '1'],\n             ['thing', ' ', '2'],\n             ['another', ' ', 'one']]]\n\n    def test_bad_parse_raises_error(self):\n        from certbot import errors\n        with pytest.raises(errors.MisconfigurationError):\n            self.bloc.parse([[[]], [[]]])\n        with pytest.raises(errors.MisconfigurationError):\n            self.bloc.parse(['lol'])\n        with pytest.raises(errors.MisconfigurationError):\n            self.bloc.parse(['fake', 'news'])\n\n    def test_set_tabs(self):\n        self.bloc.set_tabs()\n        assert self.bloc.names.dump(True)[0] == '\\n    '\n        for elem in self.bloc.contents.dump(True)[:-1]:\n            assert elem[0] == '\\n        '\n        assert self.bloc.contents.dump(True)[-1][0] == '\\n'\n\n    def test_get_tabs(self):\n        self.bloc.parse([[' \\n  \\t', 'lol'], []])\n        assert self.bloc.get_tabs() == '  \\t'\n\nclass StatementsTest(unittest.TestCase):\n    def setUp(self):\n        from certbot_nginx._internal.parser_obj import Statements\n        self.statements = Statements(None)\n        self.raw = [\n            ['sentence', 'one'],\n            ['sentence', 'two'],\n            ['and', 'another']\n        ]\n        self.raw_spaced = [\n            ['\\n  ', 'sentence', ' ', 'one'],\n            ['\\n  ', 'sentence', ' ', 'two'],\n            ['\\n  ', 'and', ' ', 'another'],\n            '\\n\\n'\n        ]\n\n    def test_set_tabs(self):\n        self.statements.parse(self.raw)\n        self.statements.set_tabs()\n        for statement in self.statements.iterate():\n            assert statement.dump(True)[0] == '\\n    '\n\n    def test_set_tabs_with_parent(self):\n        # Trailing whitespace should inherit from parent tabbing.\n        self.statements.parse(self.raw)\n        self.statements.parent = mock.Mock()\n        self.statements.parent.get_tabs.return_value = '\\t\\t'\n        self.statements.set_tabs()\n        for statement in self.statements.iterate():\n            assert statement.dump(True)[0] == '\\n    '\n        assert self.statements.dump(True)[-1] == '\\n\\t\\t'\n\n    def test_get_tabs(self):\n        self.raw[0].insert(0, '\\n \\n  \\t')\n        self.statements.parse(self.raw)\n        assert self.statements.get_tabs() == '  \\t'\n        self.statements.parse([])\n        assert self.statements.get_tabs() == ''\n\n    def test_parse_with_added_spaces(self):\n        self.statements.parse(self.raw, add_spaces=True)\n        assert self.statements.dump(True)[0] == ['sentence', ' ', 'one']\n\n    def test_parse_bad_list_raises_error(self):\n        from certbot import errors\n        with pytest.raises(errors.MisconfigurationError):\n            self.statements.parse('lol not a list')\n\n    def test_parse_hides_trailing_whitespace(self):\n        self.statements.parse(self.raw + ['\\n\\n  '])\n        assert isinstance(self.statements.dump()[-1], list)\n        assert self.statements.dump(True)[-1].isspace() is True\n        assert self.statements.dump(True)[-1] == '\\n\\n  '\n\n    def test_iterate(self):\n        self.statements.parse(self.raw)\n        expected = [['sentence', 'one'], ['sentence', 'two']]\n        for i, elem in enumerate(self.statements.iterate(match=lambda x: 'sentence' in x)):\n            assert expected[i] == elem.dump()\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/parser_test.py",
    "content": "\"\"\"Tests for certbot_nginx._internal.parser.\"\"\"\nimport glob\nimport re\nimport shutil\nimport sys\n\nimport pytest\n\nfrom certbot import errors\nfrom certbot.compat import os\nfrom certbot_nginx._internal import nginxparser\nfrom certbot_nginx._internal import obj\nfrom certbot_nginx._internal import parser\nfrom certbot_nginx._internal.tests import test_util as util\n\n\nclass NginxParserTest(util.NginxTest):\n    \"\"\"Nginx Parser Test.\"\"\"\n\n    def tearDown(self):\n        shutil.rmtree(self.temp_dir)\n        shutil.rmtree(self.config_dir)\n        shutil.rmtree(self.work_dir)\n\n    def test_root_normalized(self):\n        path = os.path.join(self.temp_dir, \"etc_nginx/////\"\n                            \"ubuntu_nginx/../../etc_nginx\")\n        nparser = parser.NginxParser(path)\n        assert nparser.root == self.config_path\n\n    def test_root_absolute(self):\n        curr_dir = os.getcwd()\n        try:\n            # On Windows current directory may be on a different drive than self.tempdir.\n            # However a relative path between two different drives is invalid. So we move to\n            # self.tempdir to ensure that we stay on the same drive.\n            os.chdir(self.temp_dir)\n            nparser = parser.NginxParser(os.path.relpath(self.config_path))\n            assert nparser.root == self.config_path\n        finally:\n            os.chdir(curr_dir)\n\n    def test_root_no_trailing_slash(self):\n        nparser = parser.NginxParser(self.config_path + os.path.sep)\n        assert nparser.root == self.config_path\n\n    def test_load(self):\n        \"\"\"Test recursive conf file parsing.\n\n        \"\"\"\n        nparser = parser.NginxParser(self.config_path)\n        nparser.load()\n        assert {nparser.abs_path(x) for x in\n                          ['foo.conf', 'nginx.conf', 'server.conf', 'mime.types',\n                           'sites-enabled/default',\n                           'sites-enabled/both.com',\n                           'sites-enabled/example.com',\n                           'sites-enabled/headers.com',\n                           'sites-enabled/migration.com',\n                           'sites-enabled/sslon.com',\n                           'sites-enabled/globalssl.com',\n                           'sites-enabled/ipv6.com',\n                           'sites-enabled/ipv6ssl.com',\n                           'sites-enabled/example.net',\n                           'sites-enabled/no-listens.com',\n                           'sites-enabled/addr-80.com']} == \\\n                         set(nparser.parsed.keys())\n        assert [['server_name', 'somename', 'alias', 'another.alias']] == \\\n                         nparser.parsed[nparser.abs_path('server.conf')]\n        assert [[['server'], [['listen', '69.50.225.155:9000'],\n                                        ['listen', '127.0.0.1'],\n                                        ['server_name', '.example.com'],\n                                        ['server_name', 'example.*']]]] == \\\n                         nparser.parsed[nparser.abs_path(\n                             'sites-enabled/example.com')]\n\n    def test_included_load(self):\n        # Test for when the root file doesn't have http in it\n        nparser = parser.NginxParser(self.config_path)\n        nparser.config_root = os.path.join(self.config_path, \"nginx-include.conf\")\n        nparser.load()\n        assert len(nparser.parsed) > 1\n        assert len(nparser.parsed[nparser.config_root]) == 4\n        assert os.path.join(self.config_path, \"nginx.conf\") in nparser.parsed\n\n    def test_abs_path(self):\n        nparser = parser.NginxParser(self.config_path)\n        if os.name != 'nt':\n            assert '/etc/nginx/*' == nparser.abs_path('/etc/nginx/*')\n            assert os.path.join(self.config_path, 'foo/bar') == \\\n                             nparser.abs_path('foo/bar')\n        else:\n            assert 'C:\\\\etc\\\\nginx\\\\*' == nparser.abs_path('C:\\\\etc\\\\nginx\\\\*')\n            assert os.path.join(self.config_path, 'foo\\\\bar') == \\\n                             nparser.abs_path('foo\\\\bar')\n\n\n    def test_filedump(self):\n        nparser = parser.NginxParser(self.config_path)\n        nparser.filedump('test', lazy=False)\n        # pylint: disable=protected-access\n        parsed = nparser._parse_files(nparser.abs_path(\n            'sites-enabled/example.com.test'))\n        assert 4 == len(glob.glob(nparser.abs_path('*.test')))\n        assert 12 == len(\n            glob.glob(nparser.abs_path('sites-enabled/*.test')))\n        assert [[['server'], [['listen', '69.50.225.155:9000'],\n                                        ['listen', '127.0.0.1'],\n                                        ['server_name', '.example.com'],\n                                        ['server_name', 'example.*']]]] == \\\n                         parsed[nparser.abs_path('sites-enabled/example.com.test')]\n\n    def test__do_for_subarray(self):\n        # pylint: disable=protected-access\n        mylists = [([[2], [3], [2]], [[0], [2]]),\n                   ([[2], [3], [4]], [[0]]),\n                   ([[4], [3], [2]], [[2]]),\n                   ([], []),\n                   (2, []),\n                   ([[[2], [3], [2]], [[2], [3], [2]]],\n                        [[0, 0], [0, 2], [1, 0], [1, 2]]),\n                   ([[[0], [3], [2]], [[2], [3], [2]]], [[0, 2], [1, 0], [1, 2]]),\n                   ([[[0], [3], [4]], [[2], [3], [2]]], [[1, 0], [1, 2]]),\n                   ([[[0], [3], [4]], [[5], [3], [2]]], [[1, 2]]),\n                   ([[[0], [3], [4]], [[5], [3], [0]]], [])]\n\n        for mylist, result in mylists:\n            paths: list[list[int]] = []\n            parser._do_for_subarray(mylist,\n                                    lambda x: isinstance(x, list) and\n                                    len(x) >= 1 and\n                                    x[0] == 2,\n                                    lambda x, y, pts=paths: pts.append(y))\n            assert paths == result\n\n    def test_get_vhosts_global_ssl(self):\n        nparser = parser.NginxParser(self.config_path)\n        vhosts = nparser.get_vhosts()\n\n        vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'),\n                                [obj.Addr('4.8.2.6', '57', True, False,\n                                          False, False)],\n                                True, True, {'globalssl.com'}, [], [0])\n\n        globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0]\n        assert vhost == globalssl_com\n\n    def test_get_vhosts(self):\n        nparser = parser.NginxParser(self.config_path)\n        vhosts = nparser.get_vhosts()\n\n        vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'),\n                                 [obj.Addr('', '8080', False, False,\n                                           False, False)],\n                                 False, True,\n                                 {'localhost',\n                                      r'~^(www\\.)?(example|bar)\\.'},\n                                 [], [10, 1, 9])\n        vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'),\n                                 [obj.Addr('somename', '8080', False, False,\n                                           False, False),\n                                  obj.Addr('', '8000', False, False,\n                                           False, False)],\n                                 False, True,\n                                 {'somename', 'another.alias', 'alias'},\n                                 [], [10, 1, 12])\n        vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'),\n                                 [obj.Addr('69.50.225.155', '9000',\n                                           False, False, False, False),\n                                  obj.Addr('127.0.0.1', '', False, False,\n                                           False, False)],\n                                 False, True,\n                                 {'.example.com', 'example.*'}, [], [0])\n        vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'),\n                                 [obj.Addr('myhost', '', False, True,\n                                           False, False),\n                                  obj.Addr('otherhost', '', False, True,\n                                           False, False)],\n                                 False, True, {'www.example.org'},\n                                 [], [0])\n        vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'),\n                                 [obj.Addr('*', '80', True, True,\n                                           False, False)],\n                                 True, True, {'*.www.foo.com',\n                                                  '*.www.example.com'},\n                                 [], [2, 1, 0])\n\n        assert 21 == len(vhosts)\n        example_com = [x for x in vhosts if 'example.com' in x.filep][0]\n        assert vhost3 == example_com\n        default = [x for x in vhosts if 'default' in x.filep][0]\n        assert vhost4 == default\n        fooconf = [x for x in vhosts if 'foo.conf' in x.filep][0]\n        assert vhost5 == fooconf\n        localhost = [x for x in vhosts if 'localhost' in x.names][0]\n        assert vhost1 == localhost\n        somename = [x for x in vhosts if 'somename' in x.names][0]\n        assert vhost2 == somename\n\n    def test_has_ssl_on_directive(self):\n        nparser = parser.NginxParser(self.config_path)\n        mock_vhost = obj.VirtualHost(None, None, None, None, None,\n              [['listen', 'myhost default_server'],\n               ['server_name', 'www.example.org'],\n               [['location', '/'], [['root', 'html'], ['index', 'index.html index.htm']]]\n               ], None)\n        assert not nparser.has_ssl_on_directive(mock_vhost)\n        mock_vhost.raw = [['listen', '*:80', 'default_server', 'ssl'],\n                          ['server_name', '*.www.foo.com', '*.www.example.com'],\n                          ['root', '/home/ubuntu/sites/foo/']]\n        assert not nparser.has_ssl_on_directive(mock_vhost)\n        mock_vhost.raw = [['listen', '80 ssl'],\n                          ['server_name', '*.www.foo.com', '*.www.example.com']]\n        assert not nparser.has_ssl_on_directive(mock_vhost)\n        mock_vhost.raw = [['listen', '80'],\n                          ['ssl', 'on'],\n                          ['server_name', '*.www.foo.com', '*.www.example.com']]\n        assert nparser.has_ssl_on_directive(mock_vhost) is True\n\n    def test_remove_server_directives(self):\n        nparser = parser.NginxParser(self.config_path)\n        mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),\n                                     None, None, None,\n                                     {'localhost',\n                                           r'~^(www\\.)?(example|bar)\\.'},\n                                     None, [10, 1, 9])\n        example_com = nparser.abs_path('sites-enabled/example.com')\n        names = {'.example.com', 'example.*'}\n        mock_vhost.filep = example_com\n        mock_vhost.names = names\n        mock_vhost.path = [0]\n        nparser.add_server_directives(mock_vhost,\n                                      [['foo', 'bar'], ['ssl_certificate',\n                                                        '/etc/ssl/cert2.pem']])\n        nparser.remove_server_directives(mock_vhost, 'foo')\n        nparser.remove_server_directives(mock_vhost, 'ssl_certificate')\n        assert nparser.parsed[example_com] == \\\n            [[['server'], [['listen', '69.50.225.155:9000'],\n                           ['listen', '127.0.0.1'],\n                           ['server_name', '.example.com'],\n                           ['server_name', 'example.*'],\n                           []]]]\n\n    def test_add_server_directives(self):\n        nparser = parser.NginxParser(self.config_path)\n        mock_vhost = obj.VirtualHost(nparser.abs_path('nginx.conf'),\n                                     None, None, None,\n                                     {'localhost',\n                                           r'~^(www\\.)?(example|bar)\\.'},\n                                     None, [10, 1, 9])\n        nparser.add_server_directives(mock_vhost,\n                                      [['foo', 'bar'], ['\\n ', 'ssl_certificate', ' ',\n                                                        '/etc/ssl/cert.pem']])\n        ssl_re = re.compile(r'\\n\\s+ssl_certificate /etc/ssl/cert.pem')\n        dump = nginxparser.dumps(nparser.parsed[nparser.abs_path('nginx.conf')])\n        assert 1 == len(re.findall(ssl_re, dump))\n\n        example_com = nparser.abs_path('sites-enabled/example.com')\n        names = {'.example.com', 'example.*'}\n        mock_vhost.filep = example_com\n        mock_vhost.names = names\n        mock_vhost.path = [0]\n        nparser.add_server_directives(mock_vhost,\n                                      [['foo', 'bar'], ['ssl_certificate',\n                                                        '/etc/ssl/cert2.pem']])\n        nparser.add_server_directives(mock_vhost, [['foo', 'bar']])\n        from certbot_nginx._internal.parser import COMMENT\n        assert nparser.parsed[example_com] == \\\n            [[['server'], [['listen', '69.50.225.155:9000'],\n                           ['listen', '127.0.0.1'],\n                           ['server_name', '.example.com'],\n                           ['server_name', 'example.*'],\n                           ['foo', 'bar'],\n                           ['#', COMMENT],\n                           ['ssl_certificate', '/etc/ssl/cert2.pem'],\n                           ['#', COMMENT], [], []\n                           ]]]\n\n        server_conf = nparser.abs_path('server.conf')\n        names = {'alias', 'another.alias', 'somename'}\n        mock_vhost.filep = server_conf\n        mock_vhost.names = names\n        mock_vhost.path = []\n        with pytest.raises(errors.MisconfigurationError):\n            nparser.add_server_directives(mock_vhost,\n                          [['foo', 'bar'],\n                           ['ssl_certificate', '/etc/ssl/cert2.pem']])\n\n    def test_comment_is_repeatable(self):\n        nparser = parser.NginxParser(self.config_path)\n        example_com = nparser.abs_path('sites-enabled/example.com')\n        mock_vhost = obj.VirtualHost(example_com,\n                                     None, None, None,\n                                     {'.example.com', 'example.*'},\n                                     None, [0])\n        nparser.add_server_directives(mock_vhost,\n                                      [['\\n  ', '#', ' ', 'what a nice comment']])\n        nparser.add_server_directives(mock_vhost,\n                                      [['\\n  ', 'include', ' ',\n                                      nparser.abs_path('comment_in_file.conf')]])\n        from certbot_nginx._internal.parser import COMMENT\n        assert nparser.parsed[example_com] == \\\n            [[['server'], [['listen', '69.50.225.155:9000'],\n                           ['listen', '127.0.0.1'],\n                           ['server_name', '.example.com'],\n                           ['server_name', 'example.*'],\n                           ['#', ' ', 'what a nice comment'],\n                           [],\n                           ['include', nparser.abs_path('comment_in_file.conf')],\n                           ['#', COMMENT],\n                           []]]]\n\n    def test_replace_server_directives(self):\n        nparser = parser.NginxParser(self.config_path)\n        target = {'.example.com', 'example.*'}\n        filep = nparser.abs_path('sites-enabled/example.com')\n        mock_vhost = obj.VirtualHost(filep, None, None, None, target, None, [0])\n        nparser.update_or_add_server_directives(\n            mock_vhost, [['server_name', 'foobar.com']])\n        from certbot_nginx._internal.parser import COMMENT\n        assert nparser.parsed[filep] == \\\n            [[['server'], [['listen', '69.50.225.155:9000'],\n                           ['listen', '127.0.0.1'],\n                           ['server_name', 'foobar.com'], ['#', COMMENT],\n                           ['server_name', 'example.*'], []\n                           ]]]\n        mock_vhost.names = {'foobar.com', 'example.*'}\n        nparser.update_or_add_server_directives(\n            mock_vhost, [['ssl_certificate', 'cert.pem']])\n        assert nparser.parsed[filep] == \\\n            [[['server'], [['listen', '69.50.225.155:9000'],\n                           ['listen', '127.0.0.1'],\n                           ['server_name', 'foobar.com'], ['#', COMMENT],\n                           ['server_name', 'example.*'], [],\n                           ['ssl_certificate', 'cert.pem'], ['#', COMMENT], [],\n                           ]]]\n\n    def test_get_best_match(self):\n        target_name = 'www.eff.org'\n        names = [{'www.eff.org', 'irrelevant.long.name.eff.org', '*.org'},\n                 {'eff.org', 'ww2.eff.org', 'test.www.eff.org'},\n                 {'*.eff.org', '.www.eff.org'},\n                 {'.eff.org', '*.org'},\n                 {'www.eff.', 'www.eff.*', '*.www.eff.org'},\n                 {'example.com', r'~^(www\\.)?(eff.+)', '*.eff.*'},\n                 {'*', r'~^(www\\.)?(eff.+)'},\n                 {'www.*', r'~^(www\\.)?(eff.+)', '.test.eff.org'},\n                 {'*.org', r'*.eff.org', 'www.eff.*'},\n                 {'*.www.eff.org', 'www.*'},\n                 {'*.org'},\n                 set(),\n                 {'example.com'},\n                 {'www.Eff.org'},\n                 {'.efF.org'}]\n        winners = [('exact', 'www.eff.org'),\n                   (None, None),\n                   ('exact', '.www.eff.org'),\n                   ('wildcard_start', '.eff.org'),\n                   ('wildcard_end', 'www.eff.*'),\n                   ('regex', r'~^(www\\.)?(eff.+)'),\n                   ('wildcard_start', '*'),\n                   ('wildcard_end', 'www.*'),\n                   ('wildcard_start', '*.eff.org'),\n                   ('wildcard_end', 'www.*'),\n                   ('wildcard_start', '*.org'),\n                   (None, None),\n                   (None, None),\n                   ('exact', 'www.Eff.org'),\n                   ('wildcard_start', '.efF.org')]\n\n        for i, winner in enumerate(winners):\n            assert winner == \\\n                             parser.get_best_match(target_name, names[i])\n\n    def test_comment_directive(self):\n        # pylint: disable=protected-access\n        block = nginxparser.UnspacedList([\n            [\"\\n\", \"a\", \" \", \"b\", \"\\n\"],\n            [\"c\", \" \", \"d\"],\n            [\"\\n\", \"e\", \" \", \"f\"]])\n        from certbot_nginx._internal.parser import COMMENT_BLOCK\n        from certbot_nginx._internal.parser import comment_directive\n        comment_directive(block, 1)\n        comment_directive(block, 0)\n        assert block.spaced == [\n            [\"\\n\", \"a\", \" \", \"b\", \"\\n\"],\n            COMMENT_BLOCK,\n            \"\\n\",\n            [\"c\", \" \", \"d\"],\n            COMMENT_BLOCK,\n            [\"\\n\", \"e\", \" \", \"f\"]]\n\n    def test_comment_out_directive(self):\n        server_block = nginxparser.loads(\"\"\"\n            server {\n                listen 80;\n                root /var/www/html;\n                index star.html;\n\n                server_name *.functorkitten.xyz;\n                ssl_session_timeout 1440m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2;\n\n                ssl_prefer_server_ciphers on;\n            }\"\"\")\n        block = server_block[0][1]\n        from certbot_nginx._internal.parser import _comment_out_directive\n        _comment_out_directive(block, 4, \"blah1\")\n        _comment_out_directive(block, 5, \"blah2\")\n        _comment_out_directive(block, 6, \"blah3\")\n        assert block.spaced == [\n            ['\\n                ', 'listen', ' ', '80'],\n            ['\\n                ', 'root', ' ', '/var/www/html'],\n            ['\\n                ', 'index', ' ', 'star.html'],\n            ['\\n\\n                ', 'server_name', ' ', '*.functorkitten.xyz'],\n            ['\\n                ', '#', ' ssl_session_timeout 1440m; # duplicated in blah1'],\n            [' ', '#', ' ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # duplicated in blah2'],\n            ['\\n\\n                ', '#', ' ssl_prefer_server_ciphers on; # duplicated in blah3'],\n            '\\n            ']\n\n    def test_parse_server_raw_ssl(self):\n        server = parser._parse_server_raw([ #pylint: disable=protected-access\n            ['listen', '443']\n        ])\n        assert not server['ssl']\n\n        server = parser._parse_server_raw([ #pylint: disable=protected-access\n            ['listen', '443', 'ssl']\n        ])\n        assert server['ssl']\n\n        server = parser._parse_server_raw([ #pylint: disable=protected-access\n            ['listen', '443'], ['ssl', 'off']\n        ])\n        assert not server['ssl']\n\n        server = parser._parse_server_raw([ #pylint: disable=protected-access\n            ['listen', '443'], ['ssl', 'on']\n        ])\n        assert server['ssl']\n\n    def test_parse_server_raw_comment(self):\n        testdata = \"\"\"\n        server_name *.goo.far\n            # commented\n            baz.com;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['names'] == {'*.goo.far', 'baz.com'}\n\n        testdata = \"\"\"\n        server_name *.goo.far # commented\n            baz.com;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['names'] == {'*.goo.far', 'baz.com'}\n\n        testdata = \"\"\"\n        server_name *.goo.far # commented\n            ;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['names'] == {'*.goo.far'}\n\n        # known bug; see https://github.com/certbot/certbot/issues/9942\n        testdata = \"\"\"\n        server_name *.goo.far\n            #commented\n            ;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['names'] == {'*.goo.far', '#commented'}\n\n        # same bug; # isn't actually allowed in domains\n        testdata = \"\"\"\n        server_name *.go#o.far\n            ;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['names'] == {'*.go#o.far'}\n\n        testdata = \"\"\"\n        listen 443\n            # commented\n            ssl;\n        \"\"\"\n        loaded = nginxparser.loads(testdata)\n        server = parser._parse_server_raw(loaded) #pylint: disable=protected-access\n        assert server['addrs'] == {obj.Addr.fromstring('443 ssl')}\n\n    def test_parse_server_raw_unix(self):\n        server = parser._parse_server_raw([ #pylint: disable=protected-access\n            ['listen', 'unix:/var/run/nginx.sock']\n        ])\n        assert len(server['addrs']) == 0\n\n    def test_parse_server_global_ssl_applied(self):\n        nparser = parser.NginxParser(self.config_path)\n        server = nparser.parse_server([\n            ['listen', '443']\n        ])\n        assert server['ssl']\n\n    def test_duplicate_vhost(self):\n        nparser = parser.NginxParser(self.config_path)\n\n        vhosts = nparser.get_vhosts()\n        default = [x for x in vhosts if 'default' in x.filep][0]\n        new_vhost = nparser.duplicate_vhost(default, remove_singleton_listen_params=True)\n        nparser.filedump(ext='')\n\n        # check properties of new vhost\n        assert next(iter(new_vhost.addrs)).default is False\n        assert new_vhost.path != default.path\n\n        # check that things are written to file correctly\n        new_nparser = parser.NginxParser(self.config_path)\n        new_vhosts = new_nparser.get_vhosts()\n        new_defaults = [x for x in new_vhosts if 'default' in x.filep]\n        assert len(new_defaults) == 2\n        new_vhost_parsed = new_defaults[1]\n        assert next(iter(new_vhost_parsed.addrs)).default is False\n        assert next(iter(default.names)) == next(iter(new_vhost_parsed.names))\n        assert len(default.raw) == len(new_vhost_parsed.raw)\n        assert next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs)))\n\n    def test_duplicate_vhost_remove_ipv6only(self):\n        nparser = parser.NginxParser(self.config_path)\n\n        vhosts = nparser.get_vhosts()\n        ipv6ssl = [x for x in vhosts if 'ipv6ssl' in x.filep][0]\n        new_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=True)\n        nparser.filedump(ext='')\n\n        for addr in new_vhost.addrs:\n            assert not addr.ipv6only\n\n        identical_vhost = nparser.duplicate_vhost(ipv6ssl, remove_singleton_listen_params=False)\n        nparser.filedump(ext='')\n\n        called = False\n        for addr in identical_vhost.addrs:\n            if addr.ipv6:\n                assert addr.ipv6only\n                called = True\n        assert called\n\n    def test_valid_unicode_characters(self):\n        nparser = parser.NginxParser(self.config_path)\n        path = nparser.abs_path('valid_unicode_comments.conf')\n        parsed = nparser._parse_files(path)  # pylint: disable=protected-access\n        assert ['server'] == parsed[path][2][0]\n        assert ['listen', '80'] == parsed[path][2][1][3]\n\n    def test_valid_unicode_roundtrip(self):\n        \"\"\"This tests the parser's ability to load and save a config containing Unicode\"\"\"\n        nparser = parser.NginxParser(self.config_path)\n        nparser._parse_files(\n            nparser.abs_path('valid_unicode_comments.conf')\n        ) # pylint: disable=protected-access\n        nparser.filedump(lazy=False)\n\n    def test_invalid_unicode_characters(self):\n        with self.assertLogs() as log:\n            nparser = parser.NginxParser(self.config_path)\n            path = nparser.abs_path('invalid_unicode_comments.conf')\n            parsed = nparser._parse_files(path)  # pylint: disable=protected-access\n\n        assert {} == parsed\n        assert any(\n            ('invalid character' in output) and ('UTF-8' in output)\n            for output in log.output\n        )\n\n    def test_valid_unicode_characters_in_ssl_options(self):\n        nparser = parser.NginxParser(self.config_path)\n        path = nparser.abs_path('valid_unicode_comments.conf')\n        parsed = parser._parse_ssl_options(path)  # pylint: disable=protected-access\n        assert ['server'] == parsed[2][0]\n        assert ['listen', '80'] == parsed[2][1][3]\n\n    def test_invalid_unicode_characters_in_ssl_options(self):\n        with self.assertLogs() as log:\n            nparser = parser.NginxParser(self.config_path)\n            path = nparser.abs_path('invalid_unicode_comments.conf')\n            parsed = parser._parse_ssl_options(path)  # pylint: disable=protected-access\n\n        assert [] == parsed\n        assert any(\n            ('invalid character' in output) and ('UTF-8' in output)\n            for output in log.output\n        )\n\n\nif __name__ == \"__main__\":\n    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/test_util.py",
    "content": "\"\"\"Common utilities for certbot_nginx.\"\"\"\nimport copy\nimport importlib.resources\nimport shutil\nimport tempfile\nfrom contextlib import contextmanager\nfrom unittest import mock\n\nimport josepy as jose\n\nfrom certbot import util\nfrom certbot.compat import os\nfrom certbot.plugins import common\nfrom certbot.tests import util as test_util\nfrom certbot_nginx._internal import configurator\nfrom certbot_nginx._internal import nginxparser\n\nclass NginxTest(test_util.ConfigTestCase):\n\n    def setUp(self):\n        super().setUp()\n\n        self.configuration = self.config\n        self.config = None\n\n        self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(\n            \"etc_nginx\", __package__)\n        self.logs_dir = tempfile.mkdtemp('logs')\n\n        self.config_path = os.path.join(self.temp_dir, \"etc_nginx\")\n\n        self.rsa512jwk = jose.JWKRSA.load(test_util.load_vector(\n            \"rsa512_key.pem\"))\n\n    def tearDown(self):\n        # Cleanup opened resources after a test. This is usually done through atexit handlers in\n        # Certbot, but during tests, atexit will not run registered functions before tearDown is\n        # called and instead will run them right before the entire test process exits.\n        # It is a problem on Windows, that does not accept to clean resources before closing them.\n        util._release_locks()  # pylint: disable=protected-access\n\n        shutil.rmtree(self.temp_dir)\n        shutil.rmtree(self.config_dir)\n        shutil.rmtree(self.work_dir)\n        shutil.rmtree(self.logs_dir)\n\n    def get_nginx_configurator(self, config_path, config_dir, work_dir, logs_dir,\n            version=(1, 6, 2), openssl_version=\"1.0.2g\"):\n        \"\"\"Create an Nginx Configurator with the specified options.\"\"\"\n\n        backups = os.path.join(work_dir, \"backups\")\n\n        self.configuration.nginx_server_root = config_path\n        self.configuration.nginx_sleep_seconds = 0.1234\n        self.configuration.le_vhost_ext = \"-le-ssl.conf\"\n        self.configuration.config_dir = config_dir\n        self.configuration.work_dir = work_dir\n        self.configuration.logs_dir = logs_dir\n        self.configuration.backup_dir = backups\n        self.configuration.temp_checkpoint_dir = os.path.join(work_dir, \"temp_checkpoints\")\n        self.configuration.in_progress_dir = os.path.join(backups, \"IN_PROGRESS\")\n        self.configuration.server = \"https://acme-server.org:443/new\"\n        self.configuration.http01_port = 80\n        self.configuration.https_port = 5001\n\n        with mock.patch(\"certbot_nginx._internal.configurator.NginxConfigurator.\"\n                        \"config_test\"):\n            with mock.patch(\"certbot_nginx._internal.configurator.util.\"\n                            \"exe_exists\") as mock_exe_exists:\n                mock_exe_exists.return_value = True\n                config = configurator.NginxConfigurator(\n                    self.configuration,\n                    name=\"nginx\",\n                    version=version,\n                    openssl_version=openssl_version)\n                config.prepare()\n\n        return config\n\n\n@contextmanager\ndef get_data_filename(filename):\n    \"\"\"Gets the filename of a test data file.\"\"\"\n    ref = importlib.resources.files(__package__) / \"testdata\" / \"etc_nginx\"/ filename\n    with importlib.resources.as_file(ref) as path:\n        yield path\n\n\ndef filter_comments(tree):\n    \"\"\"Filter comment nodes from parsed configurations.\"\"\"\n\n    def traverse(tree):\n        \"\"\"Generator dropping comment nodes\"\"\"\n        for entry in tree:\n            # key, values = entry\n            spaceless = [e for e in entry if not nginxparser.spacey(e)]\n            if spaceless:\n                key = spaceless[0]\n                values = spaceless[1] if len(spaceless) > 1 else None\n            else:\n                key = values = \"\"\n            if isinstance(key, list):\n                new = copy.deepcopy(entry)\n                new[1] = filter_comments(values)\n                yield new\n            else:\n                if key != '#' and spaceless:\n                    yield spaceless\n\n    return list(traverse(tree))\n\n\ndef contains_at_depth(haystack, needle, n):\n    \"\"\"Is the needle in haystack at depth n?\n\n    Return true if the needle is present in one of the sub-iterables in haystack\n    at depth n. Haystack must be an iterable.\n    \"\"\"\n    # Specifically use hasattr rather than isinstance(..., collections.Iterable)\n    # because we want to include lists but reject strings.\n    if not hasattr(haystack, '__iter__') or hasattr(haystack, 'strip'):\n        return False\n    if n == 0:\n        return needle in haystack\n    for item in haystack:\n        if contains_at_depth(item, needle, n - 1):\n            return True\n    return False\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/broken.conf",
    "content": "# A faulty configuration file\n\npid        logs/nginx.pid;\n\n\nevents {\n    worker_connections  1024;\n}\n\ninclude foo.conf;\n\n@@@\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/comment_in_file.conf",
    "content": "# a comment inside a file"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/edge_cases.conf",
    "content": "# This is not a valid nginx config file but it tests edge cases in valid nginx syntax\n\nserver {\n  server_name simple;\n}\n\nserver {\n  server_name with.if;\n  location ~ ^/services/.+$ {\n        if ($request_filename ~* \\.(ttf|woff)$) {\n      add_header          Access-Control-Allow-Origin \"*\";\n    }\n  }\n}\n\nserver {\n  server_name with.complicated.headers;\n\n  location ~* \\.(?:gif|jpe?g|png)$ {\n\n    add_header  Pragma public;\n    add_header  Cache-Control  'public, must-revalidate, proxy-revalidate' \"test,;{}\" foo;\n    blah  \"hello;world\";\n\n    try_files   $uri @rewrites;\n  }\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/foo.conf",
    "content": "# a test nginx conf\nuser www-data;\n\nhttp {\n    server {\n        listen   *:80 default_server ssl;\n        server_name *.www.foo.com *.www.example.com;\n        root /home/ubuntu/sites/foo/;\n\n        location /status {\n            types {\n                image/jpeg jpg;\n            }\n        }\n\n        location ~ case_sensitive\\.php$ {\n            index index.php;\n            root /var/root;\n        }\n        location ~* case_insensitive\\.php$ {}\n        location = exact_match\\.php$ {}\n        location ^~ ignore_regex\\.php$ {}\n\n    }\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/invalid_unicode_comments.conf",
    "content": "# This configuration file is saved with EUC-KR (a.k.a. cp949) encoding,\n# including some Korean letters.\n\nserver {\n    # ȳϼ. 80 Ʈ û ٸ.\n    listen\t80;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/mime.types",
    "content": ""
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/minimalistic_comments.conf",
    "content": "# Use bar.conf when it's a full moon!\ninclude foo.conf;  # Kilroy was here\ncheck_status;\n\nserver {\n    #\n    # Don't forget to open up your firewall!\n    #\n    listen     1234;\n    # listen 80;\n}\n\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/multiline_quotes.conf",
    "content": "# Test nginx configuration file with multiline quoted strings.\n# Good example of usage for multilined quoted values is when\n# using Openresty's Lua directives and you wish to keep the\n# inline Lua code readable.\nhttp {\n  server {\n      listen *:443; # because there should be no other port open.\n\n      location / {\n        body_filter_by_lua 'ngx.ctx.buffered = (ngx.ctx.buffered or \"\") .. string.sub(ngx.arg[1], 1, 1000)\n                            if ngx.arg[2] then\n                              ngx.var.resp_body = ngx.ctx.buffered\n                            end';\n      }\n  }\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx-include.conf",
    "content": "# just a comment on top\n# so we're not at the very top\ninclude nginx.conf;\n# and another comment for fun\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/nginx.conf",
    "content": "# standard default nginx config\n\nuser  nobody;\nworker_processes  1;\n\nerror_log  logs/error.log;\nerror_log  logs/error.log  notice;\nerror_log  logs/error.log  info;\n\npid        logs/nginx.pid;\n\n\nevents {\n    worker_connections  1024;\n}\n\nempty {\n}\n\ninclude foo.conf;\n\nhttp {\n    include       mime.types;\n    include sites-enabled/*;\n    default_type  application/octet-stream;\n\n    log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                      '$status $body_bytes_sent \"$http_referer\" '\n                      '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n    access_log  logs/access.log  main;\n\n    sendfile        on;\n    tcp_nopush     on;\n\n    keepalive_timeout  0;\n\n    gzip  on;\n\n    server {\n        listen       8080;\n        server_name  localhost;\n        server_name  ~^(www\\.)?(example|bar)\\.;\n\n        charset koi8-r;\n\n        access_log  logs/host.access.log  main;\n\n        location / {\n            root   html;\n            index  index.html index.htm;\n        }\n\n        error_page  404              /404.html;\n\n        # redirect server error pages to the static page /50x.html\n        error_page   500 502 503 504  /50x.html;\n        location = /50x.html {\n            root   html;\n        }\n\n        # proxy the PHP scripts to Nginx listening on 127.0.0.1:80\n        #\n        location ~ \\.php$ {\n            proxy_pass   http://127.0.0.1;\n        }\n\n        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n        #\n        location ~ \\.php$ {\n            root           html;\n            fastcgi_pass   127.0.0.1:9000;\n            fastcgi_index  index.php;\n            fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;\n        }\n\n        # deny access to .htaccess files, if Nginx's document root\n        # concurs with nginx's one\n        #\n        location ~ /\\.ht {\n            deny  all;\n        }\n    }\n\n\n    # another virtual host using mix of IP-, name-, and port-based configuration\n    #\n    server {\n        listen       8000;\n        listen       somename:8080;\n        include server.conf;\n\n        location / {\n            root   html;\n            index  index.html index.htm;\n        }\n    }\n\n\n    # HTTPS server\n    #\n    #server {\n    #    listen       443 ssl;\n    #    server_name  localhost;\n\n    #    ssl_certificate      cert.pem;\n    #    ssl_certificate_key  cert.key;\n\n    #    ssl_session_cache    shared:SSL:1m;\n    #    ssl_session_timeout  5m;\n\n    #    ssl_ciphers  HIGH:!aNULL:!MD5;\n    #    ssl_prefer_server_ciphers  on;\n\n    #    location / {\n    #        root   html;\n    #        index  index.html index.htm;\n    #    }\n    #}\n\n    #include conf.d/test.conf;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/server.conf",
    "content": "server_name  somename  alias  another.alias;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/addr-80.com",
    "content": "server {\n    listen 1.2.3.4:80;\n    listen [1:20::300]:80;\n    server_name addr-80.com;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/both.com",
    "content": "server {\n  server_name ssl.both.com;\n}\n\n# a duplicate vhost\nserver {\n  server_name ssl.both.com;\n}\n\n# a duplicate by means of wildcard\nserver {\n  server_name *.both.com;\n}\n\n# combined HTTP and HTTPS\nserver {\n  server_name ssl.both.com;\n  listen 80;\n  listen 5001 ssl;\n\n  ssl_certificate      cert.pem;\n  ssl_certificate_key  cert.key;\n}\n\n# HTTPS, duplicate by means of wildcard\nserver {\n  server_name *.both.com;\n  listen 5001 ssl;\n\n  ssl_certificate      cert.pem;\n  ssl_certificate_key  cert.key;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/default",
    "content": "server {\n    listen       myhost default_server;\n    listen       otherhost default_server;\n    server_name \"www.example.org\";\n\n    location / {\n        root   html;\n        index  index.html index.htm;\n    }\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/example.com",
    "content": "server {\n    listen       69.50.225.155:9000;\n    listen       127.0.0.1;\n    server_name .example.com;\n    server_name example.*;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/example.net",
    "content": "server {\n    listen 80;\n    listen [::]:80;\n    server_name $hostname;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/globalssl.com",
    "content": "server {\n    server_name globalssl.com;\n    listen 4.8.2.6:57;\n}\n   \nserver {\n    server_name globalsslsetssl.com;\n    listen 4.8.2.6:57 ssl;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/headers.com",
    "content": "server {\n    server_name headers.com;\n    add_header X-Content-Type-Options nosniff;\n    ssl on;\n    ssl_certificate snakeoil.cert;\n    ssl_certificate_key snakeoil.key;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/ipv6.com",
    "content": "server {\n    listen 80;\n    listen [::]:80;\n    server_name ipv6.com;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com",
    "content": "server {\n    listen 443 ssl;\n    listen [::]:443 ssl ipv6only=on;\n    listen 5001 ssl;\n    listen [::]:5001 ssl ipv6only=on;\n    server_name ipv6ssl.com;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/migration.com",
    "content": "server {\n    server_name migration.com;\n    server_name summer.com;\n}\n   \nserver {\n    listen       443 ssl;\n    server_name  migration.com;\n    server_name  geese.com;\n\n    ssl_certificate      cert.pem;\n    ssl_certificate_key  cert.key;\n\n    ssl_session_cache    shared:SSL:1m;\n    ssl_session_timeout  5m;\n\n    ssl_ciphers  HIGH:!aNULL:!MD5;\n    ssl_prefer_server_ciphers  on;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/no-listens.com",
    "content": "server {\n    server_name no-listens.com;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/sites-enabled/sslon.com",
    "content": "server {\n\tserver_name sslon.com;\n\tssl on;\n\tssl_certificate snakeoil.cert;\n\tssl_certificate_key snakeoil.key;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/fastcgi_params",
    "content": "fastcgi_param\tQUERY_STRING\t\t$query_string;\nfastcgi_param\tREQUEST_METHOD\t\t$request_method;\nfastcgi_param\tCONTENT_TYPE\t\t$content_type;\nfastcgi_param\tCONTENT_LENGTH\t\t$content_length;\n\nfastcgi_param\tSCRIPT_FILENAME\t\t$request_filename;\nfastcgi_param\tSCRIPT_NAME\t\t$fastcgi_script_name;\nfastcgi_param\tREQUEST_URI\t\t$request_uri;\nfastcgi_param\tDOCUMENT_URI\t\t$document_uri;\nfastcgi_param\tDOCUMENT_ROOT\t\t$document_root;\nfastcgi_param\tSERVER_PROTOCOL\t\t$server_protocol;\n\nfastcgi_param\tGATEWAY_INTERFACE\tCGI/1.1;\nfastcgi_param\tSERVER_SOFTWARE\t\tnginx/$nginx_version;\n\nfastcgi_param\tREMOTE_ADDR\t\t$remote_addr;\nfastcgi_param\tREMOTE_PORT\t\t$remote_port;\nfastcgi_param\tSERVER_ADDR\t\t$server_addr;\nfastcgi_param\tSERVER_PORT\t\t$server_port;\nfastcgi_param\tSERVER_NAME\t\t$server_name;\n\nfastcgi_param\tHTTPS\t\t\t$https if_not_empty;\n\n# PHP only, required if PHP was built with --enable-force-cgi-redirect\nfastcgi_param\tREDIRECT_STATUS\t\t200;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-utf",
    "content": "# This map is not a full koi8-r <> utf8 map: it does not contain\n# box-drawing and some other characters.\tBesides this map contains\n# several koi8-u and Byelorussian letters which are not in koi8-r.\n# If you need a full and standard map, use contrib/unicode2nginx/koi-utf\n# map instead.\n\ncharset_map\tkoi8-r\tutf-8 {\n\n\t80\tE282AC; # euro\n\n\t95\tE280A2; # bullet\n\n\t9A\tC2A0;\t# &nbsp;\n\n\t9E\tC2B7;\t# &middot;\n\n\tA3\tD191;\t# small yo\n\tA4\tD194;\t# small Ukrainian ye\n\n\tA6\tD196;\t# small Ukrainian i\n\tA7\tD197;\t# small Ukrainian yi\n\n\tAD\tD291;\t# small Ukrainian soft g\n\tAE\tD19E;\t# small Byelorussian short u\n\n\tB0\tC2B0;\t# &deg;\n\n\tB3\tD081;\t# capital YO\n\tB4\tD084;\t# capital Ukrainian YE\n\n\tB6\tD086;\t# capital Ukrainian I\n\tB7\tD087;\t# capital Ukrainian YI\n\n\tB9\tE28496; # numero sign\n\n\tBD\tD290;\t# capital Ukrainian soft G\n\tBE\tD18E;\t# capital Byelorussian short U\n\n\tBF\tC2A9;\t# (C)\n\n\tC0\tD18E;\t# small yu\n\tC1\tD0B0;\t# small a\n\tC2\tD0B1;\t# small b\n\tC3\tD186;\t# small ts\n\tC4\tD0B4;\t# small d\n\tC5\tD0B5;\t# small ye\n\tC6\tD184;\t# small f\n\tC7\tD0B3;\t# small g\n\tC8\tD185;\t# small kh\n\tC9\tD0B8;\t# small i\n\tCA\tD0B9;\t# small j\n\tCB\tD0BA;\t# small k\n\tCC\tD0BB;\t# small l\n\tCD\tD0BC;\t# small m\n\tCE\tD0BD;\t# small n\n\tCF\tD0BE;\t# small o\n\n\tD0\tD0BF;\t# small p\n\tD1\tD18F;\t# small ya\n\tD2\tD180;\t# small r\n\tD3\tD181;\t# small s\n\tD4\tD182;\t# small t\n\tD5\tD183;\t# small u\n\tD6\tD0B6;\t# small zh\n\tD7\tD0B2;\t# small v\n\tD8\tD18C;\t# small soft sign\n\tD9\tD18B;\t# small y\n\tDA\tD0B7;\t# small z\n\tDB\tD188;\t# small sh\n\tDC\tD18D;\t# small e\n\tDD\tD189;\t# small shch\n\tDE\tD187;\t# small ch\n\tDF\tD18A;\t# small hard sign\n\n\tE0\tD0AE;\t# capital YU\n\tE1\tD090;\t# capital A\n\tE2\tD091;\t# capital B\n\tE3\tD0A6;\t# capital TS\n\tE4\tD094;\t# capital D\n\tE5\tD095;\t# capital YE\n\tE6\tD0A4;\t# capital F\n\tE7\tD093;\t# capital G\n\tE8\tD0A5;\t# capital KH\n\tE9\tD098;\t# capital I\n\tEA\tD099;\t# capital J\n\tEB\tD09A;\t# capital K\n\tEC\tD09B;\t# capital L\n\tED\tD09C;\t# capital M\n\tEE\tD09D;\t# capital N\n\tEF\tD09E;\t# capital O\n\n\tF0\tD09F;\t# capital P\n\tF1\tD0AF;\t# capital YA\n\tF2\tD0A0;\t# capital R\n\tF3\tD0A1;\t# capital S\n\tF4\tD0A2;\t# capital T\n\tF5\tD0A3;\t# capital U\n\tF6\tD096;\t# capital ZH\n\tF7\tD092;\t# capital V\n\tF8\tD0AC;\t# capital soft sign\n\tF9\tD0AB;\t# capital Y\n\tFA\tD097;\t# capital Z\n\tFB\tD0A8;\t# capital SH\n\tFC\tD0AD;\t# capital E\n\tFD\tD0A9;\t# capital SHCH\n\tFE\tD0A7;\t# capital CH\n\tFF\tD0AA;\t# capital hard sign\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/koi-win",
    "content": "charset_map\tkoi8-r\twindows-1251 {\n\n\t80\t88;\t# euro\n\n\t95\t95;\t# bullet\n\n\t9A\tA0;\t# &nbsp;\n\n\t9E\tB7;\t# &middot;\n\n\tA3\tB8;\t# small yo\n\tA4\tBA;\t# small Ukrainian ye\n\n\tA6\tB3;\t# small Ukrainian i\n\tA7\tBF;\t# small Ukrainian yi\n\n\tAD\tB4;\t# small Ukrainian soft g\n\tAE\tA2;\t# small Byelorussian short u\n\n\tB0\tB0;\t# &deg;\n\n\tB3\tA8;\t# capital YO\n\tB4\tAA;\t# capital Ukrainian YE\n\n\tB6\tB2;\t# capital Ukrainian I\n\tB7\tAF;\t# capital Ukrainian YI\n\n\tB9\tB9;\t# numero sign\n\n\tBD\tA5;\t# capital Ukrainian soft G\n\tBE\tA1;\t# capital Byelorussian short U\n\n\tBF\tA9;\t# (C)\n\n\tC0\tFE;\t# small yu\n\tC1\tE0;\t# small a\n\tC2\tE1;\t# small b\n\tC3\tF6;\t# small ts\n\tC4\tE4;\t# small d\n\tC5\tE5;\t# small ye\n\tC6\tF4;\t# small f\n\tC7\tE3;\t# small g\n\tC8\tF5;\t# small kh\n\tC9\tE8;\t# small i\n\tCA\tE9;\t# small j\n\tCB\tEA;\t# small k\n\tCC\tEB;\t# small l\n\tCD\tEC;\t# small m\n\tCE\tED;\t# small n\n\tCF\tEE;\t# small o\n\n\tD0\tEF;\t# small p\n\tD1\tFF;\t# small ya\n\tD2\tF0;\t# small r\n\tD3\tF1;\t# small s\n\tD4\tF2;\t# small t\n\tD5\tF3;\t# small u\n\tD6\tE6;\t# small zh\n\tD7\tE2;\t# small v\n\tD8\tFC;\t# small soft sign\n\tD9\tFB;\t# small y\n\tDA\tE7;\t# small z\n\tDB\tF8;\t# small sh\n\tDC\tFD;\t# small e\n\tDD\tF9;\t# small shch\n\tDE\tF7;\t# small ch\n\tDF\tFA;\t# small hard sign\n\n\tE0\tDE;\t# capital YU\n\tE1\tC0;\t# capital A\n\tE2\tC1;\t# capital B\n\tE3\tD6;\t# capital TS\n\tE4\tC4;\t# capital D\n\tE5\tC5;\t# capital YE\n\tE6\tD4;\t# capital F\n\tE7\tC3;\t# capital G\n\tE8\tD5;\t# capital KH\n\tE9\tC8;\t# capital I\n\tEA\tC9;\t# capital J\n\tEB\tCA;\t# capital K\n\tEC\tCB;\t# capital L\n\tED\tCC;\t# capital M\n\tEE\tCD;\t# capital N\n\tEF\tCE;\t# capital O\n\n\tF0\tCF;\t# capital P\n\tF1\tDF;\t# capital YA\n\tF2\tD0;\t# capital R\n\tF3\tD1;\t# capital S\n\tF4\tD2;\t# capital T\n\tF5\tD3;\t# capital U\n\tF6\tC6;\t# capital ZH\n\tF7\tC2;\t# capital V\n\tF8\tDC;\t# capital soft sign\n\tF9\tDB;\t# capital Y\n\tFA\tC7;\t# capital Z\n\tFB\tD8;\t# capital SH\n\tFC\tDD;\t# capital E\n\tFD\tD9;\t# capital SHCH\n\tFE\tD7;\t# capital CH\n\tFF\tDA;\t# capital hard sign\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/mime.types",
    "content": "types {\n\ttext/html\t\t\t\thtml htm shtml;\n\ttext/css\t\t\t\tcss;\n\ttext/xml\t\t\t\txml rss;\n\timage/gif\t\t\t\tgif;\n\timage/jpeg\t\t\t\tjpeg jpg;\n\tapplication/x-javascript\t\tjs;\n\tapplication/atom+xml\t\t\tatom;\n\n\ttext/mathml\t\t\t\tmml;\n\ttext/plain\t\t\t\ttxt;\n\ttext/vnd.sun.j2me.app-descriptor\tjad;\n\ttext/vnd.wap.wml\t\t\twml;\n\ttext/x-component\t\t\thtc;\n\n\timage/png\t\t\t\tpng;\n\timage/tiff\t\t\t\ttif tiff;\n\timage/vnd.wap.wbmp\t\t\twbmp;\n\timage/x-icon\t\t\t\tico;\n\timage/x-jng\t\t\t\tjng;\n\timage/x-ms-bmp\t\t\t\tbmp;\n\timage/svg+xml\t\t\t\tsvg svgz;\n\n\tapplication/java-archive\t\tjar war ear;\n\tapplication/json\t\t\tjson;\n\tapplication/mac-binhex40\t\thqx;\n\tapplication/msword\t\t\tdoc;\n\tapplication/pdf\t\t\t\tpdf;\n\tapplication/postscript\t\t\tps eps ai;\n\tapplication/rtf\t\t\t\trtf;\n\tapplication/vnd.ms-excel\t\txls;\n\tapplication/vnd.ms-powerpoint\t\tppt;\n\tapplication/vnd.wap.wmlc\t\twmlc;\n\tapplication/vnd.google-earth.kml+xml\tkml;\n\tapplication/vnd.google-earth.kmz\tkmz;\n\tapplication/x-7z-compressed\t\t7z;\n\tapplication/x-cocoa\t\t\tcco;\n\tapplication/x-java-archive-diff\t\tjardiff;\n\tapplication/x-java-jnlp-file\t\tjnlp;\n\tapplication/x-makeself\t\t\trun;\n\tapplication/x-perl\t\t\tpl pm;\n\tapplication/x-pilot\t\t\tprc pdb;\n\tapplication/x-rar-compressed\t\trar;\n\tapplication/x-redhat-package-manager\trpm;\n\tapplication/x-sea\t\t\tsea;\n\tapplication/x-shockwave-flash\t\tswf;\n\tapplication/x-stuffit\t\t\tsit;\n\tapplication/x-tcl\t\t\ttcl tk;\n\tapplication/x-x509-ca-cert\t\tder pem crt;\n\tapplication/x-xpinstall\t\t\txpi;\n\tapplication/xhtml+xml\t\t\txhtml;\n\tapplication/zip\t\t\t\tzip;\n\n\tapplication/octet-stream\t\tbin exe dll;\n\tapplication/octet-stream\t\tdeb;\n\tapplication/octet-stream\t\tdmg;\n\tapplication/octet-stream\t\teot;\n\tapplication/octet-stream\t\tiso img;\n\tapplication/octet-stream\t\tmsi msp msm;\n\tapplication/ogg\t\t\t\togx;\n\n\taudio/midi\t\t\t\tmid midi kar;\n\taudio/mpeg\t\t\t\tmpga mpega mp2 mp3 m4a;\n\taudio/ogg\t\t\t\toga ogg spx;\n\taudio/x-realaudio\t\t\tra;\n\taudio/webm\t\t\t\tweba;\n\n\tvideo/3gpp\t\t\t\t3gpp 3gp;\n\tvideo/mp4\t\t\t\tmp4;\n\tvideo/mpeg\t\t\t\tmpeg mpg mpe;\n\tvideo/ogg\t\t\t\togv;\n\tvideo/quicktime\t\t\t\tmov;\n\tvideo/webm\t\t\t\twebm;\n\tvideo/x-flv\t\t\t\tflv;\n\tvideo/x-mng\t\t\t\tmng;\n\tvideo/x-ms-asf\t\t\t\tasx asf;\n\tvideo/x-ms-wmv\t\t\t\twmv;\n\tvideo/x-msvideo\t\t\t\tavi;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi-ui.conf.1.4.1",
    "content": "[nx_extract]\nusername = naxsi_web\npassword = test\nport = 8081\nrules_path = /etc/nginx/naxsi_core.rules\n\n[nx_intercept]\nport = 8080\n\n[sql]\ndbtype = sqlite\nusername = root\npassword =\nhostname = 127.0.0.1\ndbname = naxsi_sig\n\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi.rules",
    "content": "# Sample rules file for default vhost.\n\nLearningMode;\nSecRulesEnabled;\n#SecRulesDisabled;\nDeniedUrl \"/RequestDenied\";\n\n## check rules\nCheckRule \"$SQL >= 8\" BLOCK;\nCheckRule \"$RFI >= 8\" BLOCK;\nCheckRule \"$TRAVERSAL >= 4\" BLOCK;\nCheckRule \"$EVADE >= 4\" BLOCK;\nCheckRule \"$XSS >= 8\" BLOCK;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/naxsi_core.rules",
    "content": "##################################\n## INTERNAL RULES IDS:1-10      ##\n##################################\n#weird_request : 1\n#big_body : 2\n#no_content_type : 3\n\n#MainRule \"str:yesone\" \"msg:foobar test pattern\" \"mz:ARGS\" \"s:$SQL:42\" id:1999;\n\n##################################\n## SQL Injections IDs:1000-1099 ##\n##################################\nMainRule \"rx:select|union|update|delete|insert|table|from|ascii|hex|unhex\" \"msg:sql keywords\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1000;\nMainRule \"str:\\\"\" \"msg:double quote\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1001;\nMainRule \"str:0x\" \"msg:0x, possible hex encoding\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:2\" id:1002;\n## Hardcore rules\nMainRule \"str:/*\" \"msg:mysql comment (/*)\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:8\" id:1003;\nMainRule \"str:*/\" \"msg:mysql comment (*/)\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:8\" id:1004;\nMainRule \"str:|\" \"msg:mysql keyword (|)\"  \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:8\" id:1005;\nMainRule \"rx:&&\" \"msg:mysql keyword (&&)\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:8\" id:1006;\n## end of hardcore rules\nMainRule \"str:--\" \"msg:mysql comment (--)\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1007;\nMainRule \"str:;\" \"msg:; in stuff\" \"mz:BODY|URL|ARGS\" \"s:$SQL:4\" id:1008;\nMainRule \"str:=\" \"msg:equal in var, probable sql/xss\" \"mz:ARGS|BODY\" \"s:$SQL:2\" id:1009;\nMainRule \"str:(\" \"msg:parenthesis, probable sql/xss\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1010;\nMainRule \"str:)\" \"msg:parenthesis, probable sql/xss\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1011;\nMainRule \"str:'\" \"msg:simple quote\" \"mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1013;\nMainRule \"str:\\\"\" \"msg:double quote\" \"mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1014;\nMainRule \"str:,\" \"msg:, in stuff\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1015;\nMainRule \"str:#\" \"msg:mysql comment (#)\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$SQL:4\" id:1016;\n\n###############################\n## OBVIOUS RFI IDs:1100-1199 ##\n###############################\nMainRule \"str:http://\" \"msg:html comment tag\" \"mz:ARGS|BODY|$HEADERS_VAR:Cookie\" \"s:$RFI:8\" id:1100;\nMainRule \"str:https://\" \"msg:html comment tag\" \"mz:ARGS|BODY|$HEADERS_VAR:Cookie\" \"s:$RFI:8\" id:1101;\nMainRule \"str:ftp://\" \"msg:html comment tag\" \"mz:ARGS|BODY|$HEADERS_VAR:Cookie\" \"s:$RFI:8\" id:1102;\nMainRule \"str:php://\" \"msg:html comment tag\" \"mz:ARGS|BODY|$HEADERS_VAR:Cookie\" \"s:$RFI:8\" id:1103;\n\n#######################################\n## Directory traversal IDs:1200-1299 ##\n#######################################                                          \nMainRule \"str:..\" \"msg:html comment tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:4\" id:1200;\nMainRule \"str:/etc/passwd\" \"msg:html comment tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:4\" id:1202;\nMainRule \"str:c:\\\\\" \"msg:html comment tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:4\" id:1203;\nMainRule \"str:cmd.exe\" \"msg:html comment tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:4\" id:1204;\nMainRule \"str:\\\\\" \"msg:html comment tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:4\" id:1205;\n#MainRule \"str:/\" \"msg:slash in args\" \"mz:ARGS|BODY|$HEADERS_VAR:Cookie\" \"s:$TRAVERSAL:2\" id:1206;\n########################################\n## Cross Site Scripting IDs:1300-1399 ##\n########################################\nMainRule \"str:<\" \"msg:html open tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1302;\nMainRule \"str:>\" \"msg:html close tag\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1303;\nMainRule \"str:'\" \"msg:simple quote\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1306;\nMainRule \"str:\\\"\" \"msg:double quote\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1307;\nMainRule \"str:(\" \"msg:parenthesis\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1308;\nMainRule \"str:)\" \"msg:parenthesis\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1309;\nMainRule \"str:[\" \"msg:html close comment tag\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$XSS:4\" id:1310;\nMainRule \"str:]\" \"msg:html close comment tag\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$XSS:4\" id:1311;\nMainRule \"str:~\" \"msg:html close comment tag\" \"mz:BODY|URL|ARGS|$HEADERS_VAR:Cookie\" \"s:$XSS:4\" id:1312;\nMainRule \"str:;\" \"msg:semi coma\" \"mz:ARGS|URL|BODY\" \"s:$XSS:8\" id:1313;\nMainRule \"str:`\"  \"msg:grave accent !\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1314;\nMainRule \"rx:%[2|3].\"  \"msg:double encoding !\" \"mz:ARGS|URL|BODY|$HEADERS_VAR:Cookie\" \"s:$XSS:8\" id:1315;\n\n####################################\n## Evading tricks IDs: 1400-1500 ##\n####################################\nMainRule \"str:&#\" \"msg: utf7/8 encoding\" \"mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie\" \"s:$EVADE:4\" id:1400;\nMainRule \"str:%U\" \"msg: M$ encoding\" \"mz:ARGS|BODY|URL|$HEADERS_VAR:Cookie\" \"s:$EVADE:4\" id:1401;\nMainRule negative \"rx:multipart/form-data|application/x-www-form-urlencoded\" \"msg:Content is neither multipart/x-www-form..\" \"mz:$HEADERS_VAR:Content-type\" \"s:$EVADE:4\" id:1402;\n\n#############################\n## File uploads: 1500-1600 ##\n#############################\nMainRule \"rx:.ph*|.asp*\" \"msg:asp/php file upload!\" \"mz:FILE_EXT\" \"s:$UPLOAD:8\" id:1500;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/nginx.conf",
    "content": "user www-data;\nworker_processes 4;\npid /run/nginx.pid;\n\nevents {\n\tworker_connections 768;\n\t# multi_accept on;\n}\n\nhttp {\n\n\t##\n\t# Basic Settings\n\t##\n\n\tsendfile on;\n\ttcp_nopush on;\n\ttcp_nodelay on;\n\tkeepalive_timeout 65;\n\ttypes_hash_max_size 2048;\n\t# server_tokens off;\n\n\t# server_names_hash_bucket_size 64;\n\t# server_name_in_redirect off;\n\n\tinclude /etc/nginx/mime.types;\n\tdefault_type application/octet-stream;\n\n\t##\n\t# Logging Settings\n\t##\n\n\taccess_log /var/log/nginx/access.log;\n\terror_log /var/log/nginx/error.log;\n\n\t##\n\t# Gzip Settings\n\t##\n\n\tgzip on;\n\tgzip_disable \"msie6\";\n\n\t# gzip_vary on;\n\t# gzip_proxied any;\n\t# gzip_comp_level 6;\n\t# gzip_buffers 16 8k;\n\t# gzip_http_version 1.1;\n\t# gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;\n\n\t##\n\t# nginx-naxsi config\n\t##\n\t# Uncomment it if you installed nginx-naxsi\n\t##\n\n\t#include /etc/nginx/naxsi_core.rules;\n\n\t##\n\t# nginx-passenger config\n\t##\n\t# Uncomment it if you installed nginx-passenger\n\t##\n\t\n\t#passenger_root /usr;\n\t#passenger_ruby /usr/bin/ruby;\n\n\t##\n\t# Virtual Host Configs\n\t##\n\n\tinclude /etc/nginx/conf.d/*.conf;\n\tinclude /etc/nginx/sites-enabled/*;\n}\n\n\n#mail {\n#\t# See sample authentication script at:\n#\t# http://wiki.nginx.org/ImapAuthenticateWithApachePhpScript\n# \n#\t# auth_http localhost/auth.php;\n#\t# pop3_capabilities \"TOP\" \"USER\";\n#\t# imap_capabilities \"IMAP4rev1\" \"UIDPLUS\";\n# \n#\tserver {\n#\t\tlisten     localhost:110;\n#\t\tprotocol   pop3;\n#\t\tproxy      on;\n#\t}\n# \n#\tserver {\n#\t\tlisten     localhost:143;\n#\t\tprotocol   imap;\n#\t\tproxy      on;\n#\t}\n#}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/proxy_params",
    "content": "proxy_set_header Host $http_host;\nproxy_set_header X-Real-IP $remote_addr;\nproxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\nproxy_set_header X-Forwarded-Proto $scheme;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/scgi_params",
    "content": "scgi_param\tREQUEST_METHOD\t\t$request_method;\nscgi_param\tREQUEST_URI\t\t$request_uri;\nscgi_param\tQUERY_STRING\t\t$query_string;\nscgi_param\tCONTENT_TYPE\t\t$content_type;\n\nscgi_param\tDOCUMENT_URI\t\t$document_uri;\nscgi_param\tDOCUMENT_ROOT\t\t$document_root;\nscgi_param\tSCGI\t\t\t1;\nscgi_param\tSERVER_PROTOCOL\t\t$server_protocol;\n\nscgi_param\tREMOTE_ADDR\t\t$remote_addr;\nscgi_param\tREMOTE_PORT\t\t$remote_port;\nscgi_param\tSERVER_PORT\t\t$server_port;\nscgi_param\tSERVER_NAME\t\t$server_name;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/sites-available/default",
    "content": "# You may add here your\n# server {\n#\t...\n# }\n# statements for each of your virtual hosts to this file\n\n##\n# You should look at the following URL's in order to grasp a solid understanding\n# of Nginx configuration files in order to fully unleash the power of Nginx.\n# http://wiki.nginx.org/Pitfalls\n# http://wiki.nginx.org/QuickStart\n# http://wiki.nginx.org/Configuration\n#\n# Generally, you will want to move this file somewhere, and start with a clean\n# file but keep this around for reference. Or just disable in sites-enabled.\n#\n# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.\n##\n\nserver {\n\tlisten 80 default_server;\n\tlisten [::]:80 default_server ipv6only=on;\n\n\troot /usr/share/nginx/html;\n\tindex index.html index.htm;\n\n\t# Make site accessible from http://localhost/\n\tserver_name localhost;\n\n\tlocation / {\n\t\t# First attempt to serve request as file, then\n\t\t# as directory, then fall back to displaying a 404.\n\t\ttry_files $uri $uri/ =404;\n\t\t# Uncomment to enable naxsi on this location\n\t\t# include /etc/nginx/naxsi.rules\n\t}\n\n\t# Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests\n\t#location /RequestDenied {\n\t#\tproxy_pass http://127.0.0.1:8080;    \n\t#}\n\n\t#error_page 404 /404.html;\n\n\t# redirect server error pages to the static page /50x.html\n\t#\n\t#error_page 500 502 503 504 /50x.html;\n\t#location = /50x.html {\n\t#\troot /usr/share/nginx/html;\n\t#}\n\n\t# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000\n\t#\n\t#location ~ \\.php$ {\n\t#\tfastcgi_split_path_info ^(.+\\.php)(/.+)$;\n\t#\t# NOTE: You should have \"cgi.fix_pathinfo = 0;\" in php.ini\n\t#\n\t#\t# With php5-cgi alone:\n\t#\tfastcgi_pass 127.0.0.1:9000;\n\t#\t# With php5-fpm:\n\t#\tfastcgi_pass unix:/var/run/php5-fpm.sock;\n\t#\tfastcgi_index index.php;\n\t#\tinclude fastcgi_params;\n\t#}\n\n\t# deny access to .htaccess files, if Apache's document root\n\t# concurs with nginx's one\n\t#\n\t#location ~ /\\.ht {\n\t#\tdeny all;\n\t#}\n}\n\n\n# another virtual host using mix of IP-, name-, and port-based configuration\n#\n#server {\n#\tlisten 8000;\n#\tlisten somename:8080;\n#\tserver_name somename alias another.alias;\n#\troot html;\n#\tindex index.html index.htm;\n#\n#\tlocation / {\n#\t\ttry_files $uri $uri/ =404;\n#\t}\n#}\n\n\n# HTTPS server\n#\n#server {\n#\tlisten 443;\n#\tserver_name localhost;\n#\n#\troot html;\n#\tindex index.html index.htm;\n#\n#\tssl on;\n#\tssl_certificate cert.pem;\n#\tssl_certificate_key cert.key;\n#\n#\tssl_session_timeout 5m;\n#\n#\tssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;\n#\tssl_ciphers \"HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES\";\n#\tssl_prefer_server_ciphers on;\n#\n#\tlocation / {\n#\t\ttry_files $uri $uri/ =404;\n#\t}\n#}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/uwsgi_params",
    "content": "uwsgi_param\tQUERY_STRING\t\t$query_string;\nuwsgi_param\tREQUEST_METHOD\t\t$request_method;\nuwsgi_param\tCONTENT_TYPE\t\t$content_type;\nuwsgi_param\tCONTENT_LENGTH\t\t$content_length;\n\nuwsgi_param\tREQUEST_URI\t\t$request_uri;\nuwsgi_param\tPATH_INFO\t\t$document_uri;\nuwsgi_param\tDOCUMENT_ROOT\t\t$document_root;\nuwsgi_param\tSERVER_PROTOCOL\t\t$server_protocol;\nuwsgi_param\tUWSGI_SCHEME\t\t$scheme;\n\nuwsgi_param\tREMOTE_ADDR\t\t$remote_addr;\nuwsgi_param\tREMOTE_PORT\t\t$remote_port;\nuwsgi_param\tSERVER_PORT\t\t$server_port;\nuwsgi_param\tSERVER_NAME\t\t$server_name;\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/ubuntu_nginx_1_4_6/default_vhost/nginx/win-utf",
    "content": "# This map is not a full windows-1251 <> utf8 map: it does not\n# contain Serbian and Macedonian letters.\tIf you need a full map,\n# use contrib/unicode2nginx/win-utf map instead.\n\ncharset_map\twindows-1251\tutf-8 {\n\n\t82\tE2809A; # single low-9 quotation mark\n\n\t84\tE2809E; # double low-9 quotation mark\n\t85\tE280A6; # ellipsis\n\t86\tE280A0; # dagger\n\t87\tE280A1; # double dagger\n\t88\tE282AC; # euro\n\t89\tE280B0; # per mille\n\n\t91\tE28098; # left single quotation mark\n\t92\tE28099; # right single quotation mark\n\t93\tE2809C; # left double quotation mark\n\t94\tE2809D; # right double quotation mark\n\t95\tE280A2; # bullet\n\t96\tE28093; # en dash\n\t97\tE28094; # em dash\n\n\t99\tE284A2; # trade mark sign\n\n\tA0\tC2A0;   # &nbsp;\n\tA1\tD18E;   # capital Byelorussian short U\n\tA2\tD19E;   # small Byelorussian short u\n\n\tA4\tC2A4;   # currency sign\n\tA5\tD290;   # capital Ukrainian soft G\n\tA6\tC2A6;   # broken bar\n\tA7\tC2A7;   # section sign\n\tA8\tD081;   # capital YO\n\tA9\tC2A9;   # (C)\n\tAA\tD084;   # capital Ukrainian YE\n\tAB\tC2AB;   # left-pointing double angle quotation mark\n\tAC\tC2AC;   # not sign\n\tAD\tC2AD;   # soft hyphen\n\tAE\tC2AE;   # (R)\n\tAF\tD087;   # capital Ukrainian YI\n\n\tB0\tC2B0;   # &deg;\n\tB1\tC2B1;   # plus-minus sign\n\tB2\tD086;   # capital Ukrainian I\n\tB3\tD196;   # small Ukrainian i\n\tB4\tD291;   # small Ukrainian soft g\n\tB5\tC2B5;   # micro sign\n\tB6\tC2B6;   # pilcrow sign\n\tB7\tC2B7;   # &middot;\n\tB8\tD191;   # small yo\n\tB9\tE28496; # numero sign\n\tBA\tD194;   # small Ukrainian ye\n\tBB\tC2BB;   # right-pointing double angle quotation mark\n\n\tBF\tD197;   # small Ukrainian yi\n\n\tC0\tD090;   # capital A\n\tC1\tD091;   # capital B\n\tC2\tD092;   # capital V\n\tC3\tD093;   # capital G\n\tC4\tD094;   # capital D\n\tC5\tD095;   # capital YE\n\tC6\tD096;   # capital ZH\n\tC7\tD097;   # capital Z\n\tC8\tD098;   # capital I\n\tC9\tD099;   # capital J\n\tCA\tD09A;   # capital K\n\tCB\tD09B;   # capital L\n\tCC\tD09C;   # capital M\n\tCD\tD09D;   # capital N\n\tCE\tD09E;   # capital O\n\tCF\tD09F;   # capital P\n\n\tD0\tD0A0;   # capital R\n\tD1\tD0A1;   # capital S\n\tD2\tD0A2;   # capital T\n\tD3\tD0A3;   # capital U\n\tD4\tD0A4;   # capital F\n\tD5\tD0A5;   # capital KH\n\tD6\tD0A6;   # capital TS\n\tD7\tD0A7;   # capital CH\n\tD8\tD0A8;   # capital SH\n\tD9\tD0A9;   # capital SHCH\n\tDA\tD0AA;   # capital hard sign\n\tDB\tD0AB;   # capital Y\n\tDC\tD0AC;   # capital soft sign\n\tDD\tD0AD;   # capital E\n\tDE\tD0AE;   # capital YU\n\tDF\tD0AF;   # capital YA\n\n\tE0\tD0B0;   # small a\n\tE1\tD0B1;   # small b\n\tE2\tD0B2;   # small v\n\tE3\tD0B3;   # small g\n\tE4\tD0B4;   # small d\n\tE5\tD0B5;   # small ye\n\tE6\tD0B6;   # small zh\n\tE7\tD0B7;   # small z\n\tE8\tD0B8;   # small i\n\tE9\tD0B9;   # small j\n\tEA\tD0BA;   # small k\n\tEB\tD0BB;   # small l\n\tEC\tD0BC;   # small m\n\tED\tD0BD;   # small n\n\tEE\tD0BE;   # small o\n\tEF\tD0BF;   # small p\n\n\tF0\tD180;   # small r\n\tF1\tD181;   # small s\n\tF2\tD182;   # small t\n\tF3\tD183;   # small u\n\tF4\tD184;   # small f\n\tF5\tD185;   # small kh\n\tF6\tD186;   # small ts\n\tF7\tD187;   # small ch\n\tF8\tD188;   # small sh\n\tF9\tD189;   # small shch\n\tFA\tD18A;   # small hard sign\n\tFB\tD18B;   # small y\n\tFC\tD18C;   # small soft sign\n\tFD\tD18D;   # small e\n\tFE\tD18E;   # small yu\n\tFF\tD18F;   # small ya\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tests/testdata/etc_nginx/valid_unicode_comments.conf",
    "content": "# This configuration file is saved with valid UTF-8 encoding,\n# including some CJK alphabets.\n\nserver {\n    # 안녕하세요. 80번 포트에서 요청을 기다린다.\n    # こんにちは。８０番ポートからリクエストを待つ。\n    # 你好。等待端口80上的请求。\n    listen\t80;\n}\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tls_configs/options-ssl-nginx-old.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n#\n# This file is installed when nginx < 1.13.0 or compiled against openssl < 1.0.2l. Since these\n# are deprecated, it is no longer receiving updates. To get the latest configuration\n# version, update nginx.\n\nssl_session_cache shared:le_nginx_SSL:10m;\nssl_session_timeout 1440m;\n\nssl_protocols TLSv1.2;\nssl_prefer_server_ciphers off;\n\nssl_ciphers \"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\";\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls12-only.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n#\n# This file is installed when nginx < 1.13.0 or compiled against openssl < 1.0.2l. Since these\n# are deprecated, it is no longer receiving updates. To get the latest configuration\n# version, update nginx.\n\nssl_session_cache shared:le_nginx_SSL:10m;\nssl_session_timeout 1440m;\nssl_session_tickets off;\n\nssl_protocols TLSv1.2;\nssl_prefer_server_ciphers off;\n\nssl_ciphers \"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\";\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tls_configs/options-ssl-nginx-tls13-session-tix-on.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n#\n# This file is installed when nginx < 1.13.0 or compiled against openssl < 1.0.2l. Since these\n# are deprecated, it is no longer receiving updates. To get the latest configuration\n# version, update nginx.\n\nssl_session_cache shared:le_nginx_SSL:10m;\nssl_session_timeout 1440m;\n\nssl_protocols TLSv1.2 TLSv1.3;\nssl_prefer_server_ciphers off;\n\nssl_ciphers \"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\";\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf",
    "content": "# This file contains important security parameters. If you modify this file\n# manually, Certbot will be unable to automatically provide future security\n# updates. Instead, Certbot will print and log an error message with a path to\n# the up-to-date file that you will need to refer to when manually updating\n# this file. Contents are based on https://ssl-config.mozilla.org\n\nssl_session_cache shared:le_nginx_SSL:10m;\nssl_session_timeout 1440m;\nssl_session_tickets off;\n\nssl_protocols TLSv1.2 TLSv1.3;\nssl_prefer_server_ciphers off;\n\nssl_ciphers \"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\";\n"
  },
  {
    "path": "certbot-nginx/src/certbot_nginx/py.typed",
    "content": ""
  },
  {
    "path": "letsencrypt-auto-source/README.md",
    "content": "# Legacy letsencrypt-auto files\n\n`certbot-auto` and `letsencrypt-auto` were two names for the same self-updating\nshell script that wrapped Certbot. Old versions of the script continue to rely\non pulling `letsencrypt-auto` and `letsencrypt-auto.sig` from this directory hosted on Github to download and\nverify updates. We're keeping these files and the tests for them around to\nprevent these old scripts from breaking.\n\nIf we need or want to remove these files and tests in the future, we can, but\nbefore we do, we should write a Let's Encrypt forum post describing the error\nmessage users will see and how they can work around the problem. See\nhttps://github.com/certbot/certbot/issues/8812 for more info.\n"
  },
  {
    "path": "letsencrypt-auto-source/letsencrypt-auto",
    "content": "#!/bin/sh\n#\n# Download and run the latest release version of the Certbot client.\n#\n# NOTE: THIS SCRIPT IS AUTO-GENERATED AND SELF-UPDATING\n#\n# IF YOU WANT TO EDIT IT LOCALLY, *ALWAYS* RUN YOUR COPY WITH THE\n# \"--no-self-upgrade\" FLAG\n#\n# IF YOU WANT TO SEND PULL REQUESTS, THE REAL SOURCE FOR THIS FILE IS\n# letsencrypt-auto-source/letsencrypt-auto.template AND\n# letsencrypt-auto-source/pieces/bootstrappers/*\n\nset -e  # Work even if somebody does \"sh thisscript.sh\".\n\n# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,\n# if you want to change where the virtual environment will be installed\n\n# HOME might not be defined when being run through something like systemd\nif [ -z \"$HOME\" ]; then\n  HOME=~root\nfi\nif [ -z \"$XDG_DATA_HOME\" ]; then\n  XDG_DATA_HOME=~/.local/share\nfi\nif [ -z \"$VENV_PATH\" ]; then\n  # We export these values so they are preserved properly if this script is\n  # rerun with sudo/su where $HOME/$XDG_DATA_HOME may have a different value.\n  export OLD_VENV_PATH=\"$XDG_DATA_HOME/letsencrypt\"\n  export VENV_PATH=\"/opt/eff.org/certbot/venv\"\nfi\nVENV_BIN=\"$VENV_PATH/bin\"\nBOOTSTRAP_VERSION_PATH=\"$VENV_PATH/certbot-auto-bootstrap-version.txt\"\nLE_AUTO_VERSION=\"1.14.0\"\nBASENAME=$(basename $0)\nUSAGE=\"Usage: $BASENAME [OPTIONS]\nA self-updating wrapper script for the Certbot ACME client. When run, updates\nto both this script and certbot will be downloaded and installed. After\nensuring you have the latest versions installed, certbot will be invoked with\nall arguments you have provided.\n\nHelp for certbot itself cannot be provided until it is installed.\n\n  --debug                                   attempt experimental installation\n  -h, --help                                print this help\n  -n, --non-interactive, --noninteractive   run without asking for user input\n  --no-bootstrap                            do not install OS dependencies\n  --no-permissions-check                    do not warn about file system permissions\n  --no-self-upgrade                         do not download updates\n  --os-packages-only                        install OS dependencies and exit\n  --install-only                            install certbot, upgrade if needed, and exit\n  -v, --verbose                             provide more output\n  -q, --quiet                               provide only update/error output;\n                                            implies --non-interactive\n\nAll arguments are accepted and forwarded to the Certbot client when run.\"\nexport CERTBOT_AUTO=\"$0\"\n\nfor arg in \"$@\" ; do\n  case \"$arg\" in\n    --debug)\n      DEBUG=1;;\n    --os-packages-only)\n      OS_PACKAGES_ONLY=1;;\n    --install-only)\n      INSTALL_ONLY=1;;\n    --no-self-upgrade)\n      # Do not upgrade this script (also prevents client upgrades, because each\n      # copy of the script pins a hash of the python client)\n      NO_SELF_UPGRADE=1;;\n    --no-permissions-check)\n      NO_PERMISSIONS_CHECK=1;;\n    --no-bootstrap)\n      NO_BOOTSTRAP=1;;\n    --help)\n      HELP=1;;\n    --noninteractive|--non-interactive)\n      NONINTERACTIVE=1;;\n    --quiet)\n      QUIET=1;;\n    renew)\n      ASSUME_YES=1;;\n    --verbose)\n      VERBOSE=1;;\n    -[!-]*)\n      OPTIND=1\n      while getopts \":hnvq\" short_arg $arg; do\n        case \"$short_arg\" in\n          h)\n            HELP=1;;\n          n)\n            NONINTERACTIVE=1;;\n          q)\n            QUIET=1;;\n          v)\n            VERBOSE=1;;\n        esac\n      done;;\n  esac\ndone\n\nif [ $BASENAME = \"letsencrypt-auto\" ]; then\n  # letsencrypt-auto does not respect --help or --yes for backwards compatibility\n  NONINTERACTIVE=1\n  HELP=0\nfi\n\n# Set ASSUME_YES to 1 if QUIET or NONINTERACTIVE\nif [ \"$QUIET\" = 1 -o \"$NONINTERACTIVE\" = 1 ]; then\n  ASSUME_YES=1\nfi\n\nsay() {\n    if [  \"$QUIET\" != 1 ]; then\n        echo \"$@\"\n    fi\n}\n\nerror() {\n    echo \"$@\"\n}\n\n# Support for busybox and others where there is no \"command\",\n# but \"which\" instead\nif command -v command > /dev/null 2>&1 ; then\n  export EXISTS=\"command -v\"\nelif which which > /dev/null 2>&1 ; then\n  export EXISTS=\"which\"\nelse\n  error \"Cannot find command nor which... please install one!\"\n  exit 1\nfi\n\n# Certbot itself needs root access for almost all modes of operation.\n# certbot-auto needs root access to bootstrap OS dependencies and install\n# Certbot at a protected path so it can be safely run as root. To accomplish\n# this, this script will attempt to run itself as root if it doesn't have the\n# necessary privileges by using `sudo` or falling back to `su` if it is not\n# available. The mechanism used to obtain root access can be set explicitly by\n# setting the environment variable LE_AUTO_SUDO to 'sudo', 'su', 'su_sudo',\n# 'SuSudo', or '' as used below.\n\n# Because the parameters in `su -c` has to be a string,\n# we need to properly escape it.\nSuSudo() {\n  args=\"\"\n  # This `while` loop iterates over all parameters given to this function.\n  # For each parameter, all `'` will be replace by `'\"'\"'`, and the escaped string\n  # will be wrapped in a pair of `'`, then appended to `$args` string\n  # For example, `echo \"It's only 1\\$\\!\"` will be escaped to:\n  #   'echo' 'It'\"'\"'s only 1$!'\n  #     │       │└┼┘│\n  #     │       │ │ └── `'s only 1$!'` the literal string\n  #     │       │ └── `\\\"'\\\"` is a single quote (as a string)\n  #     │       └── `'It'`, to be concatenated with the strings following it\n  #     └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself\n  while [ $# -ne 0 ]; do\n    args=\"$args'$(printf \"%s\" \"$1\" | sed -e \"s/'/'\\\"'\\\"'/g\")' \"\n    shift\n  done\n  su root -c \"$args\"\n}\n\n# Sets the environment variable SUDO to be the name of the program or function\n# to call to get root access. If this script already has root privleges, SUDO\n# is set to an empty string. The value in SUDO should be run with the command\n# to called with root privileges as arguments.\nSetRootAuthMechanism() {\n  SUDO=\"\"\n  if [ -n \"${LE_AUTO_SUDO+x}\" ]; then\n    case \"$LE_AUTO_SUDO\" in\n      SuSudo|su_sudo|su)\n        SUDO=SuSudo\n        ;;\n      sudo)\n        SUDO=\"sudo -E\"\n        ;;\n      '')\n        # If we're not running with root, don't check that this script can only\n        # be modified by system users and groups.\n        NO_PERMISSIONS_CHECK=1\n        ;;\n      *)\n        error \"Error: unknown root authorization mechanism '$LE_AUTO_SUDO'.\"\n        exit 1\n    esac\n    say \"Using preset root authorization mechanism '$LE_AUTO_SUDO'.\"\n  else\n    if test \"`id -u`\" -ne \"0\" ; then\n      if $EXISTS sudo 1>/dev/null 2>&1; then\n        SUDO=\"sudo -E\"\n      else\n        say \\\"sudo\\\" is not available, will use \\\"su\\\" for installation steps...\n        SUDO=SuSudo\n      fi\n    fi\n  fi\n}\n\nif [ \"$1\" = \"--cb-auto-has-root\" ]; then\n  shift 1\nelse\n  SetRootAuthMechanism\n  if [ -n \"$SUDO\" ]; then\n    say \"Requesting to rerun $0 with root privileges...\"\n    $SUDO \"$0\" --cb-auto-has-root \"$@\"\n    exit 0\n  fi\nfi\n\n# Runs this script again with the given arguments. --cb-auto-has-root is added\n# to the command line arguments to ensure we don't try to acquire root a\n# second time. After the script is rerun, we exit the current script.\nRerunWithArgs() {\n    \"$0\" --cb-auto-has-root \"$@\"\n    exit 0\n}\n\nBootstrapMessage() {\n  # Arguments: Platform name\n  say \"Bootstrapping dependencies for $1... (you can skip this with --no-bootstrap)\"\n}\n\nExperimentalBootstrap() {\n  # Arguments: Platform name, bootstrap function name\n  if [ \"$DEBUG\" = 1 ]; then\n    if [ \"$2\" != \"\" ]; then\n      BootstrapMessage $1\n      $2\n    fi\n  else\n    error \"FATAL: $1 support is very experimental at present...\"\n    error \"if you would like to work on improving it, please ensure you have backups\"\n    error \"and then run this script again with the --debug flag!\"\n    error \"Alternatively, you can install OS dependencies yourself and run this script\"\n    error \"again with --no-bootstrap.\"\n    exit 1\n  fi\n}\n\nDeprecationBootstrap() {\n  # Arguments: Platform name, bootstrap function name\n  if [ \"$DEBUG\" = 1 ]; then\n    if [ \"$2\" != \"\" ]; then\n      BootstrapMessage $1\n      $2\n    fi\n  else\n    error \"WARNING: certbot-auto support for this $1 is DEPRECATED!\"\n    error \"Please visit certbot.eff.org to learn how to download a version of\"\n    error \"Certbot that is packaged for your system. While an existing version\"\n    error \"of certbot-auto may work currently, we have stopped supporting updating\"\n    error \"system packages for your system. Please switch to a packaged version\"\n    error \"as soon as possible.\"\n    exit 1\n  fi\n}\n\nMIN_PYTHON_2_VERSION=\"2.7\"\nMIN_PYVER2=$(echo \"$MIN_PYTHON_2_VERSION\" | sed 's/\\.//')\nMIN_PYTHON_3_VERSION=\"3.6\"\nMIN_PYVER3=$(echo \"$MIN_PYTHON_3_VERSION\" | sed 's/\\.//')\n# Sets LE_PYTHON to Python version string and PYVER to the first two\n# digits of the python version.\n# MIN_PYVER and MIN_PYTHON_VERSION are also set by this function, and their\n# values depend on if we try to use Python 3 or Python 2.\nDeterminePythonVersion() {\n  # Arguments: \"NOCRASH\" if we shouldn't crash if we don't find a good python\n  #\n  # If no Python is found, PYVER is set to 0.\n  if [ \"$USE_PYTHON_3\" = 1 ]; then\n    MIN_PYVER=$MIN_PYVER3\n    MIN_PYTHON_VERSION=$MIN_PYTHON_3_VERSION\n    for LE_PYTHON in \"$LE_PYTHON\" python3; do\n      # Break (while keeping the LE_PYTHON value) if found.\n      $EXISTS \"$LE_PYTHON\" > /dev/null && break\n    done\n  else\n    MIN_PYVER=$MIN_PYVER2\n    MIN_PYTHON_VERSION=$MIN_PYTHON_2_VERSION\n    for LE_PYTHON in \"$LE_PYTHON\" python2.7 python27 python2 python; do\n      # Break (while keeping the LE_PYTHON value) if found.\n      $EXISTS \"$LE_PYTHON\" > /dev/null && break\n    done\n  fi\n  if [ \"$?\" != \"0\" ]; then\n    if [ \"$1\" != \"NOCRASH\" ]; then\n      error \"Cannot find any Pythons; please install one!\"\n      exit 1\n    else\n      PYVER=0\n      return 0\n    fi\n  fi\n\n  PYVER=$(\"$LE_PYTHON\" -V 2>&1 | cut -d\" \" -f 2 | cut -d. -f1,2 | sed 's/\\.//')\n  if [ \"$PYVER\" -lt \"$MIN_PYVER\" ]; then\n    if [ \"$1\" != \"NOCRASH\" ]; then\n      error \"You have an ancient version of Python entombed in your operating system...\"\n      error \"This isn't going to work; you'll need at least version $MIN_PYTHON_VERSION.\"\n      exit 1\n    fi\n  fi\n}\n\n# If new packages are installed by BootstrapDebCommon below, this version\n# number must be increased.\nBOOTSTRAP_DEB_COMMON_VERSION=1\n\nBootstrapDebCommon() {\n  # Current version tested with:\n  #\n  # - Ubuntu\n  #     - 14.04 (x64)\n  #     - 15.04 (x64)\n  # - Debian\n  #     - 7.9 \"wheezy\" (x64)\n  #     - sid (2015-10-21) (x64)\n\n  # Past versions tested with:\n  #\n  # - Debian 8.0 \"jessie\" (x64)\n  # - Raspbian 7.8 (armhf)\n\n  # Believed not to work:\n  #\n  # - Debian 6.0.10 \"squeeze\" (x64)\n\n  if [ \"$QUIET\" = 1 ]; then\n    QUIET_FLAG='-qq'\n  fi\n\n  apt-get $QUIET_FLAG update || error apt-get update hit problems but continuing anyway...\n\n  # virtualenv binary can be found in different packages depending on\n  # distro version (#346)\n\n  virtualenv=\n  # virtual env is known to apt and is installable\n  if apt-cache show virtualenv > /dev/null 2>&1 ; then\n    if ! LC_ALL=C apt-cache --quiet=0 show virtualenv 2>&1 | grep -q 'No packages found'; then\n      virtualenv=\"virtualenv\"\n    fi\n  fi\n\n  if apt-cache show python-virtualenv > /dev/null 2>&1; then\n    virtualenv=\"$virtualenv python-virtualenv\"\n  fi\n\n  augeas_pkg=\"libaugeas0 augeas-lenses\"\n\n  if [ \"$ASSUME_YES\" = 1 ]; then\n    YES_FLAG=\"-y\"\n  fi\n\n  apt-get install $QUIET_FLAG $YES_FLAG --no-install-recommends \\\n    python \\\n    python-dev \\\n    $virtualenv \\\n    gcc \\\n    $augeas_pkg \\\n    libssl-dev \\\n    openssl \\\n    libffi-dev \\\n    ca-certificates \\\n\n\n  if ! $EXISTS virtualenv > /dev/null ; then\n    error Failed to install a working \\\"virtualenv\\\" command, exiting\n    exit 1\n  fi\n}\n\n# If new packages are installed by BootstrapRpmCommonBase below, version\n# numbers in rpm_common.sh and rpm_python3.sh must be increased.\n\n# Sets TOOL to the name of the package manager\n# Sets appropriate values for YES_FLAG and QUIET_FLAG based on $ASSUME_YES and $QUIET_FLAG.\n# Note: this function is called both while selecting the bootstrap scripts and\n# during the actual bootstrap. Some things like prompting to user can be done in the latter\n# case, but not in the former one.\nInitializeRPMCommonBase() {\n  if type dnf 2>/dev/null\n  then\n    TOOL=dnf\n  elif type yum 2>/dev/null\n  then\n    TOOL=yum\n\n  else\n    error \"Neither yum nor dnf found. Aborting bootstrap!\"\n    exit 1\n  fi\n\n  if [ \"$ASSUME_YES\" = 1 ]; then\n    YES_FLAG=\"-y\"\n  fi\n  if [ \"$QUIET\" = 1 ]; then\n    QUIET_FLAG='--quiet'\n  fi\n}\n\nBootstrapRpmCommonBase() {\n  # Arguments: whitespace-delimited python packages to install\n\n  InitializeRPMCommonBase # This call is superfluous in practice\n\n  pkgs=\"\n    gcc\n    augeas-libs\n    openssl\n    openssl-devel\n    libffi-devel\n    redhat-rpm-config\n    ca-certificates\n  \"\n\n  # Add the python packages\n  pkgs=\"$pkgs\n    $1\n  \"\n\n  if $TOOL list installed \"httpd\" >/dev/null 2>&1; then\n    pkgs=\"$pkgs\n      mod_ssl\n    \"\n  fi\n\n  if ! $TOOL install $YES_FLAG $QUIET_FLAG $pkgs; then\n    error \"Could not install OS dependencies. Aborting bootstrap!\"\n    exit 1\n  fi\n}\n\n# If new packages are installed by BootstrapRpmCommon below, this version\n# number must be increased.\nBOOTSTRAP_RPM_COMMON_VERSION=1\n\nBootstrapRpmCommon() {\n  # Tested with:\n  #   - Fedora 20, 21, 22, 23 (x64)\n  #   - Centos 7 (x64: on DigitalOcean droplet)\n  #   - CentOS 7 Minimal install in a Hyper-V VM\n  #   - CentOS 6\n\n  InitializeRPMCommonBase\n\n  # Most RPM distros use the \"python\" or \"python-\" naming convention.  Let's try that first.\n  if $TOOL list python >/dev/null 2>&1; then\n    python_pkgs=\"$python\n      python-devel\n      python-virtualenv\n      python-tools\n      python-pip\n    \"\n  # Fedora 26 starts to use the prefix python2 for python2 based packages.\n  # this elseif is theoretically for any Fedora over version 26:\n  elif $TOOL list python2 >/dev/null 2>&1; then\n    python_pkgs=\"$python2\n      python2-libs\n      python2-setuptools\n      python2-devel\n      python2-virtualenv\n      python2-tools\n      python2-pip\n    \"\n  # Some distros and older versions of current distros use a \"python27\"\n  # instead of the \"python\" or \"python-\" naming convention.\n  else\n    python_pkgs=\"$python27\n      python27-devel\n      python27-virtualenv\n      python27-tools\n      python27-pip\n    \"\n  fi\n\n  BootstrapRpmCommonBase \"$python_pkgs\"\n}\n\n# If new packages are installed by BootstrapRpmPython3 below, this version\n# number must be increased.\nBOOTSTRAP_RPM_PYTHON3_LEGACY_VERSION=1\n\n# Checks if rh-python36 can be installed.\nPython36SclIsAvailable() {\n  InitializeRPMCommonBase >/dev/null 2>&1;\n\n  if \"${TOOL}\" list rh-python36 >/dev/null 2>&1; then\n    return 0\n  fi\n  if \"${TOOL}\" list centos-release-scl >/dev/null 2>&1; then\n    return 0\n  fi\n  return 1\n}\n\n# Try to enable rh-python36 from SCL if it is necessary and possible.\nEnablePython36SCL() {\n  if \"$EXISTS\" python3.6 > /dev/null 2> /dev/null; then\n      return 0\n  fi\n  if [ ! -f /opt/rh/rh-python36/enable ]; then\n      return 0\n  fi\n  set +e\n  if ! . /opt/rh/rh-python36/enable; then\n    error 'Unable to enable rh-python36!'\n    exit 1\n  fi\n  set -e\n}\n\n# This bootstrap concerns old RedHat-based distributions that do not ship by default\n# with Python 2.7, but only Python 2.6. We bootstrap them by enabling SCL and installing\n# Python 3.6. Some of these distributions are: CentOS/RHEL/OL/SL 6.\nBootstrapRpmPython3Legacy() {\n  # Tested with:\n  #   - CentOS 6\n\n  InitializeRPMCommonBase\n\n  if ! \"${TOOL}\" list rh-python36 >/dev/null 2>&1; then\n    echo \"To use Certbot on this operating system, packages from the SCL repository need to be installed.\"\n    if ! \"${TOOL}\" list centos-release-scl >/dev/null 2>&1; then\n      error \"Enable the SCL repository and try running Certbot again.\"\n      exit 1\n    fi\n    if [ \"${ASSUME_YES}\" = 1 ]; then\n      /bin/echo -n \"Enabling the SCL repository in 3 seconds... (Press Ctrl-C to cancel)\"\n      sleep 1s\n      /bin/echo -ne \"\\e[0K\\rEnabling the SCL repository in 2 seconds... (Press Ctrl-C to cancel)\"\n      sleep 1s\n      /bin/echo -e \"\\e[0K\\rEnabling the SCL repository in 1 second... (Press Ctrl-C to cancel)\"\n      sleep 1s\n    fi\n    if ! \"${TOOL}\" install \"${YES_FLAG}\" \"${QUIET_FLAG}\" centos-release-scl; then\n      error \"Could not enable SCL. Aborting bootstrap!\"\n      exit 1\n    fi\n  fi\n\n  # CentOS 6 must use rh-python36 from SCL\n  if \"${TOOL}\" list rh-python36 >/dev/null 2>&1; then\n    python_pkgs=\"rh-python36-python\n      rh-python36-python-virtualenv\n      rh-python36-python-devel\n    \"\n  else\n    error \"No supported Python package available to install. Aborting bootstrap!\"\n    exit 1\n  fi\n\n  BootstrapRpmCommonBase \"${python_pkgs}\"\n\n  # Enable SCL rh-python36 after bootstrapping.\n  EnablePython36SCL\n}\n\n# If new packages are installed by BootstrapRpmPython3 below, this version\n# number must be increased.\nBOOTSTRAP_RPM_PYTHON3_VERSION=1\n\nBootstrapRpmPython3() {\n  # Tested with:\n  #   - Fedora 29\n\n  InitializeRPMCommonBase\n\n  # Fedora 29 must use python3-virtualenv\n  if $TOOL list python3-virtualenv >/dev/null 2>&1; then\n    python_pkgs=\"python3\n      python3-virtualenv\n      python3-devel\n    \"\n  else\n    error \"No supported Python package available to install. Aborting bootstrap!\"\n    exit 1\n  fi\n\n  BootstrapRpmCommonBase \"$python_pkgs\"\n}\n\n# If new packages are installed by BootstrapSuseCommon below, this version\n# number must be increased.\nBOOTSTRAP_SUSE_COMMON_VERSION=1\n\nBootstrapSuseCommon() {\n  # SLE12 don't have python-virtualenv\n\n  if [ \"$ASSUME_YES\" = 1 ]; then\n    zypper_flags=\"-nq\"\n    install_flags=\"-l\"\n  fi\n\n  if [ \"$QUIET\" = 1 ]; then\n    QUIET_FLAG='-qq'\n  fi\n\n  if zypper search -x python-virtualenv >/dev/null 2>&1; then\n    OPENSUSE_VIRTUALENV_PACKAGES=\"python-virtualenv\"\n  else\n    # Since Leap 15.0 (and associated Tumbleweed version), python-virtualenv\n    # is a source package, and python2-virtualenv must be used instead.\n    # Also currently python2-setuptools is not a dependency of python2-virtualenv,\n    # while it should be. Installing it explicitly until upstream fix.\n    OPENSUSE_VIRTUALENV_PACKAGES=\"python2-virtualenv python2-setuptools\"\n  fi\n\n  zypper $QUIET_FLAG $zypper_flags in $install_flags \\\n    python \\\n    python-devel \\\n    $OPENSUSE_VIRTUALENV_PACKAGES \\\n    gcc \\\n    augeas-lenses \\\n    libopenssl-devel \\\n    libffi-devel \\\n    ca-certificates\n}\n\n# If new packages are installed by BootstrapArchCommon below, this version\n# number must be increased.\nBOOTSTRAP_ARCH_COMMON_VERSION=1\n\nBootstrapArchCommon() {\n  # Tested with:\n  #   - ArchLinux (x86_64)\n  #\n  # \"python-virtualenv\" is Python3, but \"python2-virtualenv\" provides\n  # only \"virtualenv2\" binary, not \"virtualenv\".\n\n  deps=\"\n    python2\n    python-virtualenv\n    gcc\n    augeas\n    openssl\n    libffi\n    ca-certificates\n    pkg-config\n  \"\n\n  # pacman -T exits with 127 if there are missing dependencies\n  missing=$(pacman -T $deps) || true\n\n  if [ \"$ASSUME_YES\" = 1 ]; then\n    noconfirm=\"--noconfirm\"\n  fi\n\n  if [ \"$missing\" ]; then\n    if [ \"$QUIET\" = 1 ]; then\n      pacman -S --needed $missing $noconfirm > /dev/null\n    else\n      pacman -S --needed $missing $noconfirm\n    fi\n  fi\n}\n\n# If new packages are installed by BootstrapGentooCommon below, this version\n# number must be increased.\nBOOTSTRAP_GENTOO_COMMON_VERSION=1\n\nBootstrapGentooCommon() {\n  PACKAGES=\"\n    dev-lang/python:2.7\n    dev-python/virtualenv\n    app-admin/augeas\n    dev-libs/openssl\n    dev-libs/libffi\n    app-misc/ca-certificates\n    virtual/pkgconfig\"\n\n  ASK_OPTION=\"--ask\"\n  if [ \"$ASSUME_YES\" = 1 ]; then\n    ASK_OPTION=\"\"\n  fi\n\n  case \"$PACKAGE_MANAGER\" in\n    (paludis)\n      cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x\n      ;;\n    (pkgcore)\n      pmerge --noreplace --oneshot $ASK_OPTION $PACKAGES\n      ;;\n    (portage|*)\n      emerge --noreplace --oneshot $ASK_OPTION $PACKAGES\n      ;;\n  esac\n}\n\n# If new packages are installed by BootstrapFreeBsd below, this version number\n# must be increased.\nBOOTSTRAP_FREEBSD_VERSION=1\n\nBootstrapFreeBsd() {\n  if [ \"$QUIET\" = 1 ]; then\n    QUIET_FLAG=\"--quiet\"\n  fi\n\n  pkg install -Ay $QUIET_FLAG \\\n    python \\\n    py27-virtualenv \\\n    augeas \\\n    libffi\n}\n\n# If new packages are installed by BootstrapMac below, this version number must\n# be increased.\nBOOTSTRAP_MAC_VERSION=1\n\nBootstrapMac() {\n  if hash brew 2>/dev/null; then\n    say \"Using Homebrew to install dependencies...\"\n    pkgman=brew\n    pkgcmd=\"brew install\"\n  elif hash port 2>/dev/null; then\n    say \"Using MacPorts to install dependencies...\"\n    pkgman=port\n    pkgcmd=\"port install\"\n  else\n    say \"No Homebrew/MacPorts; installing Homebrew...\"\n    ruby -e \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)\"\n    pkgman=brew\n    pkgcmd=\"brew install\"\n  fi\n\n  $pkgcmd augeas\n  if [ \"$(which python)\" = \"/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python\" \\\n      -o \"$(which python)\" = \"/usr/bin/python\" ]; then\n    # We want to avoid using the system Python because it requires root to use pip.\n    # python.org, MacPorts or HomeBrew Python installations should all be OK.\n    say \"Installing python...\"\n    $pkgcmd python\n  fi\n\n  # Workaround for _dlopen not finding augeas on macOS\n  if [ \"$pkgman\" = \"port\" ] && ! [ -e \"/usr/local/lib/libaugeas.dylib\" ] && [ -e \"/opt/local/lib/libaugeas.dylib\" ]; then\n    say \"Applying augeas workaround\"\n    mkdir -p /usr/local/lib/\n    ln -s /opt/local/lib/libaugeas.dylib /usr/local/lib/\n  fi\n\n  if ! hash pip 2>/dev/null; then\n    say \"pip not installed\"\n    say \"Installing pip...\"\n    curl --silent --show-error --retry 5 https://bootstrap.pypa.io/get-pip.py | python\n  fi\n\n  if ! hash virtualenv 2>/dev/null; then\n    say \"virtualenv not installed.\"\n    say \"Installing with pip...\"\n    pip install virtualenv\n  fi\n}\n\n# If new packages are installed by BootstrapSmartOS below, this version number\n# must be increased.\nBOOTSTRAP_SMARTOS_VERSION=1\n\nBootstrapSmartOS() {\n  pkgin update\n  pkgin -y install 'gcc49' 'py27-augeas' 'py27-virtualenv'\n}\n\n# If new packages are installed by BootstrapMageiaCommon below, this version\n# number must be increased.\nBOOTSTRAP_MAGEIA_COMMON_VERSION=1\n\nBootstrapMageiaCommon() {\n  if [ \"$QUIET\" = 1 ]; then\n    QUIET_FLAG='--quiet'\n  fi\n\n  if ! urpmi --force $QUIET_FLAG \\\n      python \\\n      libpython-devel \\\n      python-virtualenv\n    then\n      error \"Could not install Python dependencies. Aborting bootstrap!\"\n      exit 1\n  fi\n\n  if ! urpmi --force $QUIET_FLAG \\\n      git \\\n      gcc \\\n      python-augeas \\\n      libopenssl-devel \\\n      libffi-devel \\\n      rootcerts\n    then\n      error \"Could not install additional dependencies. Aborting bootstrap!\"\n      exit 1\n    fi\n}\n\n\n# Set Bootstrap to the function that installs OS dependencies on this system\n# and BOOTSTRAP_VERSION to the unique identifier for the current version of\n# that function. If Bootstrap is set to a function that doesn't install any\n# packages BOOTSTRAP_VERSION is not set.\nif [ -f /etc/debian_version ]; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/mageia-release ]; then\n  # Mageia has both /etc/mageia-release and /etc/redhat-release\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/redhat-release ]; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\n  # Run DeterminePythonVersion to decide on the basis of available Python versions\n  # whether to use 2.x or 3.x on RedHat-like systems.\n  # Then, revert LE_PYTHON to its previous state.\n  prev_le_python=\"$LE_PYTHON\"\n  unset LE_PYTHON\n  DeterminePythonVersion \"NOCRASH\"\n\n  RPM_DIST_NAME=`(. /etc/os-release 2> /dev/null && echo $ID) || echo \"unknown\"`\n\n  if [ \"$PYVER\" -eq 26 -a $(uname -m) != 'x86_64' ]; then\n    # 32 bits CentOS 6 and affiliates are not supported anymore by certbot-auto.\n    DEPRECATED_OS=1\n  fi\n\n  # Set RPM_DIST_VERSION to VERSION_ID from /etc/os-release after splitting on\n  # '.' characters (e.g. \"8.0\" becomes \"8\"). If the command exits with an\n  # error, RPM_DIST_VERSION is set to \"unknown\".\n  RPM_DIST_VERSION=$( (. /etc/os-release 2> /dev/null && echo \"$VERSION_ID\") | cut -d '.' -f1 || echo \"unknown\")\n\n  # If RPM_DIST_VERSION is an empty string or it contains any nonnumeric\n  # characters, the value is unexpected so we set RPM_DIST_VERSION to 0.\n  if [ -z \"$RPM_DIST_VERSION\" ] || [ -n \"$(echo \"$RPM_DIST_VERSION\" | tr -d '[0-9]')\" ]; then\n     RPM_DIST_VERSION=0\n  fi\n\n  # Handle legacy RPM distributions\n  if [ \"$PYVER\" -eq 26 ]; then\n    # Check if an automated bootstrap can be achieved on this system.\n    if ! Python36SclIsAvailable; then\n      INTERACTIVE_BOOTSTRAP=1\n    fi\n\n    USE_PYTHON_3=1\n\n    # Try now to enable SCL rh-python36 for systems already bootstrapped\n    # NB: EnablePython36SCL has been defined along with BootstrapRpmPython3Legacy in certbot-auto\n    EnablePython36SCL\n  else\n    # Starting to Fedora 29, python2 is on a deprecation path. Let's move to python3 then.\n    # RHEL 8 also uses python3 by default.\n    if [ \"$RPM_DIST_NAME\" = \"fedora\" -a \"$RPM_DIST_VERSION\" -ge 29 ]; then\n      RPM_USE_PYTHON_3=1\n    elif [ \"$RPM_DIST_NAME\" = \"rhel\" -a \"$RPM_DIST_VERSION\" -ge 8 ]; then\n      RPM_USE_PYTHON_3=1\n    elif [ \"$RPM_DIST_NAME\" = \"centos\" -a \"$RPM_DIST_VERSION\" -ge 8 ]; then\n      RPM_USE_PYTHON_3=1\n    else\n      RPM_USE_PYTHON_3=0\n    fi\n\n    if [ \"$RPM_USE_PYTHON_3\" = 1 ]; then\n      USE_PYTHON_3=1\n    fi\n  fi\n\n  LE_PYTHON=\"$prev_le_python\"\nelif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/arch-release ]; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/manjaro-release ]; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/gentoo-release ]; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif uname | grep -iq FreeBSD ; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif uname | grep -iq Darwin ; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/issue ] && grep -iq \"Amazon Linux\" /etc/issue ; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelif [ -f /etc/product ] && grep -q \"Joyent Instance\" /etc/product ; then\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nelse\n  DEPRECATED_OS=1\n  NO_SELF_UPGRADE=1\nfi\n\n# We handle this case after determining the normal bootstrap version to allow\n# variables like USE_PYTHON_3 to be properly set. As described above, if the\n# Bootstrap function doesn't install any packages, BOOTSTRAP_VERSION should not\n# be set so we unset it here.\nif [ \"$NO_BOOTSTRAP\" = 1 ]; then\n  Bootstrap() {\n    :\n  }\n  unset BOOTSTRAP_VERSION\nfi\n\nif [ \"$DEPRECATED_OS\" = 1 ]; then\n  Bootstrap() {\n    error \"Skipping bootstrap because certbot-auto is deprecated on this system.\"\n  }\n  unset BOOTSTRAP_VERSION\nfi\n\n# Sets PREV_BOOTSTRAP_VERSION to the identifier for the bootstrap script used\n# to install OS dependencies on this system. PREV_BOOTSTRAP_VERSION isn't set\n# if it is unknown how OS dependencies were installed on this system.\nSetPrevBootstrapVersion() {\n  if [ -f $BOOTSTRAP_VERSION_PATH ]; then\n    PREV_BOOTSTRAP_VERSION=$(cat \"$BOOTSTRAP_VERSION_PATH\")\n  # The list below only contains bootstrap version strings that existed before\n  # we started writing them to disk.\n  #\n  # DO NOT MODIFY THIS LIST UNLESS YOU KNOW WHAT YOU'RE DOING!\n  elif grep -Fqx \"$BOOTSTRAP_VERSION\" << \"UNLIKELY_EOF\"\nBootstrapDebCommon 1\nBootstrapMageiaCommon 1\nBootstrapRpmCommon 1\nBootstrapSuseCommon 1\nBootstrapArchCommon 1\nBootstrapGentooCommon 1\nBootstrapFreeBsd 1\nBootstrapMac 1\nBootstrapSmartOS 1\nUNLIKELY_EOF\n  then\n    # If there's no bootstrap version saved to disk, but the currently selected\n    # bootstrap script is from before we started saving the version number,\n    # return the currently selected version to prevent us from rebootstrapping\n    # unnecessarily.\n    PREV_BOOTSTRAP_VERSION=\"$BOOTSTRAP_VERSION\"\n  fi\n}\n\nTempDir() {\n  mktemp -d 2>/dev/null || mktemp -d -t 'le'  # Linux || macOS\n}\n\n# Returns 0 if a letsencrypt installation exists at $OLD_VENV_PATH, otherwise,\n# returns a non-zero number.\nOldVenvExists() {\n    [ -n \"$OLD_VENV_PATH\" -a -f \"$OLD_VENV_PATH/bin/letsencrypt\" ]\n}\n\n# Given python path, version 1 and version 2, check if version 1 is outdated compared to version 2.\n# An unofficial version provided as version 1 (eg. 0.28.0.dev0) will be treated\n# specifically by printing \"UNOFFICIAL\". Otherwise, print \"OUTDATED\" if version 1\n# is outdated, and \"UP_TO_DATE\" if not.\n# This function relies only on installed python environment (2.x or 3.x) by certbot-auto.\nCompareVersions() {\n    \"$1\" - \"$2\" \"$3\" << \"UNLIKELY_EOF\"\nimport sys\nfrom distutils.version import StrictVersion\n\ntry:\n    current = StrictVersion(sys.argv[1])\nexcept ValueError:\n    sys.stdout.write('UNOFFICIAL')\n    sys.exit()\n\ntry:\n    remote = StrictVersion(sys.argv[2])\nexcept ValueError:\n    sys.stdout.write('UP_TO_DATE')\n    sys.exit()\n\nif current < remote:\n    sys.stdout.write('OUTDATED')\nelse:\n    sys.stdout.write('UP_TO_DATE')\nUNLIKELY_EOF\n}\n\n# Create a new virtual environment for Certbot. It will overwrite any existing one.\n# Parameters: LE_PYTHON, VENV_PATH, PYVER, VERBOSE\nCreateVenv() {\n    \"$1\" - \"$2\" \"$3\" \"$4\" << \"UNLIKELY_EOF\"\n#!/usr/bin/env python\nimport os\nimport shutil\nimport subprocess\nimport sys\n\n\ndef create_venv(venv_path, pyver, verbose):\n    if os.path.exists(venv_path):\n        shutil.rmtree(venv_path)\n\n    stdout = sys.stdout if verbose == '1' else open(os.devnull, 'w')\n\n    if int(pyver) <= 27:\n        # Use virtualenv binary\n        environ = os.environ.copy()\n        environ['VIRTUALENV_NO_DOWNLOAD'] = '1'\n        command = ['virtualenv', '--no-site-packages', '--python', sys.executable, venv_path]\n        subprocess.check_call(command, stdout=stdout, env=environ)\n    else:\n        # Use embedded venv module in Python 3\n        command = [sys.executable, '-m', 'venv', venv_path]\n        subprocess.check_call(command, stdout=stdout)\n\n\nif __name__ == '__main__':\n    create_venv(*sys.argv[1:])\n\nUNLIKELY_EOF\n}\n\n# Check that the given PATH_TO_CHECK has secured permissions.\n# Parameters: LE_PYTHON, PATH_TO_CHECK\nCheckPathPermissions() {\n    \"$1\" - \"$2\" << \"UNLIKELY_EOF\"\n\"\"\"Verifies certbot-auto cannot be modified by unprivileged users.\n\nThis script takes the path to certbot-auto as its only command line\nargument.  It then checks that the file can only be modified by uid/gid\n< 1000 and if other users can modify the file, it prints a warning with\na suggestion on how to solve the problem.\n\nPermissions on symlinks in the absolute path of certbot-auto are ignored\nand only the canonical path to certbot-auto is checked. There could be\npermissions problems due to the symlinks that are unreported by this\nscript, however, issues like this were not caused by our documentation\nand are ignored for the sake of simplicity.\n\nAll warnings are printed to stdout rather than stderr so all stderr\noutput from this script can be suppressed to avoid printing messages if\nthis script fails for some reason.\n\n\"\"\"\nfrom __future__ import print_function\n\nimport os\nimport stat\nimport sys\n\n\nFORUM_POST_URL = 'https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979/'\n\n\ndef has_safe_permissions(path):\n    \"\"\"Returns True if the given path has secure permissions.\n\n    The permissions are considered safe if the file is only writable by\n    uid/gid < 1000.\n\n    The reason we allow more IDs than 0 is because on some systems such\n    as Debian, system users/groups other than uid/gid 0 are used for the\n    path we recommend in our instructions which is /usr/local/bin.  1000\n    was chosen because on Debian 0-999 is reserved for system IDs[1] and\n    on RHEL either 0-499 or 0-999 is reserved depending on the\n    version[2][3]. Due to these differences across different OSes, this\n    detection isn't perfect so we only determine permissions are\n    insecure when we can be reasonably confident there is a problem\n    regardless of the underlying OS.\n\n    [1] https://www.debian.org/doc/debian-policy/ch-opersys.html#uid-and-gid-classes\n    [2] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/6/html/deployment_guide/ch-managing_users_and_groups\n    [3] https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/ch-managing_users_and_groups\n\n    :param str path: filesystem path to check\n    :returns: True if the path has secure permissions, otherwise, False\n    :rtype: bool\n\n    \"\"\"\n    # os.stat follows symlinks before obtaining information about a file.\n    stat_result = os.stat(path)\n    if stat_result.st_mode & stat.S_IWOTH:\n        return False\n    if stat_result.st_mode & stat.S_IWGRP and stat_result.st_gid >= 1000:\n        return False\n    if stat_result.st_mode & stat.S_IWUSR and stat_result.st_uid >= 1000:\n        return False\n    return True\n\n\ndef main(certbot_auto_path):\n    current_path = os.path.realpath(certbot_auto_path)\n    last_path = None\n    permissions_ok = True\n    # This loop makes use of the fact that os.path.dirname('/') == '/'.\n    while current_path != last_path and permissions_ok:\n        permissions_ok = has_safe_permissions(current_path)\n        last_path = current_path\n        current_path = os.path.dirname(current_path)\n\n    if not permissions_ok:\n        print('{0} has insecure permissions!'.format(certbot_auto_path))\n        print('To learn how to fix them, visit {0}'.format(FORUM_POST_URL))\n\n\nif __name__ == '__main__':\n    main(sys.argv[1])\n\nUNLIKELY_EOF\n}\n\nif [ \"$1\" = \"--le-auto-phase2\" ]; then\n  # Phase 2: Create venv, install LE, and run.\n\n  shift 1  # the --le-auto-phase2 arg\n\n  if [ \"$DEPRECATED_OS\" = 1 ]; then\n    # Phase 2 damage control mode for deprecated OSes.\n    # In this situation, we bypass any bootstrap or certbot venv setup.\n    error \"Your system is not supported by certbot-auto anymore.\"\n\n    if [ ! -d \"$VENV_PATH\" ] && OldVenvExists; then\n      VENV_BIN=\"$OLD_VENV_PATH/bin\"\n    fi\n\n    if [ -f \"$VENV_BIN/letsencrypt\" -a \"$INSTALL_ONLY\" != 1 ]; then\n      error \"certbot-auto and its Certbot installation will no longer receive updates.\"\n      error \"You will not receive any bug fixes including those fixing server compatibility\"\n      error \"or security problems.\"\n      error \"Please visit https://certbot.eff.org/ to check for other alternatives.\"\n      \"$VENV_BIN/letsencrypt\" \"$@\"\n      exit 0\n    else\n      error \"Certbot cannot be installed.\"\n      error \"Please visit https://certbot.eff.org/ to check for other alternatives.\"\n      exit 1\n    fi\n  fi\n\n  SetPrevBootstrapVersion\n\n  if [ -z \"$PHASE_1_VERSION\" -a \"$USE_PYTHON_3\" = 1 ]; then\n    unset LE_PYTHON\n  fi\n\n  INSTALLED_VERSION=\"none\"\n  if [ -d \"$VENV_PATH\" ] || OldVenvExists; then\n    # If the selected Bootstrap function isn't a noop and it differs from the\n    # previously used version\n    if [ -n \"$BOOTSTRAP_VERSION\" -a \"$BOOTSTRAP_VERSION\" != \"$PREV_BOOTSTRAP_VERSION\" ]; then\n      # Check if we can rebootstrap without manual user intervention: this requires that\n      # certbot-auto is in non-interactive mode AND selected bootstrap does not claim to\n      # require a manual user intervention.\n      if [ \"$NONINTERACTIVE\" = 1 -a \"$INTERACTIVE_BOOTSTRAP\" != 1 ]; then\n        CAN_REBOOTSTRAP=1\n      fi\n      # Check if rebootstrap can be done non-interactively and current shell is non-interactive\n      # (true if stdin and stdout are not attached to a terminal).\n      if [ \\( \"$CAN_REBOOTSTRAP\" = 1 \\) -o \\( \\( -t 0 \\) -a \\( -t 1 \\) \\) ]; then\n        if [ -d \"$VENV_PATH\" ]; then\n          rm -rf \"$VENV_PATH\"\n        fi\n        # In the case the old venv was just a symlink to the new one,\n        # OldVenvExists is now false because we deleted the venv at VENV_PATH.\n        if OldVenvExists; then\n          rm -rf \"$OLD_VENV_PATH\"\n          ln -s \"$VENV_PATH\" \"$OLD_VENV_PATH\"\n        fi\n        RerunWithArgs \"$@\"\n      # Otherwise bootstrap needs to be done manually by the user.\n      else\n        # If it is because bootstrapping is interactive, --non-interactive will be of no use.\n        if [ \"$INTERACTIVE_BOOTSTRAP\" = 1 ]; then\n          error \"Skipping upgrade because new OS dependencies may need to be installed.\"\n          error \"This requires manual user intervention: please run this script again manually.\"\n        # If this is because of the environment (eg. non interactive shell without\n        # --non-interactive flag set), help the user in that direction.\n        else\n          error \"Skipping upgrade because new OS dependencies may need to be installed.\"\n          error\n          error \"To upgrade to a newer version, please run this script again manually so you can\"\n          error \"approve changes or with --non-interactive on the command line to automatically\"\n          error \"install any required packages.\"\n        fi\n        # Set INSTALLED_VERSION to be the same so we don't update the venv\n        INSTALLED_VERSION=\"$LE_AUTO_VERSION\"\n        # Continue to use OLD_VENV_PATH if the new venv doesn't exist\n        if [ ! -d \"$VENV_PATH\" ]; then\n          VENV_BIN=\"$OLD_VENV_PATH/bin\"\n        fi\n      fi\n    elif [ -f \"$VENV_BIN/letsencrypt\" ]; then\n      # --version output ran through grep due to python-cryptography DeprecationWarnings\n      # grep for both certbot and letsencrypt until certbot and shim packages have been released\n      INSTALLED_VERSION=$(\"$VENV_BIN/letsencrypt\" --version 2>&1 | grep \"^certbot\\|^letsencrypt\" | cut -d \" \" -f 2)\n      if [ -z \"$INSTALLED_VERSION\" ]; then\n          error \"Error: couldn't get currently installed version for $VENV_BIN/letsencrypt: \" 1>&2\n          \"$VENV_BIN/letsencrypt\" --version\n          exit 1\n      fi\n    fi\n  fi\n\n  if [ \"$LE_AUTO_VERSION\" != \"$INSTALLED_VERSION\" ]; then\n    say \"Creating virtual environment...\"\n    DeterminePythonVersion\n    CreateVenv \"$LE_PYTHON\" \"$VENV_PATH\" \"$PYVER\" \"$VERBOSE\"\n\n    if [ -n \"$BOOTSTRAP_VERSION\" ]; then\n      echo \"$BOOTSTRAP_VERSION\" > \"$BOOTSTRAP_VERSION_PATH\"\n    elif [ -n \"$PREV_BOOTSTRAP_VERSION\" ]; then\n      echo \"$PREV_BOOTSTRAP_VERSION\" > \"$BOOTSTRAP_VERSION_PATH\"\n    fi\n\n    say \"Installing Python packages...\"\n    TEMP_DIR=$(TempDir)\n    trap 'rm -rf \"$TEMP_DIR\"' EXIT\n    # There is no $ interpolation due to quotes on starting heredoc delimiter.\n    # -------------------------------------------------------------------------\n    cat << \"UNLIKELY_EOF\" > \"$TEMP_DIR/letsencrypt-auto-requirements.txt\"\n# This is the flattened list of packages certbot-auto installs.\n# To generate this, do (with docker and package hashin installed):\n# ```\n# letsencrypt-auto-source/rebuild_dependencies.py \\\n#   letsencrypt-auto-source/pieces/dependency-requirements.txt\n# ```\n# If you want to update a single dependency, run commands similar to these:\n# ```\n# pip install hashin\n# hashin -r dependency-requirements.txt cryptography==1.5.2\n# ```\nConfigArgParse==1.2.3 \\\n    --hash=sha256:edd17be986d5c1ba2e307150b8e5f5107aba125f3574dddd02c85d5cdcfd37dc\ncertifi==2020.4.5.1 \\\n    --hash=sha256:1d987a998c75633c40847cc966fcf5904906c920a7f17ef374f5aa4282abd304 \\\n    --hash=sha256:51fcb31174be6e6664c5f69e3e1691a2d72a1a12e90f872cbdb1567eb47b6519\ncffi==1.14.0 \\\n    --hash=sha256:001bf3242a1bb04d985d63e138230802c6c8d4db3668fb545fb5005ddf5bb5ff \\\n    --hash=sha256:00789914be39dffba161cfc5be31b55775de5ba2235fe49aa28c148236c4e06b \\\n    --hash=sha256:028a579fc9aed3af38f4892bdcc7390508adabc30c6af4a6e4f611b0c680e6ac \\\n    --hash=sha256:14491a910663bf9f13ddf2bc8f60562d6bc5315c1f09c704937ef17293fb85b0 \\\n    --hash=sha256:1cae98a7054b5c9391eb3249b86e0e99ab1e02bb0cc0575da191aedadbdf4384 \\\n    --hash=sha256:2089ed025da3919d2e75a4d963d008330c96751127dd6f73c8dc0c65041b4c26 \\\n    --hash=sha256:2d384f4a127a15ba701207f7639d94106693b6cd64173d6c8988e2c25f3ac2b6 \\\n    --hash=sha256:337d448e5a725bba2d8293c48d9353fc68d0e9e4088d62a9571def317797522b \\\n    --hash=sha256:399aed636c7d3749bbed55bc907c3288cb43c65c4389964ad5ff849b6370603e \\\n    --hash=sha256:3b911c2dbd4f423b4c4fcca138cadde747abdb20d196c4a48708b8a2d32b16dd \\\n    --hash=sha256:3d311bcc4a41408cf5854f06ef2c5cab88f9fded37a3b95936c9879c1640d4c2 \\\n    --hash=sha256:62ae9af2d069ea2698bf536dcfe1e4eed9090211dbaafeeedf5cb6c41b352f66 \\\n    --hash=sha256:66e41db66b47d0d8672d8ed2708ba91b2f2524ece3dee48b5dfb36be8c2f21dc \\\n    --hash=sha256:675686925a9fb403edba0114db74e741d8181683dcf216be697d208857e04ca8 \\\n    --hash=sha256:7e63cbcf2429a8dbfe48dcc2322d5f2220b77b2e17b7ba023d6166d84655da55 \\\n    --hash=sha256:8a6c688fefb4e1cd56feb6c511984a6c4f7ec7d2a1ff31a10254f3c817054ae4 \\\n    --hash=sha256:8c0ffc886aea5df6a1762d0019e9cb05f825d0eec1f520c51be9d198701daee5 \\\n    --hash=sha256:95cd16d3dee553f882540c1ffe331d085c9e629499ceadfbda4d4fde635f4b7d \\\n    --hash=sha256:99f748a7e71ff382613b4e1acc0ac83bf7ad167fb3802e35e90d9763daba4d78 \\\n    --hash=sha256:b8c78301cefcf5fd914aad35d3c04c2b21ce8629b5e4f4e45ae6812e461910fa \\\n    --hash=sha256:c420917b188a5582a56d8b93bdd8e0f6eca08c84ff623a4c16e809152cd35793 \\\n    --hash=sha256:c43866529f2f06fe0edc6246eb4faa34f03fe88b64a0a9a942561c8e22f4b71f \\\n    --hash=sha256:cab50b8c2250b46fe738c77dbd25ce017d5e6fb35d3407606e7a4180656a5a6a \\\n    --hash=sha256:cef128cb4d5e0b3493f058f10ce32365972c554572ff821e175dbc6f8ff6924f \\\n    --hash=sha256:cf16e3cf6c0a5fdd9bc10c21687e19d29ad1fe863372b5543deaec1039581a30 \\\n    --hash=sha256:e56c744aa6ff427a607763346e4170629caf7e48ead6921745986db3692f987f \\\n    --hash=sha256:e577934fc5f8779c554639376beeaa5657d54349096ef24abe8c74c5d9c117c3 \\\n    --hash=sha256:f2b0fa0c01d8a0c7483afd9f31d7ecf2d71760ca24499c8697aeb5ca37dc090c\nchardet==3.0.4 \\\n    --hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \\\n    --hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691\nconfigobj==5.0.6 \\\n    --hash=sha256:a2f5650770e1c87fb335af19a9b7eb73fc05ccf22144eb68db7d00cd2bcb0902\ncryptography==2.8 \\\n    --hash=sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c \\\n    --hash=sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595 \\\n    --hash=sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad \\\n    --hash=sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651 \\\n    --hash=sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2 \\\n    --hash=sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff \\\n    --hash=sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d \\\n    --hash=sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42 \\\n    --hash=sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d \\\n    --hash=sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e \\\n    --hash=sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912 \\\n    --hash=sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793 \\\n    --hash=sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13 \\\n    --hash=sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7 \\\n    --hash=sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0 \\\n    --hash=sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879 \\\n    --hash=sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f \\\n    --hash=sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9 \\\n    --hash=sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2 \\\n    --hash=sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf \\\n    --hash=sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8\ndistro==1.5.0 \\\n    --hash=sha256:0e58756ae38fbd8fc3020d54badb8eae17c5b9dcbed388b17bb55b8a5928df92 \\\n    --hash=sha256:df74eed763e18d10d0da624258524ae80486432cd17392d9c3d96f5e83cd2799\nenum34==1.1.10; python_version < '3.4' \\\n    --hash=sha256:a98a201d6de3f2ab3db284e70a33b0f896fbf35f8086594e8c9e74b909058d53 \\\n    --hash=sha256:c3858660960c984d6ab0ebad691265180da2b43f07e061c0f8dca9ef3cffd328 \\\n    --hash=sha256:cce6a7477ed816bd2542d03d53db9f0db935dd013b70f336a95c73979289f248\nfuncsigs==1.0.2 \\\n    --hash=sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca \\\n    --hash=sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50\nidna==2.9 \\\n    --hash=sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb \\\n    --hash=sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa\nipaddress==1.0.23 \\\n    --hash=sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc \\\n    --hash=sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2\njosepy==1.3.0 \\\n    --hash=sha256:c341ffa403399b18e9eae9012f804843045764d1390f9cb4648980a7569b1619 \\\n    --hash=sha256:e54882c64be12a2a76533f73d33cba9e331950fda9e2731e843490b774e7a01c\nmock==1.3.0 \\\n    --hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6 \\\n    --hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb\nparsedatetime==2.5 \\\n    --hash=sha256:3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1 \\\n    --hash=sha256:d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667\npbr==5.4.5 \\\n    --hash=sha256:07f558fece33b05caf857474a366dfcc00562bca13dd8b47b2b3e22d9f9bf55c \\\n    --hash=sha256:579170e23f8e0c2f24b0de612f71f648eccb79fb1322c814ae6b3c07b5ba23e8\npyOpenSSL==19.1.0 \\\n    --hash=sha256:621880965a720b8ece2f1b2f54ea2071966ab00e2970ad2ce11d596102063504 \\\n    --hash=sha256:9a24494b2602aaf402be5c9e30a0b82d4a5c67528fe8fb475e3f3bc00dd69507\npyRFC3339==1.1 \\\n    --hash=sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4 \\\n    --hash=sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a\npycparser==2.20 \\\n    --hash=sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0 \\\n    --hash=sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705\npyparsing==2.4.7 \\\n    --hash=sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1 \\\n    --hash=sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b\npython-augeas==0.5.0 \\\n    --hash=sha256:67d59d66cdba8d624e0389b87b2a83a176f21f16a87553b50f5703b23f29bac2\npytz==2020.1 \\\n    --hash=sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed \\\n    --hash=sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048\nrequests==2.23.0 \\\n    --hash=sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee \\\n    --hash=sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6\nrequests-toolbelt==0.9.1 \\\n    --hash=sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f \\\n    --hash=sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0\nsix==1.15.0 \\\n    --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \\\n    --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced\nurllib3==1.25.9 \\\n    --hash=sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527 \\\n    --hash=sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115\nzope.component==4.6.1 \\\n    --hash=sha256:bfbe55d4a93e70a78b10edc3aad4de31bb8860919b7cbd8d66f717f7d7b279ac \\\n    --hash=sha256:d9c7c27673d787faff8a83797ce34d6ebcae26a370e25bddb465ac2182766aca\nzope.deferredimport==4.3.1 \\\n    --hash=sha256:57b2345e7b5eef47efcd4f634ff16c93e4265de3dcf325afc7315ade48d909e1 \\\n    --hash=sha256:9a0c211df44aa95f1c4e6d2626f90b400f56989180d3ef96032d708da3d23e0a\nzope.deprecation==4.4.0 \\\n    --hash=sha256:0d453338f04bacf91bbfba545d8bcdf529aa829e67b705eac8c1a7fdce66e2df \\\n    --hash=sha256:f1480b74995958b24ce37b0ef04d3663d2683e5d6debc96726eff18acf4ea113\nzope.event==4.4 \\\n    --hash=sha256:69c27debad9bdacd9ce9b735dad382142281ac770c4a432b533d6d65c4614bcf \\\n    --hash=sha256:d8e97d165fd5a0997b45f5303ae11ea3338becfe68c401dd88ffd2113fe5cae7\nzope.hookable==5.0.1 \\\n    --hash=sha256:0194b9b9e7f614abba60c90b231908861036578297515d3d6508eb10190f266d \\\n    --hash=sha256:0c2977473918bdefc6fa8dfb311f154e7f13c6133957fe649704deca79b92093 \\\n    --hash=sha256:17b8bdb3b77e03a152ca0d5ca185a7ae0156f5e5a2dbddf538676633a1f7380f \\\n    --hash=sha256:29d07681a78042cdd15b268ae9decffed9ace68a53eebeb61d65ae931d158841 \\\n    --hash=sha256:36fb1b35d1150267cb0543a1ddd950c0bc2c75ed0e6e92e3aaa6ac2e29416cb7 \\\n    --hash=sha256:3aed60c2bb5e812bbf9295c70f25b17ac37c233f30447a96c67913ba5073642f \\\n    --hash=sha256:3cac1565cc768911e72ca9ec4ddf5c5109e1fef0104f19f06649cf1874943b60 \\\n    --hash=sha256:3d4bc0cc4a37c3cd3081063142eeb2125511db3c13f6dc932d899c512690378e \\\n    --hash=sha256:3f73096f27b8c28be53ffb6604f7b570fbbb82f273c6febe5f58119009b59898 \\\n    --hash=sha256:522d1153d93f2d48aa0bd9fb778d8d4500be2e4dcf86c3150768f0e3adbbc4ef \\\n    --hash=sha256:523d2928fb7377bbdbc9af9c0b14ad73e6eaf226349f105733bdae27efd15b5a \\\n    --hash=sha256:5848309d4fc5c02150a45e8f8d2227e5bfda386a508bbd3160fed7c633c5a2fa \\\n    --hash=sha256:6781f86e6d54a110980a76e761eb54590630fd2af2a17d7edf02a079d2646c1d \\\n    --hash=sha256:6fd27921ebf3aaa945fa25d790f1f2046204f24dba4946f82f5f0a442577c3e9 \\\n    --hash=sha256:70d581862863f6bf9e175e85c9d70c2d7155f53fb04dcdb2f73cf288ca559a53 \\\n    --hash=sha256:81867c23b0dc66c8366f351d00923f2bc5902820a24c2534dfd7bf01a5879963 \\\n    --hash=sha256:81db29edadcbb740cd2716c95a297893a546ed89db1bfe9110168732d7f0afdd \\\n    --hash=sha256:86bd12624068cea60860a0759af5e2c3adc89c12aef6f71cf12f577e28deefe3 \\\n    --hash=sha256:9c184d8f9f7a76e1ced99855ccf390ffdd0ec3765e5cbf7b9cada600accc0a1e \\\n    --hash=sha256:acc789e8c29c13555e43fe4bf9fcd15a65512c9645e97bbaa5602e3201252b02 \\\n    --hash=sha256:afaa740206b7660d4cc3b8f120426c85761f51379af7a5b05451f624ad12b0af \\\n    --hash=sha256:b5f5fa323f878bb16eae68ea1ba7f6c0419d4695d0248bed4b18f51d7ce5ab85 \\\n    --hash=sha256:bd89e0e2c67bf4ac3aca2a19702b1a37269fb1923827f68324ac2e7afd6e3406 \\\n    --hash=sha256:c212de743283ec0735db24ec6ad913758df3af1b7217550ff270038062afd6ae \\\n    --hash=sha256:ca553f524293a0bdea05e7f44c3e685e4b7b022cb37d87bc4a3efa0f86587a8d \\\n    --hash=sha256:cab67065a3db92f636128d3157cc5424a145f82d96fb47159c539132833a6d36 \\\n    --hash=sha256:d3b3b3eedfdbf6b02898216e85aa6baf50207f4378a2a6803d6d47650cd37031 \\\n    --hash=sha256:d9f4a5a72f40256b686d31c5c0b1fde503172307beb12c1568296e76118e402c \\\n    --hash=sha256:df5067d87aaa111ed5d050e1ee853ba284969497f91806efd42425f5348f1c06 \\\n    --hash=sha256:e2587644812c6138f05b8a41594a8337c6790e3baf9a01915e52438c13fc6bef \\\n    --hash=sha256:e27fd877662db94f897f3fd532ef211ca4901eb1a70ba456f15c0866a985464a \\\n    --hash=sha256:e427ebbdd223c72e06ba94c004bb04e996c84dec8a0fa84e837556ae145c439e \\\n    --hash=sha256:e583ad4309c203ef75a09d43434cf9c2b4fa247997ecb0dcad769982c39411c7 \\\n    --hash=sha256:e760b2bc8ece9200804f0c2b64d10147ecaf18455a2a90827fbec4c9d84f3ad5 \\\n    --hash=sha256:ea9a9cc8bcc70e18023f30fa2f53d11ae069572a162791224e60cd65df55fb69 \\\n    --hash=sha256:ecb3f17dce4803c1099bd21742cd126b59817a4e76a6544d31d2cca6e30dbffd \\\n    --hash=sha256:ed794e3b3de42486d30444fb60b5561e724ee8a2d1b17b0c2e0f81e3ddaf7a87 \\\n    --hash=sha256:ee885d347279e38226d0a437b6a932f207f691c502ee565aba27a7022f1285df \\\n    --hash=sha256:fd5e7bc5f24f7e3d490698f7b854659a9851da2187414617cd5ed360af7efd63 \\\n    --hash=sha256:fe45f6870f7588ac7b2763ff1ce98cce59369717afe70cc353ec5218bc854bcc\nzope.interface==5.1.0 \\\n    --hash=sha256:0103cba5ed09f27d2e3de7e48bb320338592e2fabc5ce1432cf33808eb2dfd8b \\\n    --hash=sha256:14415d6979356629f1c386c8c4249b4d0082f2ea7f75871ebad2e29584bd16c5 \\\n    --hash=sha256:1ae4693ccee94c6e0c88a4568fb3b34af8871c60f5ba30cf9f94977ed0e53ddd \\\n    --hash=sha256:1b87ed2dc05cb835138f6a6e3595593fea3564d712cb2eb2de963a41fd35758c \\\n    --hash=sha256:269b27f60bcf45438e8683269f8ecd1235fa13e5411de93dae3b9ee4fe7f7bc7 \\\n    --hash=sha256:27d287e61639d692563d9dab76bafe071fbeb26818dd6a32a0022f3f7ca884b5 \\\n    --hash=sha256:39106649c3082972106f930766ae23d1464a73b7d30b3698c986f74bf1256a34 \\\n    --hash=sha256:40e4c42bd27ed3c11b2c983fecfb03356fae1209de10686d03c02c8696a1d90e \\\n    --hash=sha256:461d4339b3b8f3335d7e2c90ce335eb275488c587b61aca4b305196dde2ff086 \\\n    --hash=sha256:4f98f70328bc788c86a6a1a8a14b0ea979f81ae6015dd6c72978f1feff70ecda \\\n    --hash=sha256:558a20a0845d1a5dc6ff87cd0f63d7dac982d7c3be05d2ffb6322a87c17fa286 \\\n    --hash=sha256:562dccd37acec149458c1791da459f130c6cf8902c94c93b8d47c6337b9fb826 \\\n    --hash=sha256:5e86c66a6dea8ab6152e83b0facc856dc4d435fe0f872f01d66ce0a2131b7f1d \\\n    --hash=sha256:60a207efcd8c11d6bbeb7862e33418fba4e4ad79846d88d160d7231fcb42a5ee \\\n    --hash=sha256:645a7092b77fdbc3f68d3cc98f9d3e71510e419f54019d6e282328c0dd140dcd \\\n    --hash=sha256:6874367586c020705a44eecdad5d6b587c64b892e34305bb6ed87c9bbe22a5e9 \\\n    --hash=sha256:74bf0a4f9091131de09286f9a605db449840e313753949fe07c8d0fe7659ad1e \\\n    --hash=sha256:7b726194f938791a6691c7592c8b9e805fc6d1b9632a833b9c0640828cd49cbc \\\n    --hash=sha256:8149ded7f90154fdc1a40e0c8975df58041a6f693b8f7edcd9348484e9dc17fe \\\n    --hash=sha256:8cccf7057c7d19064a9e27660f5aec4e5c4001ffcf653a47531bde19b5aa2a8a \\\n    --hash=sha256:911714b08b63d155f9c948da2b5534b223a1a4fc50bb67139ab68b277c938578 \\\n    --hash=sha256:a5f8f85986197d1dd6444763c4a15c991bfed86d835a1f6f7d476f7198d5f56a \\\n    --hash=sha256:a744132d0abaa854d1aad50ba9bc64e79c6f835b3e92521db4235a1991176813 \\\n    --hash=sha256:af2c14efc0bb0e91af63d00080ccc067866fb8cbbaca2b0438ab4105f5e0f08d \\\n    --hash=sha256:b054eb0a8aa712c8e9030065a59b5e6a5cf0746ecdb5f087cca5ec7685690c19 \\\n    --hash=sha256:b0becb75418f8a130e9d465e718316cd17c7a8acce6fe8fe07adc72762bee425 \\\n    --hash=sha256:b1d2ed1cbda2ae107283befd9284e650d840f8f7568cb9060b5466d25dc48975 \\\n    --hash=sha256:ba4261c8ad00b49d48bbb3b5af388bb7576edfc0ca50a49c11dcb77caa1d897e \\\n    --hash=sha256:d1fe9d7d09bb07228650903d6a9dc48ea649e3b8c69b1d263419cc722b3938e8 \\\n    --hash=sha256:d7804f6a71fc2dda888ef2de266727ec2f3915373d5a785ed4ddc603bbc91e08 \\\n    --hash=sha256:da2844fba024dd58eaa712561da47dcd1e7ad544a257482392472eae1c86d5e5 \\\n    --hash=sha256:dcefc97d1daf8d55199420e9162ab584ed0893a109f45e438b9794ced44c9fd0 \\\n    --hash=sha256:dd98c436a1fc56f48c70882cc243df89ad036210d871c7427dc164b31500dc11 \\\n    --hash=sha256:e74671e43ed4569fbd7989e5eecc7d06dc134b571872ab1d5a88f4a123814e9f \\\n    --hash=sha256:eb9b92f456ff3ec746cd4935b73c1117538d6124b8617bc0fe6fda0b3816e345 \\\n    --hash=sha256:ebb4e637a1fb861c34e48a00d03cffa9234f42bef923aec44e5625ffb9a8e8f9 \\\n    --hash=sha256:ef739fe89e7f43fb6494a43b1878a36273e5924869ba1d866f752c5812ae8d58 \\\n    --hash=sha256:f40db0e02a8157d2b90857c24d89b6310f9b6c3642369852cdc3b5ac49b92afc \\\n    --hash=sha256:f68bf937f113b88c866d090fea0bc52a098695173fc613b055a17ff0cf9683b6 \\\n    --hash=sha256:fb55c182a3f7b84c1a2d6de5fa7b1a05d4660d866b91dbf8d74549c57a1499e8\nzope.proxy==4.3.5 \\\n    --hash=sha256:00573dfa755d0703ab84bb23cb6ecf97bb683c34b340d4df76651f97b0bab068 \\\n    --hash=sha256:092049280f2848d2ba1b57b71fe04881762a220a97b65288bcb0968bb199ec30 \\\n    --hash=sha256:0cbd27b4d3718b5ec74fc65ffa53c78d34c65c6fd9411b8352d2a4f855220cf1 \\\n    --hash=sha256:17fc7e16d0c81f833a138818a30f366696653d521febc8e892858041c4d88785 \\\n    --hash=sha256:19577dfeb70e8a67249ba92c8ad20589a1a2d86a8d693647fa8385408a4c17b0 \\\n    --hash=sha256:207aa914576b1181597a1516e1b90599dc690c095343ae281b0772e44945e6a4 \\\n    --hash=sha256:219a7db5ed53e523eb4a4769f13105118b6d5b04ed169a283c9775af221e231f \\\n    --hash=sha256:2b50ea79849e46b5f4f2b0247a3687505d32d161eeb16a75f6f7e6cd81936e43 \\\n    --hash=sha256:5903d38362b6c716e66bbe470f190579c530a5baf03dbc8500e5c2357aa569a5 \\\n    --hash=sha256:5c24903675e271bd688c6e9e7df5775ac6b168feb87dbe0e4bcc90805f21b28f \\\n    --hash=sha256:5ef6bc5ed98139e084f4e91100f2b098a0cd3493d4e76f9d6b3f7b95d7ad0f06 \\\n    --hash=sha256:61b55ae3c23a126a788b33ffb18f37d6668e79a05e756588d9e4d4be7246ab1c \\\n    --hash=sha256:63ddb992931a5e616c87d3d89f5a58db086e617548005c7f9059fac68c03a5cc \\\n    --hash=sha256:6943da9c09870490dcfd50c4909c0cc19f434fa6948f61282dc9cb07bcf08160 \\\n    --hash=sha256:6ad40f85c1207803d581d5d75e9ea25327cd524925699a83dfc03bf8e4ba72b7 \\\n    --hash=sha256:6b44433a79bdd7af0e3337bd7bbcf53dd1f9b0fa66bf21bcb756060ce32a96c1 \\\n    --hash=sha256:6bbaa245015d933a4172395baad7874373f162955d73612f0b66b6c2c33b6366 \\\n    --hash=sha256:7007227f4ea85b40a2f5e5a244479f6a6dfcf906db9b55e812a814a8f0e2c28d \\\n    --hash=sha256:74884a0aec1f1609190ec8b34b5d58fb3b5353cf22b96161e13e0e835f13518f \\\n    --hash=sha256:7d25fe5571ddb16369054f54cdd883f23de9941476d97f2b92eb6d7d83afe22d \\\n    --hash=sha256:7e162bdc5e3baad26b2262240be7d2bab36991d85a6a556e48b9dfb402370261 \\\n    --hash=sha256:814d62678dc3a30f4aa081982d830b7c342cf230ffc9d030b020cb154eeebf9e \\\n    --hash=sha256:8878a34c5313ee52e20aa50b03138af8d472bae465710fb954d133a9bfd3c38d \\\n    --hash=sha256:a66a0d94e5b081d5d695e66d6667e91e74d79e273eee95c1747717ba9cb70792 \\\n    --hash=sha256:a69f5cbf4addcfdf03dda564a671040127a6b7c34cf9fe4973582e68441b63fa \\\n    --hash=sha256:b00f9f0c334d07709d3f73a7cb8ae63c6ca1a90c790a63b5e7effa666ef96021 \\\n    --hash=sha256:b6ed71e4a7b4690447b626f499d978aa13197a0e592950e5d7020308f6054698 \\\n    --hash=sha256:bdf5041e5851526e885af579d2f455348dba68d74f14a32781933569a327fddf \\\n    --hash=sha256:be034360dd34e62608419f86e799c97d389c10a0e677a25f236a971b2f40dac9 \\\n    --hash=sha256:cc8f590a5eed30b314ae6b0232d925519ade433f663de79cc3783e4b10d662ba \\\n    --hash=sha256:cd7a318a15fe6cc4584bf3c4426f092ed08c0fd012cf2a9173114234fe193e11 \\\n    --hash=sha256:cf19b5f63a59c20306e034e691402b02055c8f4e38bf6792c23cad489162a642 \\\n    --hash=sha256:cfc781ce442ec407c841e9aa51d0e1024f72b6ec34caa8fdb6ef9576d549acf2 \\\n    --hash=sha256:dea9f6f8633571e18bc20cad83603072e697103a567f4b0738d52dd0211b4527 \\\n    --hash=sha256:e4a86a1d5eb2cce83c5972b3930c7c1eac81ab3508464345e2b8e54f119d5505 \\\n    --hash=sha256:e7106374d4a74ed9ff00c46cc00f0a9f06a0775f8868e423f85d4464d2333679 \\\n    --hash=sha256:e98a8a585b5668aa9e34d10f7785abf9545fe72663b4bfc16c99a115185ae6a5 \\\n    --hash=sha256:f64840e68483316eb58d82c376ad3585ca995e69e33b230436de0cdddf7363f9 \\\n    --hash=sha256:f8f4b0a9e6683e43889852130595c8854d8ae237f2324a053cdd884de936aa9b \\\n    --hash=sha256:fc45a53219ed30a7f670a6d8c98527af0020e6fd4ee4c0a8fb59f147f06d816c\n\n# Contains the requirements for the letsencrypt package.\n#\n# Since the letsencrypt package depends on certbot and using pip with hashes\n# requires that all installed packages have hashes listed, this allows\n# dependency-requirements.txt to be used without requiring a hash for a\n# (potentially unreleased) Certbot package.\n\nletsencrypt==0.7.0 \\\n    --hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \\\n    --hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9\n\ncertbot==1.14.0 \\\n    --hash=sha256:67b4d26ceaea6c7f8325d0d45169e7a165a2cabc7122c84bc971ba068ca19cca \\\n    --hash=sha256:959ea90c6bb8dca38eab9772722cb940972ef6afcd5f15deef08b3c3636841eb\nacme==1.14.0 \\\n    --hash=sha256:4f48c41261202f1a389ec2986b2580b58f53e0d5a1ae2463b34318d78b87fc66 \\\n    --hash=sha256:61daccfb0343628cbbca551a7fc4c82482113952c21db3fe0c585b7c98fa1c35\ncertbot-apache==1.14.0 \\\n    --hash=sha256:b757038db23db707c44630fecb46e99172bd791f0db5a8e623c0842613c4d3d9 \\\n    --hash=sha256:887fe4a21af2de1e5c2c9428bacba6eb7c1219257bc70f1a1d8447c8a321adb0\ncertbot-nginx==1.14.0 \\\n    --hash=sha256:8916a815437988d6c192df9f035bb7a176eab20eee0956677b335d0698d243fb \\\n    --hash=sha256:cc2a8a0de56d9bb6b2efbda6c80c647dad8db2bb90675cac03ade94bd5fc8597\n\nUNLIKELY_EOF\n    # -------------------------------------------------------------------------\n    cat << \"UNLIKELY_EOF\" > \"$TEMP_DIR/pipstrap.py\"\n#!/usr/bin/env python\n\"\"\"A small script that can act as a trust root for installing pip >=8\nEmbed this in your project, and your VCS checkout is all you have to trust. In\na post-peep era, this lets you claw your way to a hash-checking version of pip,\nwith which you can install the rest of your dependencies safely. All it assumes\nis Python 2.6 or better and *some* version of pip already installed. If\nanything goes wrong, it will exit with a non-zero status code.\n\"\"\"\n# This is here so embedded copies are MIT-compliant:\n# Copyright (c) 2016 Erik Rose\n#\n# Permission is hereby granted, free of charge, to any person obtaining a copy\n# of this software and associated documentation files (the \"Software\"), to\n# deal in the Software without restriction, including without limitation the\n# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n# sell copies of the Software, and to permit persons to whom the Software is\n# furnished to do so, subject to the following conditions:\n#\n# The above copyright notice and this permission notice shall be included in\n# all copies or substantial portions of the Software.\nfrom __future__ import print_function\nfrom distutils.version import StrictVersion\nfrom hashlib import sha256\nfrom os import environ\nfrom os.path import join\nfrom shutil import rmtree\ntry:\n    from subprocess import check_output\nexcept ImportError:\n    from subprocess import CalledProcessError, PIPE, Popen\n\n    def check_output(*popenargs, **kwargs):\n        if 'stdout' in kwargs:\n            raise ValueError('stdout argument not allowed, it will be '\n                             'overridden.')\n        process = Popen(stdout=PIPE, *popenargs, **kwargs)\n        output, unused_err = process.communicate()\n        retcode = process.poll()\n        if retcode:\n            cmd = kwargs.get(\"args\")\n            if cmd is None:\n                cmd = popenargs[0]\n            raise CalledProcessError(retcode, cmd)\n        return output\nimport sys\nfrom tempfile import mkdtemp\ntry:\n    from urllib2 import build_opener, HTTPHandler, HTTPSHandler\nexcept ImportError:\n    from urllib.request import build_opener, HTTPHandler, HTTPSHandler\ntry:\n    from urlparse import urlparse\nexcept ImportError:\n    from urllib.parse import urlparse  # 3.4\n\n\n__version__ = 1, 5, 1\nPIP_VERSION = '9.0.1'\nDEFAULT_INDEX_BASE = 'https://pypi.python.org'\n\n\n# wheel has a conditional dependency on argparse:\nmaybe_argparse = (\n    [('18/dd/e617cfc3f6210ae183374cd9f6a26b20514bbb5a792af97949c5aacddf0f/'\n      'argparse-1.4.0.tar.gz',\n      '62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4')]\n    if sys.version_info < (2, 7, 0) else [])\n\n\n# Be careful when updating the pinned versions here, in particular for pip.\n# Indeed starting from 10.0, pip will build dependencies in isolation if the\n# related projects are compliant with PEP 517. This is not something we want\n# as of now, so the isolation build will need to be disabled wherever\n# pipstrap is used (see https://github.com/certbot/certbot/issues/8256).\nPACKAGES = maybe_argparse + [\n    # Pip has no dependencies, as it vendors everything:\n    ('11/b6/abcb525026a4be042b486df43905d6893fb04f05aac21c32c638e939e447/'\n     'pip-{0}.tar.gz'.format(PIP_VERSION),\n     '09f243e1a7b461f654c26a725fa373211bb7ff17a9300058b205c61658ca940d'),\n    # This version of setuptools has only optional dependencies:\n    ('37/1b/b25507861991beeade31473868463dad0e58b1978c209de27384ae541b0b/'\n     'setuptools-40.6.3.zip',\n     '3b474dad69c49f0d2d86696b68105f3a6f195f7ab655af12ef9a9c326d2b08f8'),\n    ('c9/1d/bd19e691fd4cfe908c76c429fe6e4436c9e83583c4414b54f6c85471954a/'\n     'wheel-0.29.0.tar.gz',\n     '1ebb8ad7e26b448e9caa4773d2357849bf80ff9e313964bcaf79cbf0201a1648')\n]\n\n\nclass HashError(Exception):\n    def __str__(self):\n        url, path, actual, expected = self.args\n        return ('{url} did not match the expected hash {expected}. Instead, '\n                'it was {actual}. The file (left at {path}) may have been '\n                'tampered with.'.format(**locals()))\n\n\ndef hashed_download(url, temp, digest):\n    \"\"\"Download ``url`` to ``temp``, make sure it has the SHA-256 ``digest``,\n    and return its path.\"\"\"\n    # Based on pip 1.4.1's URLOpener but with cert verification removed. Python\n    # >=2.7.9 verifies HTTPS certs itself, and, in any case, the cert\n    # authenticity has only privacy (not arbitrary code execution)\n    # implications, since we're checking hashes.\n    def opener(using_https=True):\n        opener = build_opener(HTTPSHandler())\n        if using_https:\n            # Strip out HTTPHandler to prevent MITM spoof:\n            for handler in opener.handlers:\n                if isinstance(handler, HTTPHandler):\n                    opener.handlers.remove(handler)\n        return opener\n\n    def read_chunks(response, chunk_size):\n        while True:\n            chunk = response.read(chunk_size)\n            if not chunk:\n                break\n            yield chunk\n\n    parsed_url = urlparse(url)\n    response = opener(using_https=parsed_url.scheme == 'https').open(url)\n    path = join(temp, parsed_url.path.split('/')[-1])\n    actual_hash = sha256()\n    with open(path, 'wb') as file:\n        for chunk in read_chunks(response, 4096):\n            file.write(chunk)\n            actual_hash.update(chunk)\n\n    actual_digest = actual_hash.hexdigest()\n    if actual_digest != digest:\n        raise HashError(url, path, actual_digest, digest)\n    return path\n\n\ndef get_index_base():\n    \"\"\"Return the URL to the dir containing the \"packages\" folder.\n    Try to wring something out of PIP_INDEX_URL, if set. Hack \"/simple\" off the\n    end if it's there; that is likely to give us the right dir.\n    \"\"\"\n    env_var = environ.get('PIP_INDEX_URL', '').rstrip('/')\n    if env_var:\n        SIMPLE = '/simple'\n        if env_var.endswith(SIMPLE):\n            return env_var[:-len(SIMPLE)]\n        else:\n            return env_var\n    else:\n        return DEFAULT_INDEX_BASE\n\n\ndef main():\n    python = sys.executable or 'python'\n    pip_version = StrictVersion(check_output([python, '-m', 'pip', '--version'])\n                                .decode('utf-8').split()[1])\n    has_pip_cache = pip_version >= StrictVersion('6.0')\n    index_base = get_index_base()\n    temp = mkdtemp(prefix='pipstrap-')\n    try:\n        downloads = [hashed_download(index_base + '/packages/' + path,\n                                     temp,\n                                     digest)\n                     for path, digest in PACKAGES]\n        # Calling pip as a module is the preferred way to avoid problems about pip self-upgrade.\n        command = [python, '-m', 'pip', 'install', '--no-index', '--no-deps', '-U']\n        # Disable cache since it is not used and it otherwise sometimes throws permission warnings:\n        command.extend(['--no-cache-dir'] if has_pip_cache else [])\n        command.extend(downloads)\n        check_output(command)\n    except HashError as exc:\n        print(exc)\n    except Exception:\n        rmtree(temp)\n        raise\n    else:\n        rmtree(temp)\n        return 0\n    return 1\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n\nUNLIKELY_EOF\n    # -------------------------------------------------------------------------\n    # Set PATH so pipstrap upgrades the right (v)env:\n    PATH=\"$VENV_BIN:$PATH\" \"$VENV_BIN/python\" \"$TEMP_DIR/pipstrap.py\"\n    set +e\n    if [ \"$VERBOSE\" = 1 ]; then\n      \"$VENV_BIN/pip\" install --disable-pip-version-check --no-cache-dir --require-hashes -r \"$TEMP_DIR/letsencrypt-auto-requirements.txt\"\n    else\n      PIP_OUT=`\"$VENV_BIN/pip\" install --disable-pip-version-check --no-cache-dir --require-hashes -r \"$TEMP_DIR/letsencrypt-auto-requirements.txt\" 2>&1`\n    fi\n    PIP_STATUS=$?\n    set -e\n    if [ \"$PIP_STATUS\" != 0 ]; then\n      # Report error. (Otherwise, be quiet.)\n      error \"Had a problem while installing Python packages.\"\n      if [ \"$VERBOSE\" != 1 ]; then\n        error\n        error \"pip prints the following errors: \"\n        error \"=====================================================\"\n        error \"$PIP_OUT\"\n        error \"=====================================================\"\n        error\n        error \"Certbot has problem setting up the virtual environment.\"\n\n        if `echo $PIP_OUT | grep -q Killed` || `echo $PIP_OUT | grep -q \"allocate memory\"` ; then\n          error\n          error \"Based on your pip output, the problem can likely be fixed by \"\n          error \"increasing the available memory.\"\n        else\n          error\n          error \"We were not be able to guess the right solution from your pip \"\n          error \"output.\"\n        fi\n\n        error\n        error \"Consult https://certbot.eff.org/docs/install.html#problems-with-python-virtual-environment\"\n        error \"for possible solutions.\"\n        error \"You may also find some support resources at https://certbot.eff.org/support/ .\"\n      fi\n      rm -rf \"$VENV_PATH\"\n      exit 1\n    fi\n\n    if [ -d \"$OLD_VENV_PATH\" -a ! -L \"$OLD_VENV_PATH\" ]; then\n      rm -rf \"$OLD_VENV_PATH\"\n      ln -s \"$VENV_PATH\" \"$OLD_VENV_PATH\"\n    fi\n\n    say \"Installation succeeded.\"\n  fi\n\n  # If you're modifying any of the code after this point in this current `if` block, you\n  # may need to update the \"$DEPRECATED_OS\" = 1 case at the beginning of phase 2 as well.\n\n  if [ \"$INSTALL_ONLY\" = 1 ]; then\n    say \"Certbot is installed.\"\n    exit 0\n  fi\n\n  \"$VENV_BIN/letsencrypt\" \"$@\"\n\nelse\n  # Phase 1: Upgrade certbot-auto if necessary, then self-invoke.\n  #\n  # Each phase checks the version of only the thing it is responsible for\n  # upgrading. Phase 1 checks the version of the latest release of\n  # certbot-auto (which is always the same as that of the certbot\n  # package). Phase 2 checks the version of the locally installed certbot.\n  export PHASE_1_VERSION=\"$LE_AUTO_VERSION\"\n\n  if [ ! -f \"$VENV_BIN/letsencrypt\" ]; then\n    if ! OldVenvExists; then\n      if [ \"$HELP\" = 1 ]; then\n        echo \"$USAGE\"\n        exit 0\n      fi\n      # If it looks like we've never bootstrapped before, bootstrap:\n      Bootstrap\n    fi\n  fi\n  if [ \"$OS_PACKAGES_ONLY\" = 1 ]; then\n    say \"OS packages installed.\"\n    exit 0\n  fi\n\n  DeterminePythonVersion \"NOCRASH\"\n  # Don't warn about file permissions if the user disabled the check or we\n  # can't find an up-to-date Python.\n  if [ \"$PYVER\" -ge \"$MIN_PYVER\" -a \"$NO_PERMISSIONS_CHECK\" != 1 ]; then\n    # If the script fails for some reason, don't break certbot-auto.\n    set +e\n    # Suppress unexpected error output.\n    CHECK_PERM_OUT=$(CheckPathPermissions \"$LE_PYTHON\" \"$0\" 2>/dev/null)\n    CHECK_PERM_STATUS=\"$?\"\n    set -e\n    # Only print output if the script ran successfully and it actually produced\n    # output. The latter check resolves\n    # https://github.com/certbot/certbot/issues/7012.\n    if [ \"$CHECK_PERM_STATUS\" = 0 -a -n \"$CHECK_PERM_OUT\" ]; then\n      error \"$CHECK_PERM_OUT\"\n    fi\n  fi\n\n  if [ \"$NO_SELF_UPGRADE\" != 1 ]; then\n    TEMP_DIR=$(TempDir)\n    trap 'rm -rf \"$TEMP_DIR\"' EXIT\n    # ---------------------------------------------------------------------------\n    cat << \"UNLIKELY_EOF\" > \"$TEMP_DIR/fetch.py\"\n\"\"\"Do downloading and JSON parsing without additional dependencies. ::\n\n    # Print latest released version of LE to stdout:\n    python fetch.py --latest-version\n\n    # Download letsencrypt-auto script from git tag v1.2.3 into the folder I'm\n    # in, and make sure its signature verifies:\n    python fetch.py --le-auto-script v1.2.3\n\nOn failure, return non-zero.\n\n\"\"\"\n\nfrom __future__ import print_function, unicode_literals\n\nfrom distutils.version import LooseVersion\nfrom json import loads\nfrom os import devnull, environ\nfrom os.path import dirname, join\nimport re\nimport ssl\nfrom subprocess import check_call, CalledProcessError\nfrom sys import argv, exit\ntry:\n    from urllib2 import build_opener, HTTPHandler, HTTPSHandler\n    from urllib2 import HTTPError, URLError\nexcept ImportError:\n    from urllib.request import build_opener, HTTPHandler, HTTPSHandler\n    from urllib.error import HTTPError, URLError\n\nPUBLIC_KEY = environ.get('LE_AUTO_PUBLIC_KEY', \"\"\"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6MR8W/galdxnpGqBsYbq\nOzQb2eyW15YFjDDEMI0ZOzt8f504obNs920lDnpPD2/KqgsfjOgw2K7xWDJIj/18\nxUvWPk3LDkrnokNiRkA3KOx3W6fHycKL+zID7zy+xZYBuh2fLyQtWV1VGQ45iNRp\n9+Zo7rH86cdfgkdnWTlNSHyTLW9NbXvyv/E12bppPcEvgCTAQXgnDVJ0/sqmeiij\nn9tTFh03aM+R2V/21h8aTraAS24qiPCz6gkmYGC8yr6mglcnNoYbsLNYZ69zF1XH\ncXPduCPdPdfLlzVlKK1/U7hkA28eG3BIAMh6uJYBRJTpiGgaGdPd7YekUB8S6cy+\nCQIDAQAB\n-----END PUBLIC KEY-----\n\"\"\")\n\nclass ExpectedError(Exception):\n    \"\"\"A novice-readable exception that also carries the original exception for\n    debugging\"\"\"\n\n\nclass HttpsGetter(object):\n    def __init__(self):\n        \"\"\"Build an HTTPS opener.\"\"\"\n        # Based on pip 1.4.1's URLOpener\n        # This verifies certs on only Python >=2.7.9, and when NO_CERT_VERIFY isn't set.\n        if environ.get('NO_CERT_VERIFY') == '1' and hasattr(ssl, 'SSLContext'):\n            self._opener = build_opener(HTTPSHandler(context=cert_none_context()))\n        else:\n            self._opener = build_opener(HTTPSHandler())\n        # Strip out HTTPHandler to prevent MITM spoof:\n        for handler in self._opener.handlers:\n            if isinstance(handler, HTTPHandler):\n                self._opener.handlers.remove(handler)\n\n    def get(self, url):\n        \"\"\"Return the document contents pointed to by an HTTPS URL.\n\n        If something goes wrong (404, timeout, etc.), raise ExpectedError.\n\n        \"\"\"\n        try:\n            # socket module docs say default timeout is None: that is, no\n            # timeout\n            return self._opener.open(url, timeout=30).read()\n        except (HTTPError, IOError) as exc:\n            raise ExpectedError(\"Couldn't download %s.\" % url, exc)\n\n\ndef write(contents, dir, filename):\n    \"\"\"Write something to a file in a certain directory.\"\"\"\n    with open(join(dir, filename), 'wb') as file:\n        file.write(contents)\n\n\ndef latest_stable_version(get):\n    \"\"\"Return the latest stable release of letsencrypt.\"\"\"\n    metadata = loads(get(\n        environ.get('LE_AUTO_JSON_URL',\n                    'https://pypi.python.org/pypi/certbot/json')).decode('UTF-8'))\n    # metadata['info']['version'] actually returns the latest of any kind of\n    # release release, contrary to https://wiki.python.org/moin/PyPIJSON.\n    # The regex is a sufficient regex for picking out prereleases for most\n    # packages, LE included.\n    return str(max(LooseVersion(r) for r\n                   in metadata['releases'].keys()\n                   if re.match('^[0-9.]+$', r)))\n\n\ndef verified_new_le_auto(get, tag, temp_dir):\n    \"\"\"Return the path to a verified, up-to-date letsencrypt-auto script.\n\n    If the download's signature does not verify or something else goes wrong\n    with the verification process, raise ExpectedError.\n\n    \"\"\"\n    le_auto_dir = environ.get(\n        'LE_AUTO_DIR_TEMPLATE',\n        'https://raw.githubusercontent.com/certbot/certbot/%s/'\n        'letsencrypt-auto-source/') % tag\n    write(get(le_auto_dir + 'letsencrypt-auto'), temp_dir, 'letsencrypt-auto')\n    write(get(le_auto_dir + 'letsencrypt-auto.sig'), temp_dir, 'letsencrypt-auto.sig')\n    write(PUBLIC_KEY.encode('UTF-8'), temp_dir, 'public_key.pem')\n    try:\n        with open(devnull, 'w') as dev_null:\n            check_call(['openssl', 'dgst', '-sha256', '-verify',\n                        join(temp_dir, 'public_key.pem'),\n                        '-signature',\n                        join(temp_dir, 'letsencrypt-auto.sig'),\n                        join(temp_dir, 'letsencrypt-auto')],\n                       stdout=dev_null,\n                       stderr=dev_null)\n    except CalledProcessError as exc:\n        raise ExpectedError(\"Couldn't verify signature of downloaded \"\n                            \"certbot-auto.\", exc)\n\n\ndef cert_none_context():\n    \"\"\"Create a SSLContext object to not check hostname.\"\"\"\n    # PROTOCOL_TLS isn't available before 2.7.13 but this code is for 2.7.9+, so use this.\n    context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)\n    context.verify_mode = ssl.CERT_NONE\n    return context\n\n\ndef main():\n    get = HttpsGetter().get\n    flag = argv[1]\n    try:\n        if flag == '--latest-version':\n            print(latest_stable_version(get))\n        elif flag == '--le-auto-script':\n            tag = argv[2]\n            verified_new_le_auto(get, tag, dirname(argv[0]))\n    except ExpectedError as exc:\n        print(exc.args[0], exc.args[1])\n        return 1\n    else:\n        return 0\n\n\nif __name__ == '__main__':\n    exit(main())\n\nUNLIKELY_EOF\n    # ---------------------------------------------------------------------------\n    if [ \"$PYVER\" -lt \"$MIN_PYVER\" ]; then\n      error \"WARNING: couldn't find Python $MIN_PYTHON_VERSION+ to check for updates.\"\n    elif ! REMOTE_VERSION=`\"$LE_PYTHON\" \"$TEMP_DIR/fetch.py\" --latest-version` ; then\n      error \"WARNING: unable to check for updates.\"\n    fi\n\n    # If for any reason REMOTE_VERSION is not set, let's assume certbot-auto is up-to-date,\n    # and do not go into the self-upgrading process.\n    if [ -n \"$REMOTE_VERSION\" ]; then\n      LE_VERSION_STATE=`CompareVersions \"$LE_PYTHON\" \"$LE_AUTO_VERSION\" \"$REMOTE_VERSION\"`\n\n      if [ \"$LE_VERSION_STATE\" = \"UNOFFICIAL\" ]; then\n        say \"Unofficial certbot-auto version detected, self-upgrade is disabled: $LE_AUTO_VERSION\"\n      elif [ \"$LE_VERSION_STATE\" = \"OUTDATED\" ]; then\n        say \"Upgrading certbot-auto $LE_AUTO_VERSION to $REMOTE_VERSION...\"\n\n        # Now we drop into Python so we don't have to install even more\n        # dependencies (curl, etc.), for better flow control, and for the option of\n        # future Windows compatibility.\n        \"$LE_PYTHON\" \"$TEMP_DIR/fetch.py\" --le-auto-script \"v$REMOTE_VERSION\"\n\n        # Install new copy of certbot-auto.\n        # TODO: Deal with quotes in pathnames.\n        say \"Replacing certbot-auto...\"\n        # Clone permissions with cp. chmod and chown don't have a --reference\n        # option on macOS or BSD, and stat -c on Linux is stat -f on macOS and BSD:\n        cp -p \"$0\" \"$TEMP_DIR/letsencrypt-auto.permission-clone\"\n        cp \"$TEMP_DIR/letsencrypt-auto\" \"$TEMP_DIR/letsencrypt-auto.permission-clone\"\n        # Using mv rather than cp leaves the old file descriptor pointing to the\n        # original copy so the shell can continue to read it unmolested. mv across\n        # filesystems is non-atomic, doing `rm dest, cp src dest, rm src`, but the\n        # cp is unlikely to fail if the rm doesn't.\n        mv -f \"$TEMP_DIR/letsencrypt-auto.permission-clone\" \"$0\"\n      fi  # A newer version is available.\n    fi\n  fi  # Self-upgrading is allowed.\n\n  RerunWithArgs --le-auto-phase2 \"$@\"\nfi\n"
  },
  {
    "path": "letsencrypt-auto-source/letsencrypt-auto.sig",
    "content": "JCdxW3ܚ̌\u0017\rX}o|ܮ\u0005H] <b\u0003\t~\u0015{Q,Ǝ(:oL)\u0010S$@,\u001ceq\u0007bQ\u001fOL\"v\u0011C>Js\u0004Qj\fSllY3Q]\u0006x+/V`$4xM晾UăS\u0001Pl\rHb\u000ba<{p~`Ќ\u0004&a1E@;RNiLJ@pwk!\t&,<z\u001ao,\u0012o\u0006n\u001cL~\u001b!"
  },
  {
    "path": "letstest/README.md",
    "content": "# letstest\nSimple AWS testfarm scripts for certbot client testing\n\n- Launches EC2 instances with a given list of AMIs for different distros\n- Copies certbot repo and puts it on the instances\n- Runs certbot tests (bash scripts) on all of these\n- Logs execution and success/fail for debugging\n\n## Notes\n  - Some AWS images, e.g. official CentOS and FreeBSD images\n    require acceptance of user terms on the AWS marketplace\n    website.  This can't be automated.\n  - AWS EC2 has a default limit of 20 t2/t1 instances, if more\n    are needed, they need to be requested via online webform.\n\n## Installation and configuration\n\nThis package is installed in the Certbot development environment that is\ncreated by following the instructions at\nhttps://certbot.eff.org/docs/contributing.html#running-a-local-copy-of-the-client.\n\nThese tests use the AWS SDK for Python (boto3) to manipulate EC2 instances.\nBefore running the tests, you'll need to set up credentials by following the\ninstructions at\nhttps://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html#configuration.\nYou will also want to create a `~/.aws/config` file setting the region for your\nprofile to `us-east-1`, following the instructions in the boto3 quickstart guide above.\n\nLastly, you will want to create a file on your system containing a trusted SSH key\nby following the instructions at\nhttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/create-key-pairs.html.\n\n## Usage\nTo run tests, activate the virtual environment you created above and from this directory run:\n```\n>letstest targets/targets.yaml /path/to/your/key.pem <profile name> scripts/<test to run>\n```\n\nA temporary directory whose name is output by the tests is also created with a log file from each instance of the test and a file named \"results\" containing the output above.\nThe tests take quite a while to run.\n\n## Scripts\nExample scripts are in the 'scripts' directory, these are just bash scripts that have a few parameters passed\nto them at runtime via environment variables.  test_apache2.sh is a useful reference.\n\ntest_apache2 runs the dev venv and does local tests.\n\nSee:\n- https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html\n- https://docs.aws.amazon.com/cli/latest/userguide/cli-ec2-keypairs.html\n\nMain repos:\n- https://github.com/certbot/certbot\n"
  },
  {
    "path": "letstest/letstest/__init__.py",
    "content": ""
  },
  {
    "path": "letstest/letstest/multitester.py",
    "content": "\"\"\"\nCertbot Integration Test Tool\n\n- Launches EC2 instances with a given list of AMIs for different distros\n- Copies certbot repo and puts it on the instances\n- Runs certbot tests (bash scripts) on all of these\n- Logs execution and success/fail for debugging\n\nNotes:\n  - Some AWS images, e.g. official CentOS and FreeBSD images\n    require acceptance of user terms on the AWS marketplace\n    website.  This can't be automated.\n  - AWS EC2 has a default limit of 20 t2/t1 instances, if more\n    are needed, they need to be requested via online webform.\n\nUsage:\n  - Requires AWS IAM secrets to be set up with aws cli\n  - Requires an AWS associated keyfile <keyname>.pem\n\n>aws configure --profile HappyHacker\n[interactive: enter secrets for IAM role]\n>aws ec2 create-key-pair --profile HappyHacker --key-name MyKeyPair \\\n --query 'KeyMaterial' --output text > MyKeyPair.pem\nthen:\n>letstest targets/targets.yaml MyKeyPair.pem HappyHacker scripts/test_apache2.sh\nsee:\n  https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html\n  https://docs.aws.amazon.com/cli/latest/userguide/cli-ec2-keypairs.html\n\"\"\"\nimport argparse\nimport multiprocessing as mp\nfrom multiprocessing import Manager\nimport os\nimport socket\nimport sys\nimport tempfile\nimport time\nimport traceback\n\nimport boto3\nfrom fabric import Config\nfrom fabric import Connection\nimport yaml\n\n# Command line parser\n#-------------------------------------------------------------------------------\nparser = argparse.ArgumentParser(description='Builds EC2 cluster for testing.')\nparser.add_argument('config_file',\n                    help='yaml configuration file for AWS server cluster')\nparser.add_argument('key_file',\n                    help='key file (<keyname>.pem) for AWS')\nparser.add_argument('aws_profile',\n                    help='profile for AWS (i.e. as in ~/.aws/certificates)')\nparser.add_argument('test_script',\n                    default='test_apache2.sh',\n                    help='path of bash script in to deploy and run')\nparser.add_argument('--repo',\n                    default='https://github.com/certbot/certbot.git',\n                    help='certbot git repo to use')\nparser.add_argument('--branch',\n                    default='~',\n                    help='certbot git branch to trial')\nparser.add_argument('--pull_request',\n                    default='~',\n                    help='certbot/certbot pull request to trial')\nparser.add_argument('--merge_main',\n                    action='store_true',\n                    help=\"if set merges PR into main branch of certbot/certbot\")\nparser.add_argument('--saveinstances',\n                    action='store_true',\n                    help=\"don't kill EC2 instances after run, useful for debugging\")\nparser.add_argument('--alt_pip',\n                    default='',\n                    help=\"server from which to pull candidate release packages\")\ncl_args = parser.parse_args()\n\n# Credential Variables\n#-------------------------------------------------------------------------------\n# assumes naming: <key_filename> = <keyname>.pem\nKEYFILE = cl_args.key_file\nKEYNAME = os.path.split(cl_args.key_file)[1].split('.pem')[0]\nPROFILE = None if cl_args.aws_profile == 'SET_BY_ENV' else cl_args.aws_profile\n\n# Globals\n#-------------------------------------------------------------------------------\nSECURITY_GROUP_NAME = 'certbot-security-group'\nSENTINEL = None #queue kill signal\nSUBNET_NAME = 'certbot-subnet'\n\nclass Status:\n    \"\"\"Possible statuses of client tests.\"\"\"\n    PASS = 'pass'\n    FAIL = 'fail'\n\n# Boto3/AWS automation functions\n#-------------------------------------------------------------------------------\ndef should_use_subnet(subnet):\n    \"\"\"Should we use the given subnet for these tests?\n\n    We should if it is the default subnet for the availability zone or the\n    subnet is named \"certbot-subnet\".\n\n    \"\"\"\n    if not subnet.map_public_ip_on_launch:\n        return False\n    if subnet.default_for_az:\n        return True\n    for tag in subnet.tags:\n        if tag['Key'] == 'Name' and tag['Value'] == SUBNET_NAME:\n            return True\n    return False\n\ndef make_security_group(vpc):\n    \"\"\"Creates a security group in the given VPC.\"\"\"\n    # will fail if security group of GroupName already exists\n    # cannot have duplicate SGs of the same name\n    mysg = vpc.create_security_group(GroupName=SECURITY_GROUP_NAME,\n                                     Description='security group for automated testing')\n    mysg.authorize_ingress(IpProtocol=\"tcp\", CidrIp=\"0.0.0.0/0\", FromPort=22, ToPort=22)\n    # for mosh\n    mysg.authorize_ingress(IpProtocol=\"udp\", CidrIp=\"0.0.0.0/0\", FromPort=60000, ToPort=61000)\n    return mysg\n\ndef make_instance(ec2_client,\n                  instance_name,\n                  ami_id,\n                  keyname,\n                  security_group_id,\n                  subnet_id,\n                  self_destruct,\n                  machine_type='t2.micro'):\n    \"\"\"Creates an instance using the given parameters.\n\n    If self_destruct is True, the instance will be configured to shutdown after\n    1 hour and to terminate itself on shutdown.\n\n    \"\"\"\n    block_device_mappings = _get_block_device_mappings(ec2_client, ami_id)\n    tags = [{'Key': 'Name', 'Value': instance_name}]\n    tag_spec = [{'ResourceType': 'instance', 'Tags': tags}]\n    kwargs = {\n        'BlockDeviceMappings': block_device_mappings,\n        'ImageId': ami_id,\n        'SecurityGroupIds': [security_group_id],\n        'SubnetId': subnet_id,\n        'KeyName': keyname,\n        'MinCount': 1,\n        'MaxCount': 1,\n        'InstanceType': machine_type,\n        'TagSpecifications': tag_spec\n    }\n    if self_destruct:\n            kwargs['InstanceInitiatedShutdownBehavior'] = 'terminate'\n            kwargs['UserData'] = '#!/bin/bash\\nshutdown -P +60\\n'\n    return ec2_client.create_instances(**kwargs)[0]\n\ndef _get_block_device_mappings(ec2_client, ami_id):\n    \"\"\"Returns the list of block device mappings to ensure cleanup.\n\n    This list sets connected EBS volumes to be deleted when the EC2\n    instance is terminated.\n\n    \"\"\"\n    # Not all devices use EBS, but the default value for DeleteOnTermination\n    # when the device does use EBS is true. See:\n    # * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-blockdev-mapping.html\n    # * https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-blockdev-template.html\n    return [{'DeviceName': mapping['DeviceName'],\n             'Ebs': {'DeleteOnTermination': True}}\n            for mapping in ec2_client.Image(ami_id).block_device_mappings\n            if not mapping.get('Ebs', {}).get('DeleteOnTermination', True)]\n\n\n# Helper Routines\n#-------------------------------------------------------------------------------\ndef block_until_ssh_open(ipstring, wait_time=10, timeout=120):\n    \"Blocks until server at ipstring has an open port 22\"\n    reached = False\n    t_elapsed = 0\n    while not reached and t_elapsed < timeout:\n        try:\n            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n            sock.connect((ipstring, 22))\n            reached = True\n        except OSError:\n            time.sleep(wait_time)\n            t_elapsed += wait_time\n    sock.close()\n\ndef block_until_instance_ready(booting_instance, extra_wait_time=20):\n    \"Blocks booting_instance until AWS EC2 instance is ready to accept SSH connections\"\n    booting_instance.wait_until_running()\n    # The instance needs to be reloaded to update its local attributes. See\n    # https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ec2.html#EC2.Instance.reload.\n    booting_instance.reload()\n    # After waiting for the instance to be running and reloading the instance\n    # state, we should have an IP address.\n    assert booting_instance.public_ip_address is not None\n    block_until_ssh_open(booting_instance.public_ip_address)\n    time.sleep(extra_wait_time)\n    return booting_instance\n\n\n# Fabric Routines\n#-------------------------------------------------------------------------------\ndef local_git_clone(local_cxn, repo_url, log_dir):\n    \"\"\"clones main of repo_url\"\"\"\n    local_cxn.local('cd %s && if [ -d letsencrypt ]; then rm -rf letsencrypt; fi' % log_dir)\n    local_cxn.local('cd %s && git clone %s letsencrypt'% (log_dir, repo_url))\n    local_cxn.local('cd %s && tar czf le.tar.gz letsencrypt'% log_dir)\n\ndef local_git_branch(local_cxn, repo_url, branch_name, log_dir):\n    \"\"\"clones branch <branch_name> of repo_url\"\"\"\n    local_cxn.local('cd %s && if [ -d letsencrypt ]; then rm -rf letsencrypt; fi' % log_dir)\n    local_cxn.local('cd %s && git clone %s letsencrypt --branch %s --single-branch'%\n        (log_dir, repo_url, branch_name))\n    local_cxn.local('cd %s && tar czf le.tar.gz letsencrypt' % log_dir)\n\ndef local_git_PR(local_cxn, repo_url, PRnumstr, log_dir, merge_main=True):\n    \"\"\"clones specified pull request from repo_url and optionally merges into main\"\"\"\n    local_cxn.local('cd %s && if [ -d letsencrypt ]; then rm -rf letsencrypt; fi' % log_dir)\n    local_cxn.local('cd %s && git clone %s letsencrypt' % (log_dir, repo_url))\n    local_cxn.local('cd %s && cd letsencrypt && '\n        'git fetch origin pull/%s/head:lePRtest' % (log_dir, PRnumstr))\n    local_cxn.local('cd %s && cd letsencrypt && git checkout lePRtest' % log_dir)\n    if merge_main:\n        local_cxn.local('cd %s && cd letsencrypt && git remote update origin' % log_dir)\n        local_cxn.local('cd %s && cd letsencrypt && '\n            'git merge origin/main -m \"testmerge\"' % log_dir)\n    local_cxn.local('cd %s && tar czf le.tar.gz letsencrypt' % log_dir)\n\ndef local_repo_to_remote(cxn, log_dir):\n    \"\"\"copies local tarball of repo to remote\"\"\"\n    filename = 'le.tar.gz'\n    local_path = os.path.join(log_dir, filename)\n    cxn.put(local=local_path, remote='')\n    cxn.run('tar xzf %s' % filename)\n\ndef local_repo_clean(local_cxn, log_dir):\n    \"\"\"delete tarball\"\"\"\n    filename = 'le.tar.gz'\n    local_path = os.path.join(log_dir, filename)\n    local_cxn.local('rm %s' % local_path)\n\ndef deploy_script(cxn, scriptpath, *args):\n    \"\"\"copies to remote and executes local script\"\"\"\n    cxn.put(local=scriptpath, remote='', preserve_mode=True)\n    scriptfile = os.path.split(scriptpath)[1]\n    args_str = ' '.join(args)\n    cxn.run('./'+scriptfile+' '+args_str)\n\ndef install_and_launch_certbot(cxn, instance, target, log_dir):\n    local_repo_to_remote(cxn, log_dir)\n    # This needs to be like this, I promise. 1) The env argument to run doesn't work.\n    # See https://github.com/fabric/fabric/issues/1744. 2) prefix() sticks an && between\n    # the commands, so it needs to be exports rather than no &&s in between for the script subshell.\n    with cxn.prefix('export PUBLIC_IP=%s && export PRIVATE_IP=%s && '\n                    'export PUBLIC_HOSTNAME=%s && export PIP_EXTRA_INDEX_URL=%s && '\n                    'export OS_TYPE=%s' %\n                    (instance.public_ip_address,\n                    instance.private_ip_address,\n                    instance.public_dns_name,\n                    cl_args.alt_pip,\n                    target['type'])):\n        deploy_script(cxn, cl_args.test_script)\n\ndef grab_certbot_log(cxn):\n    \"grabs letsencrypt.log via cat into logged stdout\"\n    cxn.sudo('/bin/bash -l -i -c \\'if [ -f \"/var/log/letsencrypt/letsencrypt.log\" ]; then ' +\n        'cat \"/var/log/letsencrypt/letsencrypt.log\"; else echo \"[novarlog]\"; fi\\'')\n    # fallback file if /var/log is unwriteable...? correct?\n    cxn.sudo('/bin/bash -l -i -c \\'if [ -f ./certbot.log ]; then ' +\n        'cat ./certbot.log; else echo \"[nolocallog]\"; fi\\'')\n\n\ndef create_client_instance(ec2_client, target, security_group_id, subnet_id, self_destruct):\n    \"\"\"Create a single client instance for running tests.\"\"\"\n    if 'machine_type' in target:\n        machine_type = target['machine_type']\n    elif target['virt'] == 'hvm':\n        machine_type = 't2.medium'\n    else:\n        # 32 bit systems\n        machine_type = 'c1.medium'\n    name = 'le-%s'%target['name']\n    try:\n        instance = make_instance(ec2_client, name, target['ami'], KEYNAME,\n                                 machine_type=machine_type, security_group_id=security_group_id,\n                                 subnet_id=subnet_id, self_destruct=self_destruct)\n    except Exception:\n        print(f'FAIL: Unable to create instance {name}')\n        raise\n    print(f'Created instance {name}')\n    return instance\n\n\ndef test_client_process(fab_config, inqueue, outqueue, log_dir):\n    cur_proc = mp.current_process()\n    for inreq in iter(inqueue.get, SENTINEL):\n        ii, instance_id, target = inreq\n\n        # Each client process is given its own session due to the suggestion at\n        # https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html?highlight=multithreading#multithreading-multiprocessing.\n        aws_session = boto3.session.Session(profile_name=PROFILE)\n        ec2_client = aws_session.resource('ec2')\n        instance = ec2_client.Instance(id=instance_id)\n\n        #save all stdout to log file\n        sys.stdout = open(log_dir+'/'+'%d_%s.log'%(ii,target['name']), 'w')\n\n        print(\"[%s : client %d %s %s]\" % (cur_proc.name, ii, target['ami'], target['name']))\n        instance = block_until_instance_ready(instance)\n        print(\"server %s at %s\"%(instance, instance.public_ip_address))\n        host_string = \"%s@%s\"%(target['user'], instance.public_ip_address)\n        print(host_string)\n\n        with Connection(host_string, config=fab_config) as cxn:\n            try:\n                install_and_launch_certbot(cxn, instance, target, log_dir)\n                outqueue.put((ii, target, Status.PASS))\n                print(\"%s - %s SUCCESS\"%(target['ami'], target['name']))\n            except:\n                outqueue.put((ii, target, Status.FAIL))\n                print(\"%s - %s FAIL\"%(target['ami'], target['name']))\n                traceback.print_exc(file=sys.stdout)\n                pass\n\n            # append server certbot.log to each per-machine output log\n            print(\"\\n\\ncertbot.log\\n\" + \"-\"*80 + \"\\n\")\n            try:\n                grab_certbot_log(cxn)\n            except:\n                print(\"log fail\\n\")\n                traceback.print_exc(file=sys.stdout)\n                pass\n\n\ndef cleanup(cl_args, instances, targetlist, log_dir):\n    print('Logs in ', log_dir)\n    # If lengths of instances and targetlist aren't equal, instances failed to\n    # start before running tests so leaving instances running for debugging\n    # isn't very useful. Let's cleanup after ourselves instead.\n    if len(instances) != len(targetlist) or not cl_args.saveinstances:\n        print('Terminating EC2 Instances')\n        for instance in instances:\n            instance.terminate()\n    else:\n        # print login information for the boxes for debugging\n        for ii, target in enumerate(targetlist):\n            print(target['name'],\n                  target['ami'],\n                  \"%s@%s\"%(target['user'], instances[ii].public_ip_address))\n\n\ndef main():\n    # Fabric library controlled through global env parameters\n    fab_config = Config(overrides={\n        \"connect_kwargs\": {\n            \"key_filename\": [KEYFILE], # https://github.com/fabric/fabric/issues/2007\n        },\n        \"run\": {\n            \"echo\": True,\n            \"pty\": True,\n        },\n        \"timeouts\": {\n            \"connect\": 10,\n        },\n    })\n    # no network connection, so don't worry about closing this one.\n    local_cxn = Connection('localhost', config=fab_config)\n\n    # Set up local copy of git repo\n    #-------------------------------------------------------------------------------\n    log_dir = tempfile.mkdtemp()  # points to logging / working directory\n    print(\"Local dir for test repo and logs: %s\"%log_dir)\n\n    try:\n        # figure out what git object to test and locally create it in log_dir\n        print(\"Making local git repo\")\n        if cl_args.pull_request != '~':\n            print('Testing PR %s ' % cl_args.pull_request,\n                  \"MERGING into main\" if cl_args.merge_main else \"\")\n            local_git_PR(local_cxn, cl_args.repo, cl_args.pull_request, log_dir,\n                         cl_args.merge_main)\n        elif cl_args.branch != '~':\n            print('Testing branch %s of %s' % (cl_args.branch, cl_args.repo))\n            local_git_branch(local_cxn, cl_args.repo, cl_args.branch, log_dir)\n        else:\n            print('Testing current branch of %s' % cl_args.repo, log_dir)\n            local_git_clone(local_cxn, cl_args.repo, log_dir)\n    except BaseException:\n        print(\"FAIL: trouble with git repo\")\n        traceback.print_exc()\n        exit(1)\n\n\n    # Set up EC2 instances\n    #-------------------------------------------------------------------------------\n    configdata = yaml.safe_load(open(cl_args.config_file, 'r'))\n    targetlist = configdata['targets']\n    print('Testing against these images: [%d total]'%len(targetlist))\n    for target in targetlist:\n        print(target['ami'], target['name'])\n\n    print(\"Connecting to EC2 using\\n profile %s\\n keyname %s\\n keyfile %s\"%(PROFILE, KEYNAME, KEYFILE))\n    aws_session = boto3.session.Session(profile_name=PROFILE)\n    ec2_client = aws_session.resource('ec2')\n\n    print(\"Determining Subnet\")\n    for subnet in ec2_client.subnets.all():\n        if should_use_subnet(subnet):\n            subnet_id = subnet.id\n            vpc_id = subnet.vpc.id\n            break\n    else:\n        print(\"No usable subnet exists!\")\n        print(\"Please create a VPC with a subnet named {0}\".format(SUBNET_NAME))\n        print(\"that maps public IPv4 addresses to instances launched in the subnet.\")\n        sys.exit(1)\n\n    print(\"Making Security Group\")\n    vpc = ec2_client.Vpc(vpc_id)\n    sg_exists = False\n    for sg in vpc.security_groups.all():\n        if sg.group_name == SECURITY_GROUP_NAME:\n            security_group_id = sg.id\n            sg_exists = True\n            print(\"  %s already exists\"%SECURITY_GROUP_NAME)\n    if not sg_exists:\n        security_group_id = make_security_group(vpc).id\n        time.sleep(30)\n\n    instances = []\n    try:\n        # If we want to preserve instances, do not have them self-destruct.\n        self_destruct = not cl_args.saveinstances\n        for target in targetlist:\n            instances.append(\n                create_client_instance(ec2_client, target,\n                                       security_group_id, subnet_id,\n                                       self_destruct)\n            )\n\n        # Install and launch client scripts in parallel\n        #-------------------------------------------------------------------------------\n        print(\"Uploading and running test script in parallel: %s\"%cl_args.test_script)\n        print(\"Output routed to log files in %s\"%log_dir)\n        # (Advice: always use Manager.Queue, never regular multiprocessing.Queue\n        # the latter has implementation flaws that deadlock it in some circumstances)\n        manager = Manager()\n        outqueue = manager.Queue()\n        inqueue = manager.Queue()\n\n        # launch as many processes as clients to test\n        num_processes = len(targetlist)\n        jobs = [] #keep a reference to current procs\n\n\n        # initiate process execution\n        client_process_args=(fab_config, inqueue, outqueue, log_dir)\n        for i in range(num_processes):\n            p = mp.Process(target=test_client_process, args=client_process_args)\n            jobs.append(p)\n            p.daemon = True  # kills subprocesses if parent is killed\n            p.start()\n\n        # fill up work queue\n        for ii, target in enumerate(targetlist):\n            inqueue.put((ii, instances[ii].id, target))\n\n        # add SENTINELs to end client processes\n        for i in range(num_processes):\n            inqueue.put(SENTINEL)\n        print('Waiting on client processes', end='')\n        for p in jobs:\n            while p.is_alive():\n                p.join(5 * 60)\n                # Regularly print output to keep Travis happy\n                print('.', end='')\n                sys.stdout.flush()\n        print()\n        # add SENTINEL to output queue\n        outqueue.put(SENTINEL)\n\n        # clean up\n        local_repo_clean(local_cxn, log_dir)\n\n        # print and save summary results\n        results_file = open(log_dir+'/results', 'w')\n        outputs = list(iter(outqueue.get, SENTINEL))\n        outputs.sort(key=lambda x: x[0])\n        failed = False\n        results_msg = \"\"\n        for outq in outputs:\n            ii, target, status = outq\n            if status == Status.FAIL:\n                failed = True\n                with open(log_dir+'/'+'%d_%s.log'%(ii,target['name']), 'r') as f:\n                    print(target['name'] + \" test failed. Test log:\")\n                    print(f.read())\n            results_msg = results_msg + '%d %s %s\\n'%(ii, target['name'], status)\n            results_file.write('%d %s %s\\n'%(ii, target['name'], status))\n        print(results_msg)\n        if len(outputs) != num_processes:\n            failed = True\n            failure_message = 'FAILURE: Some target machines failed to run and were not tested. ' +\\\n                'Tests should be rerun.'\n            print(failure_message)\n            results_file.write(failure_message + '\\n')\n        results_file.close()\n\n        if failed:\n            sys.exit(1)\n\n    finally:\n        cleanup(cl_args, instances, targetlist, log_dir)\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "letstest/pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"letstest\"\ndynamic = [\"version\"]\ndescription = \"Test Certbot on different AWS images\"\nreadme = \"README.md\"\nlicense = \"Apache-2.0\"\nrequires-python = \">=3.10\"\nauthors = [\n    { name = \"Certbot Project\" },\n]\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Topic :: Internet :: WWW/HTTP\",\n    \"Topic :: Security\",\n]\ndependencies = [\n    \"boto3\",\n    \"botocore\",\n    # The API from Fabric 2.0+ is used instead of the 1.0 API.\n    \"fabric>=2\",\n    \"pyyaml\",\n]\n\n[project.scripts]\nletstest = \"letstest.multitester:main\"\n\n[project.urls]\nHomepage = \"https://github.com/certbot/certbot\"\n\n[tool.setuptools.packages.find]\ninclude = [\"letstest\"]\n"
  },
  {
    "path": "letstest/scripts/bootstrap_os_packages.sh",
    "content": "#!/bin/sh\n#\n# Install OS dependencies for test farm tests.\n#\n# This does not include the dependencies needed to build cryptography. See\n# https://cryptography.io/en/latest/installation/#building-cryptography-on-linux\n\nset -ex  # Work even if somebody does \"sh thisscript.sh\".\n\nerror() {\n    echo \"$@\"\n}\n\nif [ -f /etc/debian_version ]; then\n  sudo apt-get update || error apt-get update hit problems but continuing anyway...\n\n  PYENV_DEPS=\"make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev \\\n              wget curl llvm libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev \\\n              liblzma-dev git\"\n  ALL_DEPS=\"libaugeas-dev $PYENV_DEPS\"\n  sudo DEBIAN_FRONTEND=noninteractive apt-get install -y $ALL_DEPS\nelif [ -f /etc/redhat-release ]; then\n  # the \"codeready builder\" repository must be enabled to install the\n  # augeas-devel package needed to compile newer versions of python-augeas\n  sudo yum config-manager --set-enabled crb\n  PYENV_DEPS=\"gcc make patch zlib-devel bzip2 bzip2-devel readline-devel sqlite sqlite-devel \\\n              openssl-devel tk-devel libffi-devel xz-devel git\"\n  ALL_DEPS=\"augeas-devel $PYENV_DEPS\"\n\n  if yum list installed \"httpd\" >/dev/null 2>&1; then\n    ALL_DEPS=\"mod_ssl $ALL_DEPS\"\n  fi\n\n  sudo yum install -y $ALL_DEPS\nfi\n"
  },
  {
    "path": "letstest/scripts/test_apache2.sh",
    "content": "#!/bin/bash -ex\n\n# $OS_TYPE $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL\n# are dynamically set at execution\n\nif [ \"$OS_TYPE\" = \"ubuntu\" ]\nthen\n    CONFFILE=/etc/apache2/sites-available/000-default.conf\n    sudo apt-get update\n    sudo DEBIAN_FRONTEND=noninteractive apt-get -y --no-upgrade install apache2 curl\n    # For apache 2.4, set up ServerName\n    sudo sed -i '/ServerName/ s/#ServerName/ServerName/' $CONFFILE\n    sudo sed -i '/ServerName/ s/www.example.com/'$PUBLIC_HOSTNAME'/' $CONFFILE\nelif [ \"$OS_TYPE\" = \"centos\" ]\nthen\n    CONFFILE=/etc/httpd/conf/httpd.conf\n    sudo setenforce 0 || true #disable selinux\n    sudo yum -y install httpd mod_ssl\n    sudo service httpd start\n    sudo mkdir -p /var/www/$PUBLIC_HOSTNAME/public_html\n    sudo chmod -R oug+rwx /var/www\n    sudo chmod -R oug+rw /etc/httpd\n    sudo echo '<html><head><title>foo</title></head><body>bar</body></html>' > /var/www/$PUBLIC_HOSTNAME/public_html/index.html\n    sudo mkdir /etc/httpd/sites-available #certbot requires this...\n    sudo mkdir /etc/httpd/sites-enabled #certbot requires this...\n    #sudo echo \"IncludeOptional sites-enabled/*.conf\" >> /etc/httpd/conf/httpd.conf\n    sudo echo \"\"\"\n<VirtualHost *:80>\n    ServerName $PUBLIC_HOSTNAME\n    DocumentRoot /var/www/$PUBLIC_HOSTNAME/public_html\n    ErrorLog /var/www/$PUBLIC_HOSTNAME/error.log\n    CustomLog /var/www/$PUBLIC_HOSTNAME/requests.log combined\n</VirtualHost>\"\"\" >> /etc/httpd/conf.d/$PUBLIC_HOSTNAME.conf\n    #sudo cp /etc/httpd/sites-available/$PUBLIC_HOSTNAME.conf /etc/httpd/sites-enabled/\nfi\n\n# Run certbot-apache2.\ncd letsencrypt\n\necho \"Bootstrapping dependencies...\"\nsudo letstest/scripts/bootstrap_os_packages.sh\n\n# Install pyenv\ncurl https://pyenv.run | bash\nexport PYENV_ROOT=\"$HOME/.pyenv\"\nexport PATH=\"$PYENV_ROOT/bin:$PATH\"\neval \"$(pyenv init --path)\"\neval \"$(pyenv init -)\"\n\n# Install and configure Python\npyenv install 3.10\npyenv shell 3.10\n\ntools/venv.py -e acme -e certbot -e certbot-apache -e certbot-ci tox\nPEBBLE_LOGS=\"acme_server.log\"\nPEBBLE_URL=\"https://localhost:14000/dir\"\n# We configure Pebble to use port 80 for http-01 validation rather than an\n# alternate port because:\n#   1) It allows us to test with Apache configurations that are more realistic\n#   and closer to the default configuration on various OSes.\n#   2) As of writing this, Certbot's Apache plugin requires there to be an\n#   existing virtual host for the port used for http-01 validation.\nvenv/bin/run_acme_server --http-01-port 80 > \"${PEBBLE_LOGS}\" 2>&1 &\n\nDumpPebbleLogsOnFailure() {\n    exit_status=\"$?\"\n    if [ \"$exit_status\" != 0 ] && [ -f \"${PEBBLE_LOGS}\" ] ; then\n        echo \"Pebble's logs were:\"\n        cat \"${PEBBLE_LOGS}\"\n    fi\n    exit \"$exit_status\"\n}\ntrap DumpPebbleLogsOnFailure EXIT\n\nfor n in $(seq 1 150) ; do\n    if curl --insecure \"${PEBBLE_URL}\" 2>/dev/null; then\n        break\n    else\n        echo \"waiting for pebble\"\n        sleep 1\n    fi\ndone\nif ! curl --insecure \"${PEBBLE_URL}\" 2>/dev/null; then\n  echo \"timed out waiting for pebble to start\"\n  DumpPebbleLogs\n  exit 1\nfi\n\nsudo \"venv/bin/certbot\" -v --debug --text --agree-tos --no-verify-ssl \\\n                   --renew-by-default --redirect --register-unsafely-without-email \\\n                   --domain \"${PUBLIC_HOSTNAME}\" --server \"${PEBBLE_URL}\"\n\nif ! grep -q SSLSessionTickets /etc/letsencrypt/options-ssl-apache.conf; then\n    echo \"modern TLS options were not used\"\n    exit 1\nfi\n\nif [ \"$OS_TYPE\" = \"ubuntu\" ] ; then\n    export SERVER=\"${PEBBLE_URL}\"\n    \"venv/bin/tox\" -e apacheconftest\nelse\n    echo Not running hackish apache tests on $OS_TYPE\nfi\n"
  },
  {
    "path": "letstest/scripts/version.py",
    "content": "#!/usr/bin/env python\n\"\"\"Get the current Certbot version number.\n\nProvides a simple utility for determining the Certbot version number\n\n\"\"\"\nfrom __future__ import print_function\n\nfrom os.path import abspath\nfrom os.path import dirname\nfrom os.path import join\nimport re\n\n\ndef certbot_version(letstest_scripts_dir):\n    \"\"\"Return the version number stamped in certbot/__init__.py.\"\"\"\n    return re.search('''^__version__ = ['\"](.+)['\"].*''',\n                     file_contents(join(dirname(dirname(letstest_scripts_dir)),\n                                        'certbot',\n                                        'src',\n                                        'certbot',\n                                        '__init__.py')),\n                     re.M).group(1)\n\n\ndef file_contents(path):\n    with open(path) as file:\n        return file.read()\n\n\nif __name__ == '__main__':\n    print(certbot_version(dirname(abspath(__file__))))\n"
  },
  {
    "path": "letstest/setup.py",
    "content": "from setuptools import setup\n\nversion = '5.5.0.dev0'\n\nsetup(\n    version=version,\n)\n"
  },
  {
    "path": "letstest/targets/targets.yaml",
    "content": "# These images are located in us-east-1.\n#\n# All machines must currently use x86_64 since Pebble does not currently\n# publish images for other architectures.\n\ntargets:\n  #-----------------------------------------------------------------------------\n  # Ubuntu\n  # These AMI were found on https://cloud-images.ubuntu.com/locator/ec2/.\n  - ami: ami-045a47a3b15302634\n    name: ubuntu24.04\n    type: ubuntu\n    virt: hvm\n    user: ubuntu\n  - ami: ami-0fc5d935ebf8bc3bc\n    name: ubuntu22.04\n    type: ubuntu\n    virt: hvm\n    user: ubuntu\n  #-----------------------------------------------------------------------------\n  # Debian\n  # These AMI were found on https://wiki.debian.org/Cloud/AmazonEC2Image.\n  - ami: ami-0f238cd7c96d866ad\n    name: debian12\n    type: ubuntu\n    virt: hvm\n    user: admin\n  #-----------------------------------------------------------------------------\n  # CentOS\n  # These AMI were found on https://centos.org/download/aws-images/.\n  - ami: ami-013db6d0c7167e046\n    name: centos9stream\n    type: centos\n    virt: hvm\n    user: ec2-user\n"
  },
  {
    "path": "linter_plugin.py",
    "content": "\"\"\"\nCertbot PyLint plugin.\n\nThe built-in ImportChecker of Pylint does a similar job to ForbidStandardOsModule to detect\ndeprecated modules. You can check its behavior as a reference to what is coded here.\nSee https://github.com/PyCQA/pylint/blob/b20a2984c94e2946669d727dbda78735882bf50a/pylint/checkers/imports.py#L287\nSee https://docs.pytest.org/en/latest/writing_plugins.html\n\"\"\"\nimport os.path\n\nfrom pylint.checkers import BaseChecker\n\n# Modules whose file is matching one of these paths can import the os module.\nALLOWLIST_PATHS = [\n    '/acme/src/acme/',\n    '/certbot-ci/',\n    '/certbot-compatibility-test/',\n]\n\n\nclass ForbidStandardOsModule(BaseChecker):\n    \"\"\"\n    This checker ensures that standard os module (and submodules) is not imported by certbot\n    modules. Otherwise an 'os-module-forbidden' error will be registered for the faulty lines.\n    \"\"\"\n\n    name = 'forbid-os-module'\n    msgs = {\n        'E5001': (\n            'Forbidden use of os module, certbot.compat.os must be used instead',\n            'os-module-forbidden',\n            'Some methods from the standard os module cannot be used for security reasons on '\n            'Windows: the safe wrapper certbot.compat.os must be used instead in Certbot.'\n        )\n    }\n    priority = -1\n\n    def visit_import(self, node):\n        os_used = any(name for name in node.names if name[0] == 'os' or name[0].startswith('os.'))\n        if os_used and not _check_disabled(node):\n            self.add_message('os-module-forbidden', node=node)\n\n    def visit_importfrom(self, node):\n        if node.modname == 'os' or node.modname.startswith('os.') and not _check_disabled(node):\n            self.add_message('os-module-forbidden', node=node)\n\n\ndef register(linter):\n    \"\"\"Pylint hook to auto-register this linter\"\"\"\n    linter.register_checker(ForbidStandardOsModule(linter))\n\n\ndef _check_disabled(node):\n    module = node.root()\n    return any(path for path in ALLOWLIST_PATHS\n               if os.path.normpath(path) in os.path.normpath(module.file))\n"
  },
  {
    "path": "mypy.ini",
    "content": "[mypy]\nignore_missing_imports = True\nwarn_unused_ignores = True\nshow_error_codes = True\ndisallow_untyped_defs = True\nstrict_equality = True\n\n# Using stricter settings here is being tracked by\n# https://github.com/certbot/certbot/issues/9647.\n[mypy-*._internal.tests.*]\n# By default, mypy prints notes without erroring about any type annotations it\n# finds in untyped function bodies when check_untyped_defs is false. Disabling\n# this \"error\" code removes this visual noise.\ndisable_error_code = annotation-unchecked\ndisallow_untyped_defs = False\n"
  },
  {
    "path": "newsfragments/.gitignore",
    "content": "!.gitignore\n"
  },
  {
    "path": "newsfragments/10584.changed",
    "content": "The certbot.ocsp module has been deprecated and will be removed in the next major release. This is not a change to Certbot's OCSP functionality. The code is just being removed from Certbot's public API.\n"
  },
  {
    "path": "pytest.ini",
    "content": "# This file isn't used while testing packages in tools/_release.sh so any\n# settings we want to also change there must be added to the release script\n# directly.\n[pytest]\n# Warnings being triggered by our plugins using deprecated features in\n# acme/certbot should be fixed by having our plugins no longer using the\n# deprecated code rather than adding them to the list of ignored warnings here.\n# Fixing things in this way prevents us from shipping packages raising our own\n# deprecation warnings and gives time for plugins that don't use the deprecated\n# API to propagate, especially for plugins packaged as an external snap, before\n# we release breaking changes.\n#\n# 1) In v2.28.0, google-api-core added an annoying message that the current python's EOL\n#    is coming up. We deprecate python versions on schedule, so mostly this is just an\n#    annoyance for our own tests, and can probably be silenced forever.\nfilterwarnings =\n    error\n    ignore:You are using a Python version (.*) which Google will stop supporting:FutureWarning\n"
  },
  {
    "path": "ruff.toml",
    "content": "line-length = 100\n\nextend-exclude = ['tools', 'letstest']\n\n[lint]\n# Check for PEP 585 style annotations to prevent regressions in\n# https://github.com/certbot/certbot/issues/10195\nextend-select = [\"UP006\"]\n# Skip bare `except` rules (`E722`).\n# Skip ambiguous variable name (`E741`).\nignore = [\"E722\", \"E741\",]\n"
  },
  {
    "path": "snap/hooks/configure",
    "content": "#!/bin/sh -e\n\nexit 0\n"
  },
  {
    "path": "snap/hooks/prepare-plug-plugin",
    "content": "#!/bin/sh -e\n\n# Workaround for a very old snapctl binary on the host connecting to the wrong socket and crashing.\n# Prefer an up-to-date snapctl from the core or snapd snaps, if they exist. We ask users to install\n# the core snap in the Certbot installation instructions.\n# See https://github.com/certbot/certbot/issues/8922, https://bugs.launchpad.net/snapd/+bug/1933392\nSNAPCTL_CORE=\"/snap/core/current/usr/bin/snapctl\"\nSNAPCTL_SNAPD=\"/snap/snapd/current/usr/bin/snapctl\"\nSNAPCTL=\"snapctl\"\nif [ -x $SNAPCTL_CORE ]; then\n    SNAPCTL=$SNAPCTL_CORE\nelif [ -x $SNAPCTL_SNAPD ]; then\n    SNAPCTL=$SNAPCTL_SNAPD\nfi\n\nif [ \"$($SNAPCTL get trust-plugin-with-root)\" = \"ok\" ]; then\n    # allow the connection, but reset config to allow for other slots to go through this auth flow\n    $SNAPCTL unset trust-plugin-with-root\n    exit 0\nelse\n    echo \"Only connect this interface if you trust the plugin author to have root on the system.\"\n    echo \"Run \\`snap set $SNAP_NAME trust-plugin-with-root=ok\\` to acknowledge this and then run this command again to perform the connection.\"\n    echo \"If that doesn't work, you may need to remove all certbot-dns-* plugins from the system, then try installing the certbot snap again.\"\n    exit 1\nfi\n"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: certbot\nsummary: Automatically configure HTTPS using Let's Encrypt\ndescription: |\n The objective of Certbot, Let's Encrypt, and the ACME (Automated\n Certificate Management Environment) protocol is to make it possible\n to set up an HTTPS server and have it automatically obtain a\n browser-trusted certificate, without any human intervention. This is\n accomplished by running a certificate management agent on the web\n server.\n\n This agent is used to:\n   - Automatically prove to the Let's Encrypt CA that you control the website\n   - Obtain a browser-trusted certificate and set it up on your web server\n   - Keep track of when your certificate is going to expire, and renew it\n   - Help you revoke the certificate if that ever becomes necessary.\nconfinement: classic\nbase: core24\ngrade: stable\nadopt-info: certbot\n\nenvironment:\n  AUGEAS_LENS_LIB: \"$SNAP/usr/share/augeas/lenses/dist\"\n  CERTBOT_SNAPPED: \"True\"\n  # This is needed to help openssl find its legacy provider on architectures\n  # where we cannot use cryptography's pre-built wheels. See\n  # https://github.com/certbot/certbot/issues/10055.\n  OPENSSL_MODULES: \"$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/ossl-modules\"\n  PATH: \"$SNAP/bin:$SNAP/usr/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games\"\n  # Disable FIPS mode detection. See\n  # https://git.launchpad.net/ubuntu/+source/openssl/tree/debian/patches/fips/crypto-Add-kernel-FIPS-mode-detection.patch?h=applied/ubuntu/noble\n  # for more on this flag, and https://github.com/certbot/certbot/issues/10044\n  # for more on the issue.\n  #\n  # This is needed because the Python + OpenSSL bundled in core24 don't include\n  # an OpenSSL FIPS provider, which causes crashes on host systems with OpenSSL\n  # 1.1.1f (e.g. Ubuntu Pro 20.04). For some reason, core24's OpenSSL also looks\n  # in a non-standard location for the provider, which also causes crashes on\n  # systems with OpenSSL 3.x (e.g. RHEL 9). If you need FIPS functionality in\n  # certbot, install via pip.\n  OPENSSL_FORCE_FIPS_MODE: \"0\"\n\napps:\n  certbot:\n    command: bin/python3 -s $SNAP/bin/certbot\n  renew:\n    command: bin/python3 -s $SNAP/bin/certbot -q renew\n    daemon: oneshot\n    # Run approximately twice a day with randomization\n    timer: 00:00~24:00/2\n\n\nparts:\n  certbot:\n    plugin: python\n    source: .\n    python-packages:\n      - git+https://github.com/certbot/python-augeas.git@certbot-patched\n      - ./acme\n      - ./certbot\n      - ./certbot-apache\n      - ./certbot-nginx\n    stage:\n      - -usr/lib/python3.12/sitecustomize.py # maybe unnecessary\n      # Old versions of this file used to unstage\n      # lib/python3.8/site-packages/augeas.py to avoid conflicts between\n      # python-augeas 0.5.0 which was pinned in snap-constraints.txt and\n      # our python-augeas fork which creates an auto-generated cffi file at\n      # the same path. Since we've combined things in one part and removed the\n      # python-augeas pinning, unstaging this file had a different, unintended\n      # effect so we now stage the file to keep the auto-generated cffi file.\n    stage-packages:\n      - libaugeas0\n      - libpython3.12-dev\n      # This library included so openssl has a legacy provider available at\n      # runtime when we are unable to use cryptography's pre-built wheels. See\n      # https://github.com/certbot/certbot/issues/10055.\n      - libssl3t64\n      # added to stage python:\n      - libpython3-stdlib\n      - libpython3.12-stdlib\n      - libpython3.12-minimal\n      - python3-pip\n      - python3-wheel\n      - python3-venv\n      - python3-minimal\n      - python3-pkg-resources\n      - python3.12-minimal\n    # To build cryptography and cffi if needed\n    build-packages:\n      - gcc\n      - git\n      - libaugeas-dev\n      - build-essential\n      - libssl-dev\n      - libffi-dev\n      - python3-dev\n      - cargo\n      - pkg-config\n    build-environment:\n      # We set this environment variable while building to try and increase the\n      # stability of fetching the rust crates needed to build the cryptography\n      # library.\n      - CARGO_NET_GIT_FETCH_WITH_CLI: \"true\"\n      - PARTS_PYTHON_VENV_ARGS: --upgrade\n      # Constraints are passed through the environment variable PIP_CONSTRAINTS instead of using the\n      # parts.[part_name].constraints option available in snapcraft.yaml when the Python plugin is\n      # used. This is done to let these constraints be applied not only on the certbot package\n      # build, but also on any isolated build that pip could trigger when building wheels for\n      # dependencies. See https://github.com/certbot/certbot/pull/8443 for more info.\n      - PIP_CONSTRAINT: $CRAFT_PART_SRC/snap-constraints.txt\n      # This is a workaround for\n      # https://github.com/pypa/setuptools/issues/5039 forcing pip to use\n      # modern build conventions even in the absence of pyproject.toml files.\n      - PIP_USE_PEP517: \"true\"\n    override-build: |\n      python3 -m venv \"${CRAFT_PART_INSTALL}\"\n      \"${CRAFT_PART_INSTALL}/bin/python3\" \"${CRAFT_PART_SRC}/tools/pipstrap.py\"\n      craftctl default\n      # Workaround for misconfigured pyvenv.cfg; see discussion on snapcraft forum for more info\n      # https://forum.snapcraft.io/t/upgrading-classic-snap-to-core24-using-snapcraft-8-3-causes-python-3-12-errors-at-runtime/\n      sed -i \"${CRAFT_PART_INSTALL}/pyvenv.cfg\" \\\n          -e 's@^home = '\"${CRAFT_PART_INSTALL}\"'/usr/bin$@home = /snap/certbot/current/usr/bin@g'\n    override-pull: |\n      craftctl default\n      grep -v python-augeas \"${CRAFT_PART_SRC}/tools/requirements.txt\" >> \"${CRAFT_PART_SRC}/snap-constraints.txt\"\n      craftctl set version=$(grep -oP \"__version__ = '\\K.*(?=')\" \"${CRAFT_PART_SRC}/certbot/src/certbot/__init__.py\")\n    build-attributes:\n      # https://snapcraft.io/docs/how-to-classic#fix-linter-warnings-by-patching-elf-binaries\n      - enable-patchelf\n  shared-metadata:\n    plugin: dump\n    source: .\n    override-pull: |\n      craftctl default\n      mkdir -p certbot-metadata\n      grep -oP \"__version__ = '\\K.*(?=')\" $CRAFT_PART_SRC/certbot/src/certbot/__init__.py > certbot-metadata/certbot-version.txt\n    stage: [certbot-metadata/certbot-version.txt]\n\nplugs:\n  plugin:\n    interface: content\n    content: certbot-1\n    target: $SNAP/certbot-plugin\n\nslots:\n  certbot-metadata:\n    interface: content\n    content: metadata-1\n    read: [$SNAP/certbot-metadata]\n"
  },
  {
    "path": "tests/modification-check.py",
    "content": "#!/usr/bin/env python\n\"\"\"Ensures there have been no changes to important certbot-auto files.\"\"\"\n\nimport hashlib\nimport os\n\n# Relative to the root of the Certbot repo, these files are expected to exist\n# and have the SHA-256 hashes contained in this dictionary. These hashes were\n# taken from our v1.14.0 tag which was the last release we intended to make\n# changes to certbot-auto.\n#\n# We can delete this script and the files under letsencrypt-auto-source when\n# we're comfortable breaking any old certbot-auto scripts that haven't updated\n# to the last version of the script yet.  See\n# https://opensource.eff.org/eff-open-source/pl/65geri7c4tr6iqunc1rpb3mpna and\n# letsencrypt-auto-source/README.md for more info.\nEXPECTED_FILES = {\n    os.path.join('letsencrypt-auto-source', 'letsencrypt-auto'):\n        'b997e3608526650a08e36e682fc3bf0c29903c06fa5ba4cc49308c43832450c2',\n    os.path.join('letsencrypt-auto-source', 'letsencrypt-auto.sig'):\n        '61c036aabf75da350b0633da1b2bef0260303921ecda993455ea5e6d3af3b2fe',\n}\n\n\ndef find_repo_root():\n    return os.path.dirname(os.path.dirname(os.path.realpath(__file__)))\n\n\ndef sha256_hash(filename):\n    hash_object = hashlib.sha256()\n    with open(filename, 'rb') as f:\n        hash_object.update(f.read())\n    return hash_object.hexdigest()\n\n\ndef main():\n    repo_root = find_repo_root()\n    for filename, expected_hash in EXPECTED_FILES.items():\n        filepath = os.path.join(repo_root, filename)\n        assert sha256_hash(filepath) == expected_hash, f'unexpected changes to {filepath}'\n    print('All certbot-auto files have correct hashes.')\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/_release.sh",
    "content": "#!/bin/bash -xe\n# Release packages to PyPI\n\nif [ \"$RELEASE_DIR\" = \"\" ]; then\n    echo Please run this script through the tools/release.sh wrapper script or set the environment\n    echo variable RELEASE_DIR to the directory where the release should be built.\n    exit 1\nfi\n\nExitWarning() {\n    exit_status=\"$?\"\n    if [ \"$exit_status\" != 0 ]; then\n        # Don't print each command before executing it because it will disrupt\n        # the desired output.\n        set +x\n        echo '******************************'\n        echo '*                            *'\n        echo '* THE RELEASE SCRIPT FAILED! *'\n        echo '*                            *'\n        echo '******************************'\n        set -x\n    fi\n    exit \"$exit_status\"\n}\n\ntrap ExitWarning EXIT\n\nversion=\"$1\"\necho Releasing production version \"$version\"...\nnextversion=\"$2\"\nRELEASE_BRANCH=\"candidate-$version\"\n\n# If RELEASE_GPG_KEY isn't set, determine the key to use.\nif [ \"$RELEASE_GPG_KEY\" = \"\" ]; then\n    TRUSTED_KEYS=\"\n        BF6BCFC89E90747B9A680FD7B6029E8500F7DB16\n        86379B4F0AF371B50CD9E5FF3402831161D1D280\n        20F201346BF8F3F455A73F9A780CC99432A28621\n        F2871B4152AE13C49519111F447BF683AA3B26C3\n    \"\n    for key in $TRUSTED_KEYS; do\n        if gpg --with-colons --card-status | grep -q \"$key\"; then\n            RELEASE_GPG_KEY=\"$key\"\n            break\n        fi\n    done\n    if [ \"$RELEASE_GPG_KEY\" = \"\" ]; then\n        echo A trusted PGP key was not found on your PGP card.\n        exit 1\n    fi\nfi\n\n# Needed to fix problems with git signatures and pinentry\nexport GPG_TTY=$(tty)\n\n# port for a local Python Package Index (used in testing)\nPORT=${PORT:-1234}\n\n# subpackages to be released (the way the script thinks about them)\nSUBPKGS_NO_CERTBOT=\"acme certbot-apache certbot-nginx certbot-dns-cloudflare \\\n                    certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy \\\n                    certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns \\\n                    certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 \\\n                    certbot-dns-sakuracloud\"\nSUBPKGS=\"certbot $SUBPKGS_NO_CERTBOT\"\n# certbot_compatibility_test is not packaged because:\n# - it is not meant to be used by anyone else than Certbot devs\n# - it causes problems when running pytest - the latter tries to\n#   run everything that matches test*, while there are no unittests\n#   there\n\ntag=\"v$version\"\nbuilt_package_dir=\"packages\"\nif [ -d \"$built_package_dir\" ]; then\n    echo \"there shouldn't already be a $built_package_dir directory!\"\n    echo \"if it's not important, maybe delete it and try running the script again?\"\n    exit 1\nfi\ngit tag --delete \"$tag\" || true\n\ntmpvenv=$(mktemp -d)\npython3 -m venv \"$tmpvenv\"\n. $tmpvenv/bin/activate\n# update packaging tools to their pinned versions\ntools/pip_install.py build towncrier uv virtualenv\n\nroot_without_le=\"$version.$$\"\nroot=\"$RELEASE_DIR/le.$root_without_le\"\n\necho \"Cloning into fresh copy at $root\"  # clean repo = no artifacts\ngit clone . $root\ngit rev-parse HEAD\ncd $root\nif [ \"$RELEASE_BRANCH\" != \"candidate-$version\" ] ; then\n    git branch -f \"$RELEASE_BRANCH\"\nfi\ngit checkout \"$RELEASE_BRANCH\"\n\n# Update changelog. `--yes` automatically clears out older newsfragments,\n# and all of towncrier's changes are staged for commit when it's done\ntowncrier build --version \"$version\" --yes\ngit commit -m \"Update changelog for $version release\"\n\nSetVersion() {\n    ver=\"$1\"\n    # bumping Certbot's version number is done differently\n    for pkg_dir in $SUBPKGS_NO_CERTBOT certbot-compatibility-test certbot-ci letstest\n    do\n      setup_file=\"$pkg_dir/setup.py\"\n      if [ $(grep -c '^version' \"$setup_file\") != 1 ]; then\n        echo \"Unexpected count of version variables in $setup_file\"\n        exit 1\n      fi\n      sed -i \"s/^version.*/version = '$ver'/\" $pkg_dir/setup.py\n    done\n    init_file=\"certbot/src/certbot/__init__.py\"\n    if [ $(grep -c '^__version' \"$init_file\") != 1 ]; then\n      echo \"Unexpected count of __version variables in $init_file\"\n      exit 1\n    fi\n    sed -i \"s/^__version.*/__version__ = '$ver'/\" \"$init_file\"\n\n    git add $SUBPKGS certbot-compatibility-test certbot-ci letstest\n}\n\nSetVersion \"$version\"\n\n# Unset CERTBOT_OLDEST to prevent wheels from being built improperly due to\n# conditionals like the one found in certbot-dns-dnsimple's setup.py file.\nunset CERTBOT_OLDEST\necho \"Preparing sdists and wheels\"\nfor pkg_dir in $SUBPKGS\ndo\n  cd $pkg_dir\n\n  rm -rf build dist\n  # It's not strictly necessary, but using uv to install build dependencies speeds things up a\n  # little bit.\n  python -m build --installer uv\n\n  cd -\ndone\n\nmkdir \"$built_package_dir\"\nfor pkg_dir in $SUBPKGS\ndo\n  mv \"$pkg_dir\"/dist/* \"$built_package_dir\"\ndone\n\n\ncd \"$built_package_dir\"\necho \"Generating checksum file and signing it\"\nsha256sum *.tar.gz > SHA256SUMS\ngpg -u \"$RELEASE_GPG_KEY\" --detach-sign --armor --sign --digest-algo sha256 SHA256SUMS\ngit add *.tar.gz SHA256SUMS*\n\necho \"Installing packages to generate documentation\"\n# cd .. is NOT done on purpose: we make sure that all subpackages are\n# installed from local archives rather than current directory (repo root)\nVIRTUALENV_NO_DOWNLOAD=1 virtualenv ../venv\n. ../venv/bin/activate\npip install -U setuptools\npip install -U pip\n\n# This creates a string like \"acme==a.b.c certbot==a.b.c ...\" which can be used\n# with pip to ensure the correct versions of our packages installed.\nsubpkgs_with_version=\"\"\nfor pkg in $SUBPKGS; do\n    subpkgs_with_version=\"$subpkgs_with_version $pkg==$version\"\ndone\n\n# Now, use our local archives. Disable cache so we get the correct packages even if\n# we (or our dependencies) have conditional dependencies implemented with if\n# statements in setup.py and we have cached wheels lying around that would cause\n# those ifs to not be evaluated.\npython ../tools/pip_install.py \\\n  --no-cache-dir \\\n  --find-links . \\\n  $subpkgs_with_version\ncd ~-\n\n# get a snapshot of the CLI help for the docs\n# We set CERTBOT_DOCS to use dummy values in example user-agent string.\nCERTBOT_DOCS=1 certbot --help all > certbot/docs/cli-help.txt\njws --help > acme/docs/jws-help.txt\n\ndeactivate\n\n\ngit add certbot/docs/cli-help.txt\nwhile ! git commit --gpg-sign=\"$RELEASE_GPG_KEY\" -m \"Release $version\"; do\n    echo \"Unable to sign the release commit using git.\"\n    echo \"You may have to configure git to use gpg by running:\"\n    echo 'git config --global gpg.program $(command -v gpg)'\n    read -p \"Press enter to try signing again.\"\ndone\ngit tag --local-user \"$RELEASE_GPG_KEY\" --sign --message \"Release $version\" \"$tag\"\n\ngit rm --cached -r \"$built_package_dir\"\ngit commit -m \"Remove built packages from git\"\n\nif [ \"$RELEASE_BRANCH\" = candidate-\"$version\" ] ; then\n    SetVersion \"$nextversion\".dev0\n    git commit -m \"Bump version to $nextversion\"\nfi\n"
  },
  {
    "path": "tools/docker/Dockerfile",
    "content": "#base image\nFROM python:3.12-alpine3.20 AS certbot\n\nENTRYPOINT [ \"certbot\" ]\nEXPOSE 80 443\nVOLUME /etc/letsencrypt /var/lib/letsencrypt\nWORKDIR /opt/certbot\n\n# Copy certbot code\nCOPY CHANGELOG.md README.rst src/\nCOPY tools tools\nCOPY acme src/acme\nCOPY certbot src/certbot\n\n# Install certbot runtime dependencies\nRUN apk add --no-cache --virtual .certbot-deps \\\n        libffi \\\n        libssl3 \\\n        openssl \\\n        ca-certificates \\\n        binutils\n\n# We set this environment variable and install git while building to try and\n# increase the stability of fetching the rust crates needed to build the\n# cryptography library\nARG CARGO_NET_GIT_FETCH_WITH_CLI=true\n# Install certbot from sources\n#\n# For some reason, setting the CARGO_LOG and CARGO_TERM_VERBOSE environment\n# variables and -v/--verbose flags on pip seems to help cryptography builds not\n# hang when building Docker images for other architectures using QEMU. See\n# https://github.com/certbot/certbot/issues/10020. This may hopefully also help\n# us to get more information about the problem to aid further debugging.\nRUN apk add --no-cache --virtual .build-deps \\\n        gcc \\\n        linux-headers \\\n        openssl-dev \\\n        musl-dev \\\n        libffi-dev \\\n        python3-dev \\\n        cargo \\\n        git \\\n        pkgconfig \\\n    && CARGO_LOG=trace CARGO_TERM_VERBOSE=true python tools/pip_install.py \\\n            --no-cache-dir -vvv \\\n            --editable src/acme \\\n            --editable src/certbot \\\n    && apk del .build-deps \\\n    && rm -rf ${HOME}/.cargo\n\n#static definition for making a plugin, but beware that\n#using this layer definition will cause collisions if you make\n#extensive use of the cache.\nFROM certbot AS certbot-plugin\nCOPY --from=plugin-src . /opt/certbot/src/plugin\nRUN python tools/pip_install.py --no-cache-dir --editable /opt/certbot/src/plugin\n"
  },
  {
    "path": "tools/docker/README.md",
    "content": "Running Certbot in Docker \n=========================\n\nDocker is an amazingly simple and quick way to obtain a certificate. However, this mode of operation is unable to install certificates automatically or configure your webserver, because our installer plugins cannot reach your webserver from inside the Docker container.\n \n**Most users should install Certbot by following the installation instructions at https://certbot.eff.org/instructions. You should only use Docker if you are sure you know what you are doing (you understand [volumes](https://docs.docker.com/storage/volumes/)) and have a good reason to do so, such as following the [one service per container rule](https://docs.docker.com/config/containers/multi-service_container/).**\n\nFor more information, please read [Certbot - Alternative 1: Docker](https://eff-certbot.readthedocs.io/en/latest/install.html#alternative-1-docker).\n\n\nCertbot Docker Tools\n======================\n\nGoal\n----\n\nThis code is used to build and deploy new versions of the Certbot and Certbot\nDNS plugin Docker images to Docker Hub.\n\nHigh-level behavior\n-------------------\n\nRunning `./build.sh <TAG> all` causes the Docker images to be built for all \nsupported architectures. The generated images are stored in the local docker image cache.\n\nRunning `./test.sh <TAG> all` loads images from the docker image cache\nand runs a test command to validate the image contents.\n\nRunning `./deploy_images.sh <TAG> all` will push the previously generated images \nto Docker Hub.  The `<TAG>` argument is an identifier applied to all docker\nimages and manifests. It may be something like `nightly` or `v2.3.2`. If \nthe tag is a version stamp greater than `v2.0.0`, then a `latest` tag will \nalso be generated and pushed to the docker hub repo. \n\nRunning `./deploy_manifests.sh <TAG> all` will add multiarch manifests to \nDocker Hub. This command assumes that `./deploy_images.sh <TAG> all` has\nbeen previously run with the same tag.\n\nConfiguration\n-------------\n\nTo run these scripts you need:\n\n1. A computer with Docker installed and the Docker daemon running. You probably \ndon't want to use the docker snap as these scripts have failed when using that \nin the past.\n2. To be logged into Docker Hub with an account able to push to the Certbot and \nCertbot DNS Docker images on Docker Hub. Altering the value of `DOCKER_HUB_ORG` \nin `lib/common` will allow you to push to your own account for testing.\n"
  },
  {
    "path": "tools/docker/build.sh",
    "content": "#!/bin/bash\nset -euxo pipefail\n\n# This script builds docker images for certbot and each dns plugin from the\n# local Certbot source files. Results are stored in the docker image cache\n\n# Usage: \n#       ./build.sh <tag> all \n#       ./build.sh <tag> <architectures> \n#   The <tag> argument is used to identify the code version (e.g v2.3.1) or type of build\n#   (e.g. nightly). This will be used when saving images to the docker image cache.\n#   The argument \"all\" will build all known architectures. Alternatively, the\n#   user may provide a comma separated list of architectures drawn from the\n#   known architectures. Known architectures include amd64, arm32v6, and arm64v8.\n\nsource \"$(realpath \"$(dirname \"${BASH_SOURCE[0]}\")\")/lib/common\"\n\nParseArgs \"$@\"\n\n#jump to root, matching popd handed by Cleanup on EXIT via trap\npushd \"${REPO_ROOT}\"\n\n# Set trap here, as the popd won't work as expected if invoked prior to pushd\ntrap Cleanup EXIT\n# Create the builder\nCreateBuilder\nInstallMultiarchSupport\n\n\nBuildAndLoadByArch() {\n    TAG_ARCH=$1\n    docker buildx build --target certbot --builder certbot_builder \\\n        --platform \"$(arch2platform \"$TAG_ARCH\")\" \\\n        -f \"${WORK_DIR}/Dockerfile\" \\\n        -t \"${DOCKER_HUB_ORG}/certbot:${TAG_ARCH}-${TAG_VER}\" \\\n        --load \\\n        .\n    for plugin in \"${CERTBOT_PLUGINS[@]}\"; do\n        docker buildx build --target certbot-plugin --builder certbot_builder \\\n            --platform \"$(arch2platform \"$TAG_ARCH\")\" \\\n            --build-context plugin-src=\"${REPO_ROOT}/certbot-${plugin}\" \\\n            -f \"${WORK_DIR}/Dockerfile\" \\\n            -t \"${DOCKER_HUB_ORG}/${plugin}:${TAG_ARCH}-${TAG_VER}\" \\\n            --load \\\n            .\n    done\n}\n\n# In principle, there is a better way to do with by using `docker buildx bake`\n# instead of a for-loop. However, issues have been found in the results\n# of such a build. See the branch buildx-bake and\n# https://github.com/certbot/certbot/issues/9587.\n\nfor ARCH in \"${REQUESTED_ARCH_ARRAY[@]}\"; do\n    BuildAndLoadByArch \"$ARCH\"\ndone"
  },
  {
    "path": "tools/docker/deploy_images.sh",
    "content": "#!/bin/bash\nset -euxo pipefail\n\n# This script takes docker images in the local docker cache and pushes them to\n# Docker Hub.\n\n# Usage: \n#       ./deploy_images.sh <TAG> all\n#       ./deploy_images.sh <TAG> <architectures>\n#   The <TAG> argument is an identifier applied to all docker images and manifests.\n#   It may be something like `nightly` or `v2.3.2`. If the tag is a version\n#   stamp greater than v2.0.0, then a `latest` tag will also be generated and\n#   pushed to the docker hub repo.\n#   The argument \"all\" will push all known architectures. Alternatively, the\n#   user may provide a comma separated list of architectures drawn from the\n#   known architectures. Known architectures include amd64, arm32v6, and arm64v8.\n\nsource \"$(realpath $(dirname \"${BASH_SOURCE[0]}\"))/lib/common\"\n\nParseArgs \"$@\"\n\n#jump to root, matching popd handed by Cleanup on EXIT via trap\npushd \"${REPO_ROOT}\"\n\n# Set trap here, as the popd won't work as expected if invoked prior to pushd\ntrap popd EXIT\n\nREGISTRY_SPEC=\"${DOCKER_HUB_ORG}/\"\n\nDeployImage() {\n    IMAGE_NAME=$1\n    TAG_ARCH=$2\n    docker push \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-${TAG_VER}\"\n    if [[ \"${TAG_VER}\" =~ ^v([2-9]|[1-9][0-9]+)\\.[0-9]+\\.[0-9]+$ ]]; then\n        docker tag \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-${TAG_VER}\" \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-latest\"\n        docker push \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-latest\"\n    fi\n}\n\n\nfor TAG_ARCH in \"${REQUESTED_ARCH_ARRAY[@]}\"; do\n    DeployImage certbot \"$TAG_ARCH\"\n    for PLUGIN in \"${CERTBOT_PLUGINS[@]}\"; do\n        DeployImage \"$PLUGIN\" \"$TAG_ARCH\"\n    done\ndone"
  },
  {
    "path": "tools/docker/deploy_manifests.sh",
    "content": "#!/bin/bash\nset -euxo pipefail\n\n# This script generates multi-arch manifests for images previously pushed to\n# Docker Hub via deploy_images.sh\n\n# Usage: \n#       ./deploy_manifest.sh <TAG> all\n#       ./deploy_manifest.sh <TAG> <architectures>\n#   The <TAG> argument is an identifier applied to all docker images and manifests.\n#   It may be something like `nightly` or `v2.3.2`. If the tag is a version\n#   stamp greater than v2.0.0, then a `latest` tag will also be generated and\n#   pushed to the docker hub repo.\n#   The argument \"all\" will push all know architectures. Alternatively, the\n#   user may provide a comma separated list of architectures drawn from the\n#   known architectures. Know architectures include amd64, arm32v6, and arm64v8.\n\n\nsource \"$(realpath $(dirname ${BASH_SOURCE[0]}))/lib/common\"\n\nParseArgs \"$@\"\n\n#jump to root, matching popd handed by Cleanup on EXIT via trap\npushd \"${REPO_ROOT}\"\n\n# Set trap here, as the popd won't work as expected if invoked prior to pushd\ntrap popd EXIT\n\nREGISTRY_SPEC=\"${DOCKER_HUB_ORG}/\"\n\nDeployManifest() {\n    IMAGE_NAME=$1\n    \n    SRC_IMAGES=\"\"\n    for TAG_ARCH in \"${REQUESTED_ARCH_ARRAY[@]}\"; do\n        SRC_IMAGES+=\"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-${TAG_VER} \"\n    done\n    docker buildx imagetools create -t \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_VER}\" $SRC_IMAGES\n\n    if [[ \"${TAG_VER}\" =~ ^v([2-9]|[1-9][0-9]+)\\.[0-9]+\\.[0-9]+$ ]]; then\n        docker buildx imagetools create -t \"${REGISTRY_SPEC}${IMAGE_NAME}:latest\" $SRC_IMAGES\n    fi\n}\n\nDeployManifest certbot\nfor PLUGIN in \"${CERTBOT_PLUGINS[@]}\"; do\n    DeployManifest \"$PLUGIN\"\ndone"
  },
  {
    "path": "tools/docker/lib/common",
    "content": "#!/bin/bash\nset -euxo pipefail\n\n# Current supported architectures\nexport ALL_TARGET_ARCH=(amd64 arm32v6 arm64v8)\n\n# Name of the Certbot Docker organizaation on GitHub. After creating\n# repositories with the same names (e.g. \"certbot\", \"dns-dnsmadeeasy\", etc.)\n# using a different account on Docker Hub, you can change this value to have\n# the scripts modify those Docker repositories rather than the repositories for\n# the official Certbot Docker images.\nexport DOCKER_HUB_ORG=\"certbot\"\n\n# List of Certbot plugins\nexport CERTBOT_PLUGINS=(\n    \"dns-dnsmadeeasy\"\n    \"dns-dnsimple\"\n    \"dns-ovh\"\n    \"dns-cloudflare\"\n    \"dns-digitalocean\"\n    \"dns-google\"\n    \"dns-luadns\"\n    \"dns-nsone\"\n    \"dns-rfc2136\"\n    \"dns-route53\"\n    \"dns-gehirn\"\n    \"dns-linode\"\n    \"dns-sakuracloud\"\n)\n\n# WORK_DIR is two levels above this file\nexport WORK_DIR=\"$(realpath \"$(dirname \"${BASH_SOURCE[0]}\")/..\")\"\n# REPO_ROOT is two levels above that\nexport REPO_ROOT=\"$(realpath \"${WORK_DIR}/../..\")\"\n\n# Converts input architecture identifier to the platform specification\n# understood by `docker build buildx --platform <specification>`.\n# Usage: arch2platform [arm64|arm32v6|arm64v8]\n# If the input is not recognized, an error is returned\narch2platform() {\n    REQUESTED_ARCH=\"${1}\"\n    case $REQUESTED_ARCH in\n        amd64)\n            echo \"linux/amd64\"\n            ;;\n        arm32v6)\n            echo \"linux/arm/v6\"\n            ;;\n        arm64v8)\n            echo \"linux/arm64\"\n            ;;\n        *)\n            return 1\n            ;;\n    esac\n}\n\nParseArgs() {\n    export TAG_VER=\"$1\"\n    if [ -z \"$TAG_VER\" ]; then\n        echo \"We cannot tag Docker images with an empty string!\" >&2\n        exit 1\n    fi\n    ARCH_LIST=\"$2\"\n    if [ -z \"$ARCH_LIST\" ]; then\n        echo \"Architectures must be specified!\" >&2\n        exit 1\n    fi\n\n    local IFS=\",\"\n    # Handle the special value \"all\"\n    if [[ \"${ARCH_LIST}\" == \"all\" ]]; then\n        # Replace with comma separated\n        ARCH_LIST=\"${ALL_TARGET_ARCH[*]}\"\n    fi\n\n    # Turn arch list into an array\n    read -ra REQUESTED_ARCH_ARRAY <<< \"$ARCH_LIST\"\n    # And make sure all individual elements are in the list of all known architectures\n    for REQUESTED_ARCH in \"${REQUESTED_ARCH_ARRAY[@]}\"; do\n        local IFS=\" \"\n        if [[ ! \" ${ALL_TARGET_ARCH[*]} \" =~ \" ${REQUESTED_ARCH} \" ]]; then\n            echo \"unknown architecture identifier: ${REQUESTED_ARCH}\" >&2\n            exit 1\n        fi\n    done\n    export REQUESTED_ARCH_ARRAY\n}\n\n\n# Function for use with trap in the primary scripts to remove the\n# docker builder and restore the original directory\nCleanup() {\n    docker buildx rm certbot_builder || true\n    popd\n}\n\n# add binfmt tools to the docker environment, with integration into the new builder instance\nInstallMultiarchSupport() {\n    docker run --privileged --rm tonistiigi/binfmt --install all\n}\n\n# Function to create a docker builder using the buildkit docker-container\n# driver\nCreateBuilder() {\n    # just in case the env is not perfectly clean, remove any old instance of the builder\n    docker buildx rm certbot_builder || true\n    # create the builder instance\n    #\n    # BUILDKIT_STEP_LOG_MAX_* environment variables are set to prevent docker\n    # from truncating build logs that can be useful during debugging. See\n    # https://github.com/docker/buildx/issues/484#issuecomment-749352728\n    docker buildx create --name certbot_builder --driver docker-container \\\n        --driver-opt=network=host --driver-opt env.BUILDKIT_STEP_LOG_MAX_SIZE=-1 \\\n        --driver-opt env.BUILDKIT_STEP_LOG_MAX_SPEED=-1 --bootstrap\n}\n"
  },
  {
    "path": "tools/docker/test.sh",
    "content": "#!/bin/bash\nset -euxo pipefail\n\n# This script tests certbot docker and certbot dns plugin images.\n\n# Usage: \n#       ./test.sh <tag> all\n#       ./test.sh <tag> <architectures>\n#   The <tag> argument is used to identify the code version (e.g v2.3.1) or type of build\n#   (e.g. nightly). This will be used when saving images to the docker image cache.\n#   The argument \"all\" will build all know architectures. Alternatively, the\n#   user may provide a comma separated list of architectures drawn from the\n#   known architectures. Known architectures include amd64, arm32v6, and arm64v8.\n\nsource \"$(realpath $(dirname \"${BASH_SOURCE[0]}\"))/lib/common\"\n\nParseArgs \"$@\"\n\n#jump to root, matching popd handed by Cleanup on EXIT via trap\npushd \"${REPO_ROOT}\"\n\n# Set trap here, as the popd won't work as expected if invoked prior to pushd\ntrap popd EXIT\n\nInstallMultiarchSupport\n\n\nREGISTRY_SPEC=\"${DOCKER_HUB_ORG}/\"\n\nTestImage() {\n    IMAGE_NAME=$1\n    TAG_ARCH=$2\n    TAG_VER=$3\n    docker run --rm \"${REGISTRY_SPEC}${IMAGE_NAME}:${TAG_ARCH}-${TAG_VER}\" plugins --prepare\n}\n\n\nfor TAG_ARCH in \"${REQUESTED_ARCH_ARRAY[@]}\"; do\n    TestImage certbot \"$TAG_ARCH\" \"$TAG_VER\"\n    for PLUGIN in \"${CERTBOT_PLUGINS[@]}\"; do\n        TestImage \"$PLUGIN\" \"$TAG_ARCH\" \"$TAG_VER\"\n    done\ndone"
  },
  {
    "path": "tools/extract_changelog.py",
    "content": "#!/usr/bin/env python\nfrom __future__ import print_function\n\nimport os\nimport re\nimport sys\n\nCERTBOT_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))\n\nNEW_SECTION_PATTERN = re.compile(r'^##\\s*[\\d.]+\\s*-\\s*[\\d-]+$')\n\n\ndef main():\n    version = sys.argv[1]\n    if version.endswith('.dev0'):\n        version = version[:-5]\n\n    section_pattern = re.compile(r'^##\\s*{0}\\s*-\\s*.*$'\n                                 .format(version.replace('.', '\\\\.')))\n\n    with open(os.path.join(CERTBOT_ROOT, 'certbot', 'CHANGELOG.md')) as file_h:\n        lines = file_h.read().splitlines()\n\n    changelog = []\n\n    i = 0\n    while i < len(lines):\n        if section_pattern.match(lines[i]):\n            i = i + 2\n            while i < len(lines):\n                if NEW_SECTION_PATTERN.match(lines[i]):\n                    break\n                changelog.append(lines[i])\n                i = i + 1\n        i = i + 1\n\n    print('\\n'.join(changelog))\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/finish_release.py",
    "content": "#!/usr/bin/env python\n\"\"\"\nPost-release script to publish artifacts created from Azure Pipelines.\n\nThis currently includes:\n\n* Moving snaps from the beta channel to the stable channel\n\nSetup:\n - Install the snapcraft command line tool and log in to a privileged account.\n   - https://snapcraft.io/docs/installing-snapcraft\n   - Use the command `snapcraft login` to log in.\n\nRun:\n\npython tools/finish_release.py\n\nTesting:\n\nThis script can be safely run between releases. When this is done, the script\nshould execute successfully.\n\n\"\"\"\n\nimport argparse\nimport glob\nimport os.path\nimport re\nimport subprocess\nimport sys\n\nfrom azure.devops.connection import Connection\n\n# Path to the root directory of the Certbot repository containing this script\nREPO_ROOT = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))\n# This list contains the names of all Certbot DNS plugins. We used to have a\n# CloudXNS plugin and since it's possible devs still have that directory\n# locally, we filter it out here. If it's included in this list, this script\n# will crash later when it fails to find a CloudXNS snap on the snap store with\n# the current version since we no longer build it.\nPLUGIN_SNAPS = [os.path.basename(path)\n                for path in glob.glob(os.path.join(REPO_ROOT, 'certbot-dns-*'))\n                if not path.endswith('certbot-dns-cloudxns')]\n# This list contains the name of all Certbot snaps that should be published to\n# the stable channel.\nALL_SNAPS = ['certbot'] + PLUGIN_SNAPS\n# This is the count of the architectures currently supported by our snaps used\n# for sanity checking.\nSNAP_ARCH_COUNT = 3\n\n\ndef parse_args(args):\n    \"\"\"Parse command line arguments.\n\n    :param args: command line arguments with the program name removed. This is\n        usually taken from sys.argv[1:].\n    :type args: `list` of `str`\n\n    :returns: parsed arguments\n    :rtype: argparse.Namespace\n\n    \"\"\"\n    # Use the file's docstring for the help text and don't let argparse reformat it.\n    parser = argparse.ArgumentParser(description=__doc__,\n                                     formatter_class=argparse.RawDescriptionHelpFormatter)\n    return parser.parse_args(args)\n\n\ndef assert_logged_into_snapcraft():\n    \"\"\"Confirms that snapcraft is logged in to an account.\n\n    :raises SystemExit: if the command snapcraft is unavailable or it\n        isn't logged into an account\n\n    \"\"\"\n    cmd = 'snapcraft whoami'.split()\n    try:\n        subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL,\n                       stderr=subprocess.DEVNULL, universal_newlines=True)\n    except (subprocess.CalledProcessError, OSError):\n        print(\"Please make sure that the command line tool snapcraft is\")\n        print(\"installed and that you have logged in to an account by running\")\n        print(\"'snapcraft login'. If that fails, your credentials may have expired\")\n        print(\"and you should run `snapcraft logout` followed by 'snapcraft login'.\")\n        sys.exit(1)\n\n\ndef get_snap_revisions(snap, channel, version):\n    \"\"\"Finds the revisions for the snap and version in the given channel.\n\n    If you call this function without being logged in with snapcraft, it\n    will hang with no output.\n\n    :param str snap: the name of the snap on the snap store\n    :param str channel: snap channel to pull revisions from\n    :param str version: snap version number, e.g. 1.7.0\n\n    :returns: list of revision numbers\n    :rtype: `list` of `str`\n\n    :raises subprocess.CalledProcessError: if the snapcraft command\n        fails\n\n    :raises AssertionError: if the expected snaps are not found\n\n    \"\"\"\n    print('Getting revision numbers for', snap, version)\n    cmd = ['snapcraft', 'status', snap]\n    process = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True)\n    pattern = f'\\\\s+{channel}\\\\s+{version}\\\\s+(\\\\d+)\\\\s*'\n    revisions = re.findall(pattern, process.stdout, re.MULTILINE)\n    assert len(revisions) == SNAP_ARCH_COUNT, f'Unexpected number of snaps found for {channel} {snap} {version} (expected {SNAP_ARCH_COUNT}, found {len(revisions)})'\n    return revisions\n\n\ndef promote_snaps(snaps, source_channel, version, progressive_percentage=None):\n    \"\"\"Promotes the given snaps from source_channel to the stable channel.\n\n    If the snaps have already been released to the stable channel, this\n    function will try to release them again which has no effect.\n\n    :param snaps: snap package names to be promoted\n    :type snaps: `list` of `str`\n    :param str source_channel: snap channel to promote from\n    :param str version: the version number that should be found in the\n        candidate channel, e.g. 1.7.0\n    :param progressive_percentage: specifies the percentage of a progressive\n        deployment\n    :type progressive_percentage: int or None\n\n    :raises SystemExit: if the command snapcraft is unavailable or it\n        isn't logged into an account\n\n    :raises subprocess.CalledProcessError: if a snapcraft command fails\n        for another reason\n\n    \"\"\"\n    assert_logged_into_snapcraft()\n    for snap in snaps:\n        revisions = get_snap_revisions(snap, source_channel, version)\n        # The loop below is kind of slow, so let's print some output about what\n        # it is doing.\n        print('Releasing', snap, 'snaps to the stable channel')\n        for revision in revisions:\n            cmd = ['snapcraft', 'release', snap, revision, 'stable']\n            if progressive_percentage:\n                cmd.extend(f'--progressive {progressive_percentage}'.split())\n            try:\n                subprocess.run(cmd, check=True, stdout=subprocess.PIPE, universal_newlines=True)\n            except subprocess.CalledProcessError as e:\n                print(\"The command\", f\"'{' '.join(cmd)}'\", \"failed.\")\n                print(\"The output printed to stdout was:\")\n                print(e.stdout)\n                raise\n\ndef fetch_version_number(major_version=None):\n    \"\"\"Retrieve version number for release from Azure Pipelines\n\n    :param major_version: only consider releases for the specified major\n        version\n    :type major_version: str or None\n\n    :returns: version number\n\n    \"\"\"\n    # Create a connection to the azure org\n    organization_url = 'https://dev.azure.com/certbot'\n    connection = Connection(base_url=organization_url)\n\n    # Find the build artifacts\n    build_client = connection.clients.get_build_client()\n    builds = build_client.get_builds('certbot', definitions='3')\n    for build in builds:\n        version = build_client.get_build('certbot', build.id).source_branch.split('v')[1]\n        if major_version is None or version.split('.')[0] == major_version:\n            return version\n    raise ValueError('Release not found on Azure Pipelines!')\n\ndef main(args):\n    parsed_args = parse_args(args)\n    version = fetch_version_number()\n    promote_snaps(ALL_SNAPS, 'beta', version)\n\nif __name__ == \"__main__\":\n    main(sys.argv[1:])\n"
  },
  {
    "path": "tools/oldest_constraints.txt",
    "content": "# This file was generated by tools/pinning/oldest/repin.sh and can be updated using\n# that script.\napacheconfig==0.3.2 ; python_version == \"3.10\"\nasn1crypto==0.24.0 ; python_version == \"3.10\"\nastroid==4.0.3 ; python_version == \"3.10\"\nattrs==25.4.0 ; python_version == \"3.10\"\nbeautifulsoup4==4.14.3 ; python_version == \"3.10\"\nboto3==1.20.34 ; python_version == \"3.10\"\nbotocore==1.23.34 ; python_version == \"3.10\"\ncachetools==5.5.2 ; python_version == \"3.10\"\ncertifi==2026.1.4 ; python_version == \"3.10\"\ncffi==1.14.1 ; python_version == \"3.10\"\nchardet==3.0.4 ; python_version == \"3.10\"\ncloudflare==2.19.0 ; python_version == \"3.10\"\ncolorama==0.4.6 ; (sys_platform == \"win32\" or platform_system == \"Windows\") and python_version == \"3.10\"\nconfigargparse==1.5.3 ; python_version == \"3.10\"\nconfigobj==5.0.6 ; python_version == \"3.10\"\ncoverage==7.13.3 ; python_version == \"3.10\"\ncryptography==43.0.0 ; python_version == \"3.10\"\ncython==0.29.37 ; python_version == \"3.10\"\ndill==0.4.1 ; python_version == \"3.10\"\ndistlib==0.4.0 ; python_version == \"3.10\"\ndistro==1.0.1 ; python_version == \"3.10\"\ndns-lexicon==3.15.1 ; python_version == \"3.10\"\ndnspython==2.6.1 ; python_version == \"3.10\"\nexceptiongroup==1.3.1 ; python_version == \"3.10\"\nexecnet==2.1.2 ; python_version == \"3.10\"\nfilelock==3.20.3 ; python_version == \"3.10\"\nfuncsigs==0.4 ; python_version == \"3.10\"\ngoogle-api-python-client==1.6.5 ; python_version == \"3.10\"\ngoogle-auth==2.16.0 ; python_version == \"3.10\"\nhttplib2==0.9.2 ; python_version == \"3.10\"\nidna==2.6 ; python_version == \"3.10\"\niniconfig==2.3.0 ; python_version == \"3.10\"\nipaddress==1.0.16 ; python_version == \"3.10\"\nisort==7.0.0 ; python_version == \"3.10\"\njmespath==0.10.0 ; python_version == \"3.10\"\njosepy==2.2.0 ; python_version == \"3.10\"\njsonlines==4.0.0 ; python_version == \"3.10\"\njsonpickle==4.1.1 ; python_version == \"3.10\"\nlibrt==0.7.8 ; python_version == \"3.10\" and platform_python_implementation != \"PyPy\"\nmccabe==0.7.0 ; python_version == \"3.10\"\nmypy-extensions==1.1.0 ; python_version == \"3.10\"\nmypy==1.19.1 ; python_version == \"3.10\"\nndg-httpsclient==0.3.2 ; python_version == \"3.10\"\noauth2client==4.1.3 ; python_version == \"3.10\"\npackaging==26.0 ; python_version == \"3.10\"\nparsedatetime==2.6 ; python_version == \"3.10\"\npathspec==1.0.4 ; python_version == \"3.10\"\npbr==1.8.0 ; python_version == \"3.10\"\npip==26.0 ; python_version == \"3.10\"\nplatformdirs==4.5.1 ; python_version == \"3.10\"\npluggy==1.6.0 ; python_version == \"3.10\"\nply==3.4 ; python_version == \"3.10\"\npy==1.11.0 ; python_version == \"3.10\"\npyasn1-modules==0.4.1 ; python_version == \"3.10\"\npyasn1==0.4.8 ; python_version == \"3.10\"\npycparser==2.14 ; python_version == \"3.10\"\npygments==2.19.2 ; python_version == \"3.10\"\npylint==4.0.4 ; python_version == \"3.10\"\npyopenssl==25.0.0 ; python_version == \"3.10\"\npyotp==2.9.0 ; python_version == \"3.10\"\npyparsing==3.0.0 ; python_version == \"3.10\"\npyrfc3339==1.0 ; python_version == \"3.10\"\npytest-cov==7.0.0 ; python_version == \"3.10\"\npytest-xdist==3.8.0 ; python_version == \"3.10\"\npytest==9.0.2 ; python_version == \"3.10\"\npython-augeas==0.5.0 ; python_version == \"3.10\"\npython-dateutil==2.9.0.post0 ; python_version == \"3.10\"\npython-digitalocean==1.15.0 ; python_version == \"3.10\"\npytz==2025.2 ; python_version == \"3.10\"\npywin32==311 ; python_version == \"3.10\" and sys_platform == \"win32\"\npyyaml==6.0.3 ; python_version == \"3.10\"\nrequests-file==3.0.1 ; python_version == \"3.10\"\nrequests==2.25.1 ; python_version == \"3.10\"\nrsa==4.9.1 ; python_version == \"3.10\"\nruff==0.15.0 ; python_version == \"3.10\"\ns3transfer==0.5.2 ; python_version == \"3.10\"\nsetuptools==80.10.2 ; python_version == \"3.10\"\nsix==1.16.0 ; python_version == \"3.10\"\nsoupsieve==2.8.3 ; python_version == \"3.10\"\ntldextract==5.3.1 ; python_version == \"3.10\"\ntomli==2.4.0 ; python_version == \"3.10\"\ntomlkit==0.14.0 ; python_version == \"3.10\"\ntox==3.28.0 ; python_version == \"3.10\"\ntypes-httplib2==0.31.2.20260125 ; python_version == \"3.10\"\ntypes-pyrfc3339==2.0.1.20250825 ; python_version == \"3.10\"\ntypes-python-dateutil==2.9.0.20260124 ; python_version == \"3.10\"\ntypes-pywin32==311.0.0.20251008 ; python_version == \"3.10\"\ntypes-requests==2.31.0.6 ; python_version == \"3.10\"\ntypes-setuptools==80.10.0.20260124 ; python_version == \"3.10\"\ntypes-urllib3==1.26.25.14 ; python_version == \"3.10\"\ntyping-extensions==4.15.0 ; python_version == \"3.10\"\nuritemplate==3.0.1 ; python_version == \"3.10\"\nurllib3==1.26.5 ; python_version == \"3.10\"\nuv==0.9.28 ; python_version == \"3.10\"\nvirtualenv==20.36.1 ; python_version == \"3.10\"\nwheel==0.46.3 ; python_version == \"3.10\"\n"
  },
  {
    "path": "tools/pinning/DESIGN.md",
    "content": "# Certbot dependency pinning\n\nAs described in the developer guide, we try to pin Certbot's dependencies to\nwell tested versions in almost all cases. Pinning Python dependencies across\ndifferent environments like this is actually quite tricky though and the files\nunder this directory make a somewhat reasonable, best effort approach to solve\nthis problem.\n\n## Python packaging background\n\n### Sdists and wheels\n\nPython projects are most commonly distributed on [PyPI](https://pypi.org/) as\neither a [source\ndistribution](https://packaging.python.org/glossary/#term-Source-Distribution-or-sdist)\nor a [wheel](https://packaging.python.org/glossary/#term-Wheel). Wheels don't\npresent a problem for us pinning dependencies because they offer a well defined\nformat where the dependencies of a package can be easily parsed.\n\nSource distributions or \"sdists\" are a problem though because they can contain\narbitrary Python code that must be run to determine the dependencies of the\npackage. This code could theoretically do anything, but it most commonly\ninspects the environment it is running in and changes the project's\ndependencies based the environment. As of writing this, we even do this in some\nof Certbot's packages as you can see\n[here](https://github.com/certbot/certbot/blob/8b610239bfcf7aac06f6e36d09a5abba3ba87047/certbot-dns-cloudflare/setup.py#L15-L27).\nThis is a problem because it means the environment an sdist was run in affects\nthe dependencies it declares making it difficult for us to determine Certbot's\ndependencies for an arbitrary environment. Luckily, this is becoming less and\nless of an issue with the increasing use of wheels, however, as of writing\nthis, some of Certbot's dependencies are still only available as sdists on some\nplatforms.\n\n### Environment markers and pyproject.toml\n\nTwo other things have helped reduce the problems caused by sdists and are\nrelevant here. The first is the usage of [environment\nmarkers](https://www.python.org/dev/peps/pep-0496/) which allows a package to\nconsistently declare its conditional dependencies with a static string\nspecifying the conditions where a dependency is required instead of dynamically\ngenerating the list of required dependencies at runtime. This static string\nkeeps the package's declaration of its dependencies consistent across\nenvironments.\n\nThe other relatively recent change in Python packaging is the adoption of\n[pyproject.toml files](https://www.python.org/dev/peps/pep-0518/) which allows\nsdists to define their packages using a static file instead of a setup.py\nfile, which has historically been the norm.\nUsing a static file instead of arbitrary Python code makes it\nmuch easier for package declarations to be reliably interpreted. The\nintroduction of pyproject.toml also allows for the use of build systems other\nthan setuptools which becomes relevant in the next section of this doc.\n\n## Our pinning system\n\n### Overview\n\nThe files inside `tools/pinning` are used to generate Certbot's pinning files.\nThe files under `oldest` are used to generate the constraints file used for our\n\"oldest\" tests while `current` is used to generate the constraints used\neverywhere else. `common` includes shared files that are used for both sets of\npinnings.\n\nUnder `current` and `oldest`, there are two files as of writing this. One is a\npyproject.toml file for use with [Poetry](https://python-poetry.org/) while\nthe other is a script that can be run to regenerate pinnings. The\npyproject.toml file defines a Python package that depends on everything we want\nto pin. This file largely just depends on our own local packages, however,\nextra dependencies can be declared to further constrain package versions or to\ndeclare additional dependencies.\n\nThe reason we use Poetry is that it is somewhat unique among Python packaging\ntools in that when locking dependencies, it makes a best effort approach to do\nthis for all environments rather than just the current environment. This\nincludes recursively resolving dependencies declared through environment\nmarkers that are not relevant for the current platform. It also includes\nchecking all wheels and sdists of a package for dependencies when picking a\nspecific version of a package from PyPI. You can see this in action through\nthe inclusion of dependencies like pywin32 which we only have a dependency on\nfor Windows.\n\n### Potential problems\n\nAs of writing this, I'm aware of two potential problems with this pinning\nsystem. The first is largely described earlier in the doc which is the problem\nof sdists that use code to dynamically declare its dependencies. It's simply\nnot feasible to ensure this arbitrary Python code declares its dependencies in\nthe same way across all environments. Luckily, this is a largely a theoretical\nproblem and I'm aware of no issues with our current dependencies.\n\nThe second problem with this approach is that build dependencies are not\ntracked and pinned. To be clear, normal runtime dependencies like those\ndeclared in \"install_requires\" in setup.py files and even optional runtime\ndependencies like those declared in \"extras_require\" in setup.py are managed\nproperly. The problem here is with dependencies needed at build time when\ninitially installing a Python project. These are usually specified in\n\"setup_requires\" in setup.py or under \"requires\" in the \"build-system\" section\nof pyproject.toml files.\n\nUnfortunately, [tracking and pinning build dependencies seems to be a largely\nunsolved problem in Python right\nnow](https://discuss.python.org/t/pinning-build-dependencies/8363). Our tooling\nensures that when installing build dependencies when using our pinning files\nthat versions from the pinning files are used (see\nhttps://github.com/certbot/certbot/pull/8443 for more info about that), but I'm\nnot aware of any tool that automates the process of tracking and pinning down\nbuild dependencies. For now, if we find any unpinned build dependencies, we can\ndeclare a dependency on them in pyproject.toml. If a build dependency isn't\nincluded in the constraints file, pip will use the latest version available on\nPyPI.\n\n## Theoretical future work\n\nI think the system described above should work pretty well and I think it's\nmuch better than the system we had before where how to update things like our\n\"oldest\" pinnings was an open question. If we wanted to improve on this in the\nfuture though, I think things to consider would be:\n\n1. We could require that wheels are used for all of our dependencies. If a\n   wheel is not available for one of our dependencies, we could try to work\n   with upstream to change that or build it and host it locally for ourselves.\n   (If we do the latter, how to pin build dependencies when building the wheel\n   remains an open question.)\n2. We could only really try to pin our dependencies for certain environments.\n   This would be done by doing something like installing our packages in each\n   environment we care about and saving the output of a command like `pip\n   freeze`. With our use of snaps and Docker, this may be somewhat reasonable\n   because we could base them all on a common system like Ubuntu LTS, however,\n   it's not entirely trivial because we still have problems such as supporting\n   multiple CPU architectures and pinning dependencies for Windows. Alternative\n   development and test environments also wouldn't be fully supported.\n3. We could help build better tooling that solves some of the problems with\n   this approach or adopts it when it becomes available.\n"
  },
  {
    "path": "tools/pinning/common/export-pinned-dependencies.sh",
    "content": "#!/bin/bash\n# This script accepts a directory containing a pyproject.toml file configured\n# for use with poetry and generates and prints the pinned dependencies of that\n# file. Any dependencies on acme or those referencing certbot will be removed\n# from the output. The exported requirements are printed to stdout.\n#\n# For example, if a directory containing a pyproject.toml file for poetry is at\n# ../current, you could activate Certbot's developer environment and then run a\n# command like the following to generate requirements.txt for that environment:\n#   ./export-pinned-dependencies.sh ../current > requirements.txt\nset -euo pipefail\n\n# If this script wasn't given a command line argument, print usage and exit.\nif [ -z ${1+x} ]; then\n    echo \"Usage:\" >&2\n    echo \"$0 PYPROJECT_TOML_DIRECTORY [POETRY_ARGS]\" >&2\n    exit 1\nfi\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nWORK_DIR=\"$1\"\n\nif ! command -v poetry >/dev/null || [ $(poetry --version | grep -oE '[0-9]+\\.[0-9]+' | sed 's/\\.//') -lt 12 ]; then\n    echo \"Please install poetry 1.2+.\" >&2\n    echo \"You may need to recreate Certbot's virtual environment and activate it.\" >&2\n    exit 1\nfi\n\n# Old eggs can cause outdated dependency information to be used by poetry so we\n# delete them before generating the lock file. See\n# https://github.com/python-poetry/poetry/issues/4103 for more info.\nrm -rf ${REPO_ROOT}/*/*.egg-info\nrm -rf ${REPO_ROOT}/*/src/*.egg-info\n\ncd \"${WORK_DIR}\"\n\nif [ -f poetry.lock ]; then\n    rm poetry.lock\nfi\n\necho \"If this takes more than a few minutes, you may want to try clearing poetry's\" >&2\necho \"caches with a command like the following and running this script again:\" >&2\necho >&2\necho \"    poetry cache list | xargs -L1 poetry cache clear --all\" >&2\necho >&2\necho \"If that doesn't help, you can also try running this script with extra arguments\" >&2\necho 'for \"poetry lock\" on the command line such as -vvv to help see where poetry is' >&2\necho \"getting stuck.\" >&2\nextra_args=\"${*:2}\"\npoetry lock ${extra_args:+\"$extra_args\"} >&2\ntrap 'rm poetry.lock' EXIT\n\n# POETRY_WARNINGS_EXPORT is set to remove warning output about\n# poetry-plugin-export no longer being installed with poetry by default in the\n# future which we can ignore because we explicitly depend on the plugin\n# package.\n#\n# sed is then used to remove local packages from the output.\nPOETRY_WARNINGS_EXPORT=false poetry export --format constraints.txt --without-hashes | sed '/^acme @/d; /certbot/d;'\n"
  },
  {
    "path": "tools/pinning/current/pyproject.toml",
    "content": "[tool.poetry]\nname = \"certbot-pinner\"\nversion = \"0.1.0\"\ndescription = \"A simple project for pinning Certbot's dependencies using Poetry.\"\nauthors = [\"Certbot Project\"]\nlicense = \"Apache License 2.0\"\n\n[tool.poetry.dependencies]\npython = \"^3.10\"\n\n# Local dependencies\n# Any local packages that have dependencies on other local packages must be\n# listed below before the package it depends on. For instance, certbot depends\n# on acme so certbot must be listed before acme.\ncertbot-ci = {path = \"../../../certbot-ci\"}\ncertbot-compatibility-test = {path = \"../../../certbot-compatibility-test\"}\ncertbot-dns-cloudflare = {path = \"../../../certbot-dns-cloudflare\", extras = [\"docs\"]}\ncertbot-dns-digitalocean = {path = \"../../../certbot-dns-digitalocean\", extras = [\"docs\"]}\ncertbot-dns-dnsimple = {path = \"../../../certbot-dns-dnsimple\", extras = [\"docs\"]}\ncertbot-dns-dnsmadeeasy = {path = \"../../../certbot-dns-dnsmadeeasy\", extras = [\"docs\"]}\ncertbot-dns-gehirn = {path = \"../../../certbot-dns-gehirn\", extras = [\"docs\"]}\ncertbot-dns-google = {path = \"../../../certbot-dns-google\", extras = [\"docs\"]}\ncertbot-dns-linode = {path = \"../../../certbot-dns-linode\", extras = [\"docs\"]}\ncertbot-dns-luadns = {path = \"../../../certbot-dns-luadns\", extras = [\"docs\"]}\ncertbot-dns-nsone = {path = \"../../../certbot-dns-nsone\", extras = [\"docs\"]}\ncertbot-dns-ovh = {path = \"../../../certbot-dns-ovh\", extras = [\"docs\"]}\ncertbot-dns-rfc2136 = {path = \"../../../certbot-dns-rfc2136\", extras = [\"docs\"]}\ncertbot-dns-route53 = {path = \"../../../certbot-dns-route53\", extras = [\"docs\"]}\ncertbot-dns-sakuracloud = {path = \"../../../certbot-dns-sakuracloud\", extras = [\"docs\"]}\ncertbot-nginx = {path = \"../../../certbot-nginx\"}\ncertbot-apache = {path = \"../../../certbot-apache\", extras = [\"dev\"]}\ncertbot = {path = \"../../../certbot\", extras = [\"all\"]}\nacme = {path = \"../../../acme\", extras = [\"docs\", \"test\"]}\nletstest = {path = \"../../../letstest\"}\n\n# Extra dependencies\n# As of writing this, cython is a build dependency of pyyaml. Since there\n# doesn't appear to be a good way to automatically track down and pin build\n# dependencies in Python (see\n# https://discuss.python.org/t/how-to-pin-build-dependencies/8238), we list it\n# as a dependency here to ensure a version of cython is pinned for extra\n# stability.\n#\n# As of pyyaml 6.0.2, cython >= 3.0 is required for py >= 3.13, and < 3.0 for py < 3.13.\n# See https://github.com/yaml/pyyaml/pull/808/files.\ncython = [\n    { version = \"<3.0\", python = \"<3.13\" },\n    { version = \">=3.0\", python = \">=3.13\" }\n]\n\n# setuptools-rust is a build dependency of cryptography, and since we don't have\n# a great way of pinning build dependencies, we simply list it here to ensure a\n# working version. Note: if build dependencies of setuptools-rust break at some\n# point, it's probably worth enumerating and pinning them (and recursing to\n# THEIR build dependencies) as well.\nsetuptools-rust = \"*\"\n# pylint often adds new checks that we need to conform our code to when\n# upgrading our dependencies. To help control when this needs to be done, we\n# pin pylint to a compatible version here.\n#\n# If this pinning is removed, we may still need to add a lower bound for the\n# pylint version. See https://github.com/certbot/certbot/pull/9229.\npylint = \"3.3.3\"\n# mypy often adds new checks that we need to conform our code to when updating\n# dependencies. To help control when this needs to be done, we pin mypy to a\n# compatible version here.\nmypy = \"1.9.0\"\n\n# Branch 4.x of tox introduces backward incompatibility changes, so require a newer\n# version of tox to keep deterministic builds.\ntox = \">=4\"\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tools/pinning/current/repin.sh",
    "content": "#!/bin/bash\n# This script automates the process of updating Certbot's dependencies\n# including automatically updating the correct file. It is usually run without\n# any arguments, but if any are provided, they will be passed to Poetry.\n# Dependencies can be pinned to older versions by modifying pyproject.toml in\n# the same directory as this file.\nset -euo pipefail\n\nWORK_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null && pwd )\"\nCOMMON_DIR=\"$(dirname \"${WORK_DIR}\")/common\"\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nif ! RELATIVE_WORK_DIR=\"$(realpath --relative-to \"$REPO_ROOT\" \"$WORK_DIR\")\"; then\n    echo this script needs GNU coreutils to be first in your PATH rather than macOS/BSD versions\n    exit 1\nfi\nRELATIVE_SCRIPT_PATH=\"$RELATIVE_WORK_DIR/$(basename \"${BASH_SOURCE[0]}\")\"\nREQUIREMENTS_FILE=\"$REPO_ROOT/tools/requirements.txt\"\n\nPINNINGS=$(\"${COMMON_DIR}/export-pinned-dependencies.sh\" \"${WORK_DIR}\" \"$@\")\ncat << EOF > \"$REQUIREMENTS_FILE\"\n# This file was generated by $RELATIVE_SCRIPT_PATH and can be updated using\n# that script.\n#\n# It is normally used as constraints to pip, however, it has the name\n# requirements.txt so that is scanned by GitHub. See\n# https://docs.github.com/en/github/visualizing-repository-data-with-graphs/about-the-dependency-graph#supported-package-ecosystems\n# for more info.\nEOF\necho \"${PINNINGS}\" >> \"${REQUIREMENTS_FILE}\"\n"
  },
  {
    "path": "tools/pinning/oldest/pyproject.toml",
    "content": "# The purpose of this file is to help us test Certbot against the oldest\n# versions of our dependencies which we claim to support in our setup.py files.\n#\n# Security alerts about vulnerable packages in this file can be ignored since\n# they are only used during testing.\n#\n# Ideally, generating package pinnings based on our minimum allowed dependency\n# versions would be done automatically by tooling, but as of writing this, both\n# https://github.com/pypa/pip/issues/8085 and\n# https://github.com/python-poetry/poetry/issues/3527 remain unresolved.\n[tool.poetry]\nname = \"certbot-pinner\"\nversion = \"0.1.0\"\ndescription = \"A simple project for pinning Certbot's dependencies using Poetry.\"\nauthors = [\"Certbot Project\"]\nlicense = \"Apache License 2.0\"\n\n[tool.poetry.dependencies]\n# The Python version here should be kept in sync with the one used in our\n# oldest tests in tox.ini.\npython = \"<3.11 >= 3.10\"\n\n# Local dependencies\n# Any local packages that have dependencies on other local packages must be\n# listed below before the package it depends on. For instance, certbot depends\n# on acme so certbot must be listed before acme.\ncertbot-ci = {path = \"../../../certbot-ci\"}\ncertbot-dns-cloudflare = {path = \"../../../certbot-dns-cloudflare\"}\ncertbot-dns-digitalocean = {path = \"../../../certbot-dns-digitalocean\"}\ncertbot-dns-dnsimple = {path = \"../../../certbot-dns-dnsimple\"}\ncertbot-dns-dnsmadeeasy = {path = \"../../../certbot-dns-dnsmadeeasy\"}\ncertbot-dns-gehirn = {path = \"../../../certbot-dns-gehirn\"}\ncertbot-dns-google = {path = \"../../../certbot-dns-google\"}\ncertbot-dns-linode = {path = \"../../../certbot-dns-linode\"}\ncertbot-dns-luadns = {path = \"../../../certbot-dns-luadns\"}\ncertbot-dns-nsone = {path = \"../../../certbot-dns-nsone\"}\ncertbot-dns-ovh = {path = \"../../../certbot-dns-ovh\"}\ncertbot-dns-rfc2136 = {path = \"../../../certbot-dns-rfc2136\"}\ncertbot-dns-route53 = {path = \"../../../certbot-dns-route53\"}\ncertbot-dns-sakuracloud = {path = \"../../../certbot-dns-sakuracloud\"}\ncertbot-nginx = {path = \"../../../certbot-nginx\"}\ncertbot-apache = {path = \"../../../certbot-apache\", extras = [\"dev\"]}\ncertbot = {path = \"../../../certbot\", extras = [\"test\"]}\nacme = {path = \"../../../acme\", extras = [\"test\"]}\n\n# Oldest dependencies\n# We specify the oldest versions of our dependencies that we keep support for\n# below. These dependencies can be updated as desired to simplify or improve\n# Certbot or its development. If the dependency being updated is a direct\n# dependency of one of our own packages, the minimum required version of that\n# dependency should be updated in our setup.py files as well to communicate\n# this information to our users.\n\nConfigArgParse = \"1.5.3\"\napacheconfig = \"0.3.2\"\nasn1crypto = \"0.24.0\"\n# we were already ignoring debian 11 for our boto* versions; this uses the versions from ubuntu 22\nboto3 = \"1.20.34\"\nbotocore = \"1.23.34\"\ncffi = \"1.14.1\"\nchardet = \"3.0.4\"\ncloudflare = \"2.19\"\nconfigobj = \"5.0.6\"\ncryptography = \"43.0.0\"\ndistro = \"1.0.1\"\ndns-lexicon = \"3.15.1\"\ndnspython = \"2.6.1\"\nfuncsigs = \"0.4\"\ngoogle-api-python-client = \"1.6.5\"\ngoogle-auth = \"2.16.0\"\nhttplib2 = \"0.9.2\"\nidna = \"2.6\"\nipaddress = \"1.0.16\"\nndg-httpsclient = \"0.3.2\"\nparsedatetime = \"2.6\"\npbr = \"1.8.0\"\nply = \"3.4\"\npyOpenSSL = \"25.0.0\"\npyRFC3339 = \"1.0\"\npyasn1 = \"0.4.8\"\npycparser = \"2.14\"\npyparsing = \"3.0.0\"\npython-augeas = \"0.5.0\"\npython-digitalocean = \"1.15.0\"\nrequests = \"2.25.1\"\nsix = \"1.16.0\"\nurllib3 = \"1.26.5\"\n\n# Build dependencies\n# Since there doesn't appear to\n# doesn't appear to be a good way to automatically track down and pin build\n# dependencies in Python (see\n# https://discuss.python.org/t/how-to-pin-build-dependencies/8238), we list any\n# build dependencies here to ensure they're pinned for extra stability.\n\n# cython is a build dependency of pyyaml\n#\n#\n# As of pyyaml 6.0.2, cython >= 3.0 is required for py >= 3.13, and < 3.0 for py < 3.13.\n# See https://github.com/yaml/pyyaml/pull/808/files.\ncython = [\n    { version = \"<3.0\", python = \"<3.13\" },\n    { version = \">=3.0\", python = \">=3.13\" }\n]\n\n# Other dependencies\n# We add any dependencies that must be specified in this file for any another\n# reason below.\n\n[build-system]\nrequires = [\"poetry-core>=1.0.0\"]\nbuild-backend = \"poetry.core.masonry.api\"\n"
  },
  {
    "path": "tools/pinning/oldest/repin.sh",
    "content": "#!/bin/bash\n# This script automates the process of updating Certbot's dependencies\n# including automatically updating the correct file. It is usually run without\n# any arguments, but if any are provided, they will be passed to Poetry.\n# Dependencies can be pinned to older versions by modifying pyproject.toml in\n# the same directory as this file.\nset -euo pipefail\n\nWORK_DIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null && pwd )\"\nCOMMON_DIR=\"$(dirname \"${WORK_DIR}\")/common\"\nREPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nif ! RELATIVE_WORK_DIR=\"$(realpath --relative-to \"$REPO_ROOT\" \"$WORK_DIR\")\"; then\n    echo this script needs GNU coreutils to be first in your PATH rather than macOS/BSD versions\n    exit 1\nfi\nRELATIVE_SCRIPT_PATH=\"$RELATIVE_WORK_DIR/$(basename \"${BASH_SOURCE[0]}\")\"\nCONSTRAINTS_FILE=\"$REPO_ROOT/tools/oldest_constraints.txt\"\n\nPINNINGS=$(\"${COMMON_DIR}/export-pinned-dependencies.sh\" \"${WORK_DIR}\" \"$@\")\ncat << EOF > \"$CONSTRAINTS_FILE\"\n# This file was generated by $RELATIVE_SCRIPT_PATH and can be updated using\n# that script.\nEOF\necho \"${PINNINGS}\" >> \"${CONSTRAINTS_FILE}\"\n"
  },
  {
    "path": "tools/pip_install.py",
    "content": "#!/usr/bin/env python\n# pip installs packages using pinned package versions. If CERTBOT_OLDEST is set\n# to 1, tools/oldest_constraints.txt is used, otherwise, tools/requirements.txt\n# is used. Before installing the requested packages, core Python packaging\n# tools like pip, setuptools, and wheel are updated to pinned versions to\n# increase stability of the install.\n#\n# cryptography is currently using this script in their CI at\n# https://github.com/pyca/cryptography/blob/14d45c2259b01f1459eeab8bb7d85ce4cfb0841b/.github/downstream.d/certbot.sh#L8-L9.\n# We should try to remember to keep their repo updated if we make any changes\n# to this script which may break things for them.\n\nfrom __future__ import absolute_import\nfrom __future__ import print_function\n\nimport os\nimport subprocess\nimport sys\n\n\ndef find_tools_path():\n    return os.path.dirname(os.path.realpath(__file__))\n\n\ndef call_with_print(command, env):\n    assert env is not None\n    print(command)\n    subprocess.check_call(command, shell=True, env=env)\n\n\ndef uv_install_with_print(args_str, env):\n    command = ['\"', sys.executable, '\" -m uv pip install ', args_str]\n    call_with_print(''.join(command), env=env)\n\ndef pip_install_with_print(args_str, env):\n    command = ['\"', sys.executable, '\" -m pip install --disable-pip-version-check --use-pep517 ', args_str]\n    call_with_print(''.join(command), env=env)\n\n\ndef pip_constrained_environ():\n    tools_path = find_tools_path()\n\n    repo_path = os.path.dirname(tools_path)\n    if os.environ.get('CERTBOT_OLDEST') == '1':\n        constraints_path = os.path.normpath(os.path.join(\n            repo_path, 'tools', 'oldest_constraints.txt'))\n    else:\n        constraints_path = os.path.normpath(os.path.join(\n            repo_path, 'tools', 'requirements.txt'))\n\n    env = os.environ.copy()\n    # We set constraints for pip using an environment variable so that they\n    # are also used when installing build dependencies. See\n    # https://github.com/certbot/certbot/pull/8443 for more info.\n    env[\"PIP_CONSTRAINT\"] = constraints_path\n    env[\"UV_CONSTRAINT\"] = constraints_path\n    env[\"UV_BUILD_CONSTRAINT\"] = constraints_path\n    return env\n\n\ndef pipstrap(env=None):\n    if env is None:\n        env = pip_constrained_environ()\n    pip_install_with_print('pip setuptools wheel uv', env=env)\n\n\ndef main(args):\n    env = pip_constrained_environ()\n    pipstrap(env)\n    uv_install_with_print(' '.join(args), env=env)\n\n\nif __name__ == '__main__':\n    main(sys.argv[1:])\n"
  },
  {
    "path": "tools/pipstrap.py",
    "content": "#!/usr/bin/env python\n\"\"\"Uses pip to upgrade Python packaging tools to pinned versions.\"\"\"\nimport pip_install\n\n\ndef main():\n    pip_install.pipstrap()\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "tools/release.sh",
    "content": "#!/bin/bash -e\n# Release packages to PyPI\n\nif [ \"`dirname $0`\" != \"tools\" ] ; then\n    echo Please run this script from the repo root\n    exit 1\nfi\n\nCheckVersion() {\n    # Args: <version number>\n    if ! echo \"$1\" | grep -q -e '[0-9]\\+.[0-9]\\+.[0-9]\\+' ; then\n        echo \"$1 doesn't look like 1.2.3\"\n        echo \"Usage:\"\n        echo \"$0 RELEASE_VERSION NEXT_VERSION\"\n        exit 1\n    fi\n}\n\nCheckVersion \"$1\"\nCheckVersion \"$2\"\n\nif [ \"$RELEASE_GPG_KEY\" = \"\" ] && ! gpg --card-status >/dev/null 2>&1; then\n    echo OpenPGP card not found!\n    echo Please insert your PGP card and run this script again.\n    exit 1\nfi\n\nif ! command -v script >/dev/null 2>&1; then\n    echo The command script was not found.\n    echo Please install it.\n    exit 1\nfi\n\nif [ -n \"$SNAP_BUILD\" ]; then\n    echo \"Running the release script with the environment variable SNAP_BUILD\"\n    echo \"set will cause plugins' wheels to be built without dependencies\"\n    echo \"on Certbot. See https://github.com/certbot/certbot/pull/8091 for more\"\n    echo \"info. Please unset this environment variable and run this script\"\n    echo \"again.\"\n    exit 1\nfi\n\nexport RELEASE_DIR=\"./releases\"\nmv \"$RELEASE_DIR\" \"$RELEASE_DIR.$(date +%s).bak\" || true\nLOG_PATH=\"log\"\nmv \"$LOG_PATH\" \"$LOG_PATH.$(date +%s).bak\" || true\n\n# Work with both Linux and macOS versions of script\nif script --help | grep -q -- '--command'; then\n    script --command \"tools/_release.sh $1 $2\" \"$LOG_PATH\"\nelse\n    script \"$LOG_PATH\" tools/_release.sh \"$1\" \"$2\"\nfi\n"
  },
  {
    "path": "tools/requirements.txt",
    "content": "# This file was generated by tools/pinning/current/repin.sh and can be updated using\n# that script.\n#\n# It is normally used as constraints to pip, however, it has the name\n# requirements.txt so that is scanned by GitHub. See\n# https://docs.github.com/en/github/visualizing-repository-data-with-graphs/about-the-dependency-graph#supported-package-ecosystems\n# for more info.\nalabaster==1.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nanyio==4.12.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\napacheconfig==0.3.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nastroid==3.3.11 ; python_version >= \"3.10\" and python_version < \"4.0\"\nasttokens==3.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nattrs==25.4.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nazure-core==1.38.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nazure-devops==7.1.0b4 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbabel==2.18.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbackports-tarfile==1.2.0 ; python_version >= \"3.10\" and python_version < \"3.12\"\nbcrypt==5.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbeautifulsoup4==4.14.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nboto3==1.42.40 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbotocore==1.42.40 ; python_version >= \"3.10\" and python_version < \"4.0\"\nbuild==1.4.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncachecontrol==0.14.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncachetools==7.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncertifi==2026.1.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncffi==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nchardet==5.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncharset-normalizer==3.4.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncleo==2.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nclick==8.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncloudflare==2.19.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncolorama==0.4.6 ; python_version >= \"3.10\" and python_version < \"4.0\"\nconfigargparse==1.7.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nconfigobj==5.0.9 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncoverage==7.13.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncrashtest==0.4.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncryptography==46.0.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\ncython==0.29.37 ; python_version >= \"3.10\" and python_version <= \"3.12\"\ncython==3.2.4 ; python_version >= \"3.13\" and python_version < \"4.0\"\ndecorator==5.2.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndeprecated==1.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndill==0.4.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndistlib==0.4.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndistro==1.9.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndns-lexicon==3.23.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndnspython==2.8.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ndocutils==0.21.2 ; python_version == \"3.10\"\ndocutils==0.22.4 ; python_version >= \"3.11\" and python_version < \"4.0\"\ndulwich==1.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nexceptiongroup==1.3.1 ; python_version == \"3.10\"\nexecnet==2.1.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nexecuting==2.2.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nfabric==3.2.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nfastjsonschema==2.21.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nfilelock==3.20.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nfindpython==0.7.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ngoogle-api-core==2.29.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ngoogle-api-python-client==2.188.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ngoogle-auth-httplib2==0.3.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ngoogle-auth==2.48.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ngoogleapis-common-protos==1.72.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nh11==0.16.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nhttpcore==1.0.9 ; python_version >= \"3.10\" and python_version < \"4.0\"\nhttplib2==0.31.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nhttpx==0.28.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nid==1.5.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nidna==3.11 ; python_version >= \"3.10\" and python_version < \"4.0\"\nimagesize==1.4.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nimportlib-metadata==8.7.1 ; python_version >= \"3.10\" and python_version < \"3.12\"\niniconfig==2.3.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ninstaller==0.7.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ninvoke==2.2.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nipdb==0.13.13 ; python_version >= \"3.10\" and python_version < \"4.0\"\nipython-pygments-lexers==1.1.1 ; python_version >= \"3.11\" and python_version < \"4.0\"\nipython==8.38.0 ; python_version == \"3.10\"\nipython==9.10.0 ; python_version >= \"3.11\" and python_version < \"4.0\"\nisodate==0.7.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nisort==5.13.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\njaraco-classes==3.4.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njaraco-context==6.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njaraco-functools==4.4.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njedi==0.19.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\njeepney==0.9.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform == \"linux\"\njinja2==3.1.6 ; python_version >= \"3.10\" and python_version < \"4.0\"\njmespath==1.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njosepy==2.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njsonlines==4.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\njsonpickle==4.1.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nkeyring==25.7.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmarkdown-it-py==4.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmarkupsafe==3.0.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmatplotlib-inline==0.2.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmccabe==0.7.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmdurl==0.1.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmore-itertools==10.8.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmsgpack==1.1.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmsrest==0.7.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmypy-extensions==1.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nmypy==1.9.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nnh3==0.3.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\noauthlib==3.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\npackaging==26.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nparamiko==4.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nparsedatetime==2.6 ; python_version >= \"3.10\" and python_version < \"4.0\"\nparso==0.8.5 ; python_version >= \"3.10\" and python_version < \"4.0\"\npbs-installer==2026.1.27 ; python_version >= \"3.10\" and python_version < \"4.0\"\npexpect==4.9.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform != \"win32\" and sys_platform != \"emscripten\"\npip==26.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npkginfo==1.12.1.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nplatformdirs==4.5.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\npluggy==1.6.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nply==3.11 ; python_version >= \"3.10\" and python_version < \"4.0\"\npoetry-core==2.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\npoetry-plugin-export==1.10.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npoetry==2.3.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nprompt-toolkit==3.0.52 ; python_version >= \"3.10\" and python_version < \"4.0\"\nproto-plus==1.27.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nprotobuf==6.33.5 ; python_version >= \"3.10\" and python_version < \"4.0\"\nptyprocess==0.7.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform != \"win32\" and sys_platform != \"emscripten\"\npure-eval==0.2.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyasn1-modules==0.4.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyasn1==0.6.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npycparser==3.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and implementation_name != \"PyPy\"\npygments==2.19.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npylint==3.3.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\npynacl==1.6.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyopenssl==25.3.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyotp==2.9.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyparsing==3.3.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyproject-api==1.10.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyproject-hooks==1.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npyrfc3339==2.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npytest-cov==7.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npytest-xdist==3.8.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npytest==9.0.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\npython-augeas==1.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npython-dateutil==2.9.0.post0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npython-digitalocean==1.17.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\npywin32-ctypes==0.2.3 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform == \"win32\"\npywin32==311 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform == \"win32\"\npyyaml==6.0.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrapidfuzz==3.14.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nreadme-renderer==44.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrequests-file==3.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrequests-oauthlib==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrequests-toolbelt==1.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrequests==2.32.5 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrfc3986==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nrich==14.3.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nroman-numerals==4.1.0 ; python_version >= \"3.11\" and python_version < \"4.0\"\nrsa==4.9.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nruff==0.15.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ns3transfer==0.16.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsecretstorage==3.5.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform == \"linux\"\nsemantic-version==2.10.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsetuptools-rust==1.12.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsetuptools==80.10.2 ; python_version >= \"3.10\" and python_version < \"4.0\"\nshellingham==1.5.4 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsix==1.17.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsnowballstemmer==3.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsoupsieve==2.8.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinx-rtd-theme==3.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinx==8.1.3 ; python_version == \"3.10\"\nsphinx==9.0.4 ; python_version == \"3.11\"\nsphinx==9.1.0 ; python_version >= \"3.12\" and python_version < \"4.0\"\nsphinxcontrib-applehelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-devhelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-htmlhelp==2.1.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-jquery==4.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-jsmath==1.0.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-qthelp==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nsphinxcontrib-serializinghtml==2.0.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nstack-data==0.6.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntldextract==5.3.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntomli==2.4.0 ; python_version == \"3.10\"\ntomlkit==0.14.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntowncrier==25.8.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntox==4.34.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntraitlets==5.14.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntrove-classifiers==2026.1.14.14 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntwine==6.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-httplib2==0.31.2.20260125 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-pyrfc3339==2.0.1.20250825 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-python-dateutil==2.9.0.20260124 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-pywin32==311.0.0.20251008 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-requests==2.32.4.20260107 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntypes-setuptools==80.10.0.20260124 ; python_version >= \"3.10\" and python_version < \"4.0\"\ntyping-extensions==4.15.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nuritemplate==4.2.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\nurllib3==2.6.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nuv==0.9.28 ; python_version >= \"3.10\" and python_version < \"4.0\"\nvirtualenv==20.36.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nwcwidth==0.5.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nwheel==0.46.3 ; python_version >= \"3.10\" and python_version < \"4.0\"\nwrapt==2.1.1 ; python_version >= \"3.10\" and python_version < \"4.0\"\nxattr==1.3.0 ; python_version >= \"3.10\" and python_version < \"4.0\" and sys_platform == \"darwin\"\nzipp==3.23.0 ; python_version >= \"3.10\" and python_version < \"3.12\"\nzstandard==0.25.0 ; python_version >= \"3.10\" and python_version < \"4.0\"\n"
  },
  {
    "path": "tools/retry.sh",
    "content": "#!/bin/bash\n# Retries a command if it fails.\n#\n# This is based on travis_retry.bash\n# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/bash/travis_retry.bash.\nset -e\nresult=0\ncount=1\nwhile [[ \"${count}\" -le 3 ]]; do\n  result=0\n  \"${@}\" || result=\"${?}\"\n  if [[ $result -eq 0 ]]; then break; fi\n  count=\"$((count + 1))\"\n  sleep 1\ndone\n\nexit \"${result}\"\n"
  },
  {
    "path": "tools/snap/README.md",
    "content": "# Building Certbot Snaps\n\n## Local Testing and Development\n\nThese instructions are recommended when testing anything about the snap setup for ease of debugging.\nThe architecture of the built snap is limited to the architecture of the system it is built on.\n\n### Initial VM Set Up\n\nThese steps need to be done once to set up your VM and do not need to be run again to rebuild the snap.\n\n 1. Start with a Focal VM. You need a full virtual machine using something like DigitalOcean, EC2, or VirtualBox. Docker won't work. Another version of Ubuntu can probably be used, but Focal was used when writing these instructions.\n 2. Set up a user other than root with sudo privileges for use with snapcraft and run all of the following commands with it. A command to do this for a user named certbot looks like `adduser certbot && usermod -aG sudo certbot && su - certbot`.\n 3. Install git and python with `sudo apt update && sudo apt install -y git python`.\n 4. Set up lxd for use with snapcraft by running `sudo snap install lxd; sudo /snap/bin/lxd waitready && sudo /snap/bin/lxd init --auto` (errors here are ok; it may already\n have been installed on your system).\n 5. Add your current user to the lxd group and update your shell to have the new assignment by running `sudo usermod -a -G lxd ${USER} && newgrp lxd`.\n 6. Install snapcraft with `sudo snap install --classic snapcraft`.\n 7. `cd ~` (or any other directory where you want our source files to be)\n 8. Run `git clone https://github.com/certbot/certbot`\n 9. `cd certbot` (All further instructions are relative to this directory.)\n\n### Certbot Snap\n\n#### Reset the Environment\n\nIf the snap has been built before, the instructions below clean up the build environment so it can reliably be used again.\n\n 1. `snapcraft clean --use-lxd`\n 2. [Optional] `mv certbot_*_amd64.snap certbot_amd64.snap.bak`\n\n#### Build the Certbot Snap\n\nThese are the steps to build and install the Certbot snap. If you have run these steps before, you may want to run the commands in the section above to clean things up or save a previous build before building the snap again (running `snapcraft` again will overwrite the previous snap).\n\n 1. Run `snapcraft --use-lxd`.\n 2. Install the generated snap with `sudo snap install --dangerous --classic certbot_*_amd64.snap`. You can transfer the snap to a different machine to run it there instead if you prefer.\n\n#### Run\n\nRun Certbot as normal. For example, `certbot plugins` should display the Apache and Nginx plugins.\n\n### Certbot Plugin Snaps\n\nThese instructions use the `certbot-dns-dnsimple` plugin as an example, but all of Certbot's other plugin snaps can be built in the same way.\n\n#### Reset the Environment\n\nIf the plugin snap has been built before, the instructions below clean up the build environment so it can reliably be used again.\n\n 1. `cd certbot-dns-dnsimple`\n 2. `snapcraft clean --use-lxd`\n 3. [Optional] `mv certbot-dns-dnsimple_*_amd64.snap certbot-dns-simple_amd64.snap.bak`\n 4. `cd ..`\n\n#### Build a Certbot Plugin Snap\n\nThese are the steps to build and install the Certbot DNSimple plugin snap. If you have run these steps before, you may want to run the commands in the section above to clean things up or save a previous build before building the snap again (running `snapcraft` again will overwrite the previous snap).\n\n 1. Run `tools/snap/generate_dnsplugins_all.sh` to generate all necessary files for all plugin snaps.\n 2. `cd certbot-dns-dnsimple`\n 3. `snapcraft --use-lxd`\n 4. Run `sudo snap set certbot trust-plugin-with-root=ok`.\n 5. Install the generated snap with `sudo snap install --dangerous certbot-dns-dnsimple_*_amd64.snap`. Again, you can transfer the snap to a different machine to run it there instead if you prefer.\n 6. Connect the plugin with `sudo snap connect certbot:plugin certbot-dns-dnsimple`.\n 7. Connect the plugin metadata with `sudo snap connect certbot-dns-dnsimple:certbot-metadata certbot:certbot-metadata`. Install the plugin again to test refresh; if the plugin's hook creates any logs, they are at `/var/snap/certbot-dns-dnsimple/current/debuglog`.\n\n#### Run\n\nRun Certbot as normal. For example, `certbot plugins` should display the DNSimple plugin as installed.\n\n## Building for Other Architectures\n\nTo build for an unavailable architecture or for multiple architectures simultaneously, we recommend using snapcraft's remote build feature.\nIt is easiest to run this from a local machine.\n\n### Initial Local Setup\n\n 1. Create or log into an Ubuntu One account [here](https://login.launchpad.net/).\n 2. Install git and python with `sudo apt update && sudo apt install -y git python`.\n 3. Install snapcraft with `sudo snap install --classic snapcraft`.\n 4. `cd ~` (or any other directory where you want our source files to be)\n 5. Run `git clone https://github.com/certbot/certbot`\n 6. `cd certbot` (All further instructions are relative to this directory.)\n 7. To trigger `snapcraft` to request access to your Launchpad account, run\n    `snapcraft remote-build --launchpad-accept-public-upload --status`. A URL where you need\n    to grant this access will be printed to your terminal and automatically open in your browser\n    if one is available.\n\n### Build Snaps Remotely\n\nCertbot provides a wrapper around snapcraft's remote build to make building all of our plugins easier. To see all available\noptions, run `python3 tools/snap/build_remote.py --help`.\n\nFor example, to build all available snaps for all architectures, run `python3 tools/snap/build_remote.py ALL --archs amd64 arm64 armhf`.\n\nTo build only the certbot snap on only amd64, run `python3 tools/snap/build_remote.py certbot --archs armhf`.\n\nThe command will upload the entire contents of the working directory, so if the remote build\nappears to hang, try using a clean clone of the `certbot` repository.\n"
  },
  {
    "path": "tools/snap/build_remote.py",
    "content": "#!/usr/bin/env python3\nimport argparse\nimport datetime\nimport functools\nimport glob\nfrom multiprocessing import Manager\nfrom multiprocessing import Pool\nfrom multiprocessing import Process\nfrom multiprocessing.managers import SyncManager\nimport os\nfrom os.path import basename\nfrom os.path import dirname\nfrom os.path import join\nfrom os.path import realpath\nimport random\nimport re\nimport string\nimport subprocess\nimport sys\nimport tempfile\nfrom threading import Lock\nimport time\nfrom typing import Dict\nfrom typing import List\nfrom typing import Set\nfrom typing import Tuple\n\nCERTBOT_DIR = dirname(dirname(dirname(realpath(__file__))))\nPLUGINS = [basename(path) for path in glob.glob(join(CERTBOT_DIR, 'certbot-dns-*'))]\n\n\n# In Python, stdout and stderr are buffered in each process by default. When\n# printing output from multiple processes, this can cause delays in printing\n# output with lines from different processes being interleaved depending\n# on when the output for that process is flushed. To prevent this, we override\n# print so that it always flushes its output. Disabling output buffering can\n# also be done through command line flags or environment variables set when the\n# Python process starts, but this approach was taken instead to ensure\n# consistent behavior regardless of how the script is invoked.\nprint = functools.partial(print, flush=True)\n\n\ndef _execute_build(\n        target: str, archs: Set[str], status: Dict[str, Dict[str, str]],\n        workspace: str, output_lock: Lock) -> Tuple[int, List[str]]:\n    # The implementation of remote-build recovery has changed over time.\n    # Currently, you cannot set a build-id, and the build-id is instead derived\n    # from a hash of the contents of the files in the directory:\n    # https://github.com/canonical/craft-application/blob/5b09ab3d9152a2b61ffcdf57691289023ed6ba26/craft_application/remote/utils.py#L64\n    #\n    # We want a unique build ID so a fresh build is started for each run instead\n    # of potentially reusing an old build. See https://github.com/certbot/certbot/pull/8719\n    # and https://github.com/snapcore/snapcraft/pull/3554 for more info.\n    #\n    # In the hope that one day you can again set a build ID, we will modify\n    # the directory by creating a file containing a build ID that conforms\n    # to the shape of snapcraft's build ID: using a MD5 hash represented as a\n    # 32 character hex string (we use a larger character set).\n\n    random_string = ''.join(random.choice(string.ascii_lowercase + string.digits)\n                            for _ in range(32))\n    # place random string in build_id file inside `workspace` directory\n    with open(join(workspace, 'build_id'), 'w') as build_id_file:\n        build_id_file.write(random_string)\n\n    with tempfile.TemporaryDirectory() as tempdir:\n        environ = os.environ.copy()\n        environ['XDG_CACHE_HOME'] = tempdir\n        process = subprocess.Popen([\n            'snapcraft', 'remote-build', '--launchpad-accept-public-upload',\n            '--build-for', ','.join(archs)],\n            stdout=subprocess.PIPE, stderr=subprocess.STDOUT,\n            universal_newlines=True, env=environ, cwd=workspace)\n\n    killed = False\n    process_output: List[str] = []\n    for line in process.stdout:\n        process_output.append(line)\n        _extract_state(target, line, status)\n\n        if not killed and any(state for state in status[target].values() if state == 'Chroot problem'):\n            # On this error the snapcraft process hangs. Let's finish it.\n            #\n            # killed is used to stop us from executing this code path\n            # multiple times per build that encounters \"Chroot problem\".\n            with output_lock:\n                print('Chroot problem encountered for build '\n                      f'{target} for {\",\".join(archs)}.\\n'\n                      'Launchpad seems to be unable to recover from this '\n                      'state so we are terminating the build.')\n            process.kill()\n            killed = True\n\n    process_state = process.wait()\n\n    return process_state, process_output\n\n\ndef _build_snap(\n        target: str, archs: Set[str], status: Dict[str, Dict[str, str]],\n        running: Dict[str, bool], output_lock: Lock) -> bool:\n    if target == 'certbot':\n        workspace = CERTBOT_DIR\n    else:\n        workspace = join(CERTBOT_DIR, target)\n        # Init and commit git repo in workspace. This is necessary starting in core24\n        # as \"Projects must be at the top level of a git repository\"\n        # https://snapcraft.io/docs/migrate-core24#remote-build\n        subprocess.run(['git', 'init'], capture_output=True, check=True, cwd=workspace)\n        subprocess.run(['git', 'add', '-A'], capture_output=True, check=True, cwd=workspace)\n        subprocess.run(['git', 'commit', '-m', 'init'], capture_output=True, check=True, cwd=workspace)\n\n    build_success = False\n    retry = 3\n    while retry:\n        # Let's reset the status before each build so we're not starting with\n        # old state values.\n        status[target] = {arch: '...' for arch in archs}\n        exit_code, process_output = _execute_build(target, archs, status, workspace, output_lock)\n        with output_lock:\n            print(f'Build {target} for {\",\".join(archs)} (attempt {4-retry}/3) ended with '\n                  f'exit code {exit_code}.')\n\n            # This output may change, and is set by\n            # https://github.com/canonical/snapcraft/blob/8ab7fd0c8a1d3f13045bec41a6e0158c063faa9b/snapcraft/commands/remote.py#L278\n            failed_archs = [arch for arch in archs if status[target][arch] != 'Succeeded']\n            # If the command failed or any architecture wasn't built\n            # successfully, let's try to print all the output about the problem\n            # that we can.\n            dump_output = exit_code != 0 or failed_archs\n            if exit_code == 0 and not failed_archs:\n                # We expect to have all target snaps available, or something bad happened.\n                snaps_list = glob.glob(join(workspace, '*.snap'))\n                if not len(snaps_list) == len(archs):\n                    print('Some of the expected snaps for a successful build are missing '\n                          f'(current list: {snaps_list}).')\n                    dump_output = True\n                else:\n                    build_success = True\n                    break\n            if dump_output:\n                print(f'Dumping snapcraft remote-build output build for {target}:')\n                print('\\n'.join(process_output))\n                _dump_failed_build_logs(target, archs, status, workspace)\n\n        # Retry the remote build if it has been interrupted (non zero status code)\n        # or if some builds have failed.\n        retry = retry - 1\n\n    running[target] = False\n\n    return build_success\n\n\ndef _extract_state(project: str, output: str, status: Dict[str, Dict[str, str]]) -> None:\n    state = status[project]\n\n    # This output may change, and is set by\n    # https://github.com/canonical/snapcraft/blob/8ab7fd0c8a1d3f13045bec41a6e0158c063faa9b/snapcraft/commands/remote.py#L218\n    if \"Starting new build\" in output:\n        for arch in state.keys():\n            state[arch] = \"Starting new build\"\n\n    match = re.match(r'^(\\w+): (\\w+)$', output)\n    if match:\n        arch = match.group(2)\n        state[arch] = match.group(1)\n\n    # You need to reassign the value of status[project] here (rather than doing\n    # something like status[project][arch] = match.group(1)) for the state change\n    # to propagate to other processes. See\n    # https://docs.python.org/3.8/library/multiprocessing.html#proxy-objects for\n    # more info.\n    status[project] = state\n\n\ndef _dump_status_helper(archs: Set[str], status: Dict[str, Dict[str, str]]) -> None:\n    headers = ['project', *archs]\n    print(''.join(f'| {item:<25}' for item in headers))\n    print(f'|{\"-\" * 26}' * len(headers))\n    for project, states in sorted(status.items()):\n        print(''.join(f'| {item:<25}' for item in [project, *[states[arch] for arch in archs]]))\n    print(f'|{\"-\" * 26}' * len(headers))\n    print()\n\n\ndef _dump_status(\n        archs: Set[str], status: Dict[str, Dict[str, str]],\n        running: Dict[str, bool], output_lock: Lock) -> None:\n    while any(running.values()):\n        with output_lock:\n            print(f'Remote build status at {datetime.datetime.now()}')\n            _dump_status_helper(archs, status)\n        time.sleep(10)\n\n\ndef _dump_failed_build_logs(\n        target: str, archs: Set[str], status: Dict[str, Dict[str, str]],\n        workspace: str) -> None:\n    logs_list = glob.glob(join(workspace, f'snapcraft-{target}-*.txt'))\n    for arch in archs:\n        result = status[target][arch]\n\n        if result != 'Succeeded':\n            failures = True\n\n            # log name is no longer set deterministically as target_arch.txt\n            # this will still result in a single output though, as we're filtering\n            # by target above and arch here\n            build_output_path = [log_name for log_name in logs_list if arch in log_name]\n            if not build_output_path:\n                build_output = 'No output has been dumped by snapcraft remote-build.'\n            else:\n                with open(build_output_path[0]) as file_h:\n                    build_output = file_h.read()\n\n            print(f'Output for failed build target={target} arch={arch}')\n            print('-------------------------------------------')\n            print(build_output)\n            print('-------------------------------------------')\n            print()\n\n\ndef _dump_results(archs: Set[str], status: Dict[str, Dict[str, str]]) -> None:\n    print(f'Results for remote build finished at {datetime.datetime.now()}')\n    _dump_status_helper(archs, status)\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument('targets', nargs='+', choices=['ALL', 'DNS_PLUGINS', 'certbot', *PLUGINS],\n                        help='the list of snaps to build')\n    parser.add_argument('--archs', nargs='+', choices=['amd64', 'arm64', 'armhf'],\n                        default=['amd64'], help='the architectures for which snaps are built')\n    parser.add_argument('--timeout', type=int, default=None,\n                        help='build process will fail after the provided timeout (in seconds)')\n    args = parser.parse_args()\n\n    archs = set(args.archs)\n    targets = set(args.targets)\n\n    if 'ALL' in targets:\n        targets.remove('ALL')\n        targets.update(['certbot', 'DNS_PLUGINS'])\n\n    if 'DNS_PLUGINS' in targets:\n        targets.remove('DNS_PLUGINS')\n        targets.update(PLUGINS)\n\n    # If we're building anything other than just Certbot, we need to\n    # generate the snapcraft files for the DNS plugins.\n    if targets != {'certbot'}:\n        subprocess.run(['tools/snap/generate_dnsplugins_all.sh'],\n                       check=True, cwd=CERTBOT_DIR)\n\n    print('Start remote snap builds...')\n    print(f' - archs: {\", \".join(archs)}')\n    print(f' - projects: {\", \".join(sorted(targets))}')\n    print()\n\n    manager: SyncManager = Manager()\n    pool = Pool(processes=len(targets))\n    with manager, pool:\n        status: Dict[str, Dict[str, str]] = manager.dict()\n        running = manager.dict({target: True for target in targets})\n        # While multiple processes are running, this lock should be acquired\n        # before printing output.\n        output_lock = manager.Lock()\n\n        async_results = [pool.apply_async(_build_snap, (target, archs, status, running, output_lock))\n                         for target in targets]\n\n        process = Process(target=_dump_status, args=(archs, status, running, output_lock))\n        process.start()\n\n        try:\n            process.join(args.timeout)\n\n            if process.is_alive():\n                for target in targets:\n                    if target == 'certbot':\n                        workspace = CERTBOT_DIR\n                    else:\n                        workspace = join(CERTBOT_DIR, target)\n                    _dump_failed_build_logs(target, archs, status, workspace)\n                raise ValueError(f\"Timeout out reached ({args.timeout} seconds) during the build!\")\n\n            build_success = True\n            for async_result in async_results:\n                if not async_result.get():\n                    build_success = False\n\n            _dump_results(archs, status)\n            if build_success:\n                print('All builds succeeded.')\n            else:\n                print('Some builds failed.')\n                raise ValueError(\"There were failures during the build!\")\n        finally:\n            process.terminate()\n\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  },
  {
    "path": "tools/snap/generate_dnsplugins_all.sh",
    "content": "#!/bin/bash\n# Generate all necessary files for building snaps for all DNS plugins\nset -eu\n\nDIR=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" >/dev/null 2>&1 && pwd )\"\nCERTBOT_DIR=\"$(dirname \"$(dirname \"${DIR}\")\")\"\n\nfor PLUGIN_PATH in \"${CERTBOT_DIR}\"/certbot-dns-*; do\n  bash \"${CERTBOT_DIR}\"/tools/snap/generate_dnsplugins_snapcraft.sh $PLUGIN_PATH\n  bash \"${CERTBOT_DIR}\"/tools/snap/generate_dnsplugins_postrefreshhook.sh $PLUGIN_PATH\n  # Create constraints file\n  cp \"${CERTBOT_DIR}\"/tools/requirements.txt \"${PLUGIN_PATH}\"/snap-constraints.txt\ndone\n"
  },
  {
    "path": "tools/snap/generate_dnsplugins_postrefreshhook.sh",
    "content": "#!/bin/bash\n# Generate the hooks/post-refresh file for a DNS plugin\n# Usage: bash generate_dnsplugins_postrefreshhook.sh path/to/dns/plugin\n# For example, from the certbot home directory:\n#   tools/snap/generate_dnsplugins_postrefreshhook.sh certbot-dns-dnsimple\nset -eu\n\nPLUGIN_PATH=$1\nmkdir -p \"${PLUGIN_PATH}/snap/hooks\"\ncat <<EOF > \"${PLUGIN_PATH}/snap/hooks/post-refresh\"\n#!/bin/sh -e\n# This file is generated automatically and should not be edited manually.\n\n# get certbot version\nif [ ! -f \"\\$SNAP/certbot-shared/certbot-version.txt\" ]; then\n  echo \"No certbot version available; not doing version comparison check\" >> \"\\$SNAP_DATA/debuglog\"\n  exit 0\nfi\ncb_installed=\\$(cat \\$SNAP/certbot-shared/certbot-version.txt)\n\n# get required certbot version for plugin. certbot version must be at least the plugin's\n# version. note that this is not the required version in setup.py, but the version number itself.\ncb_required=\\$(grep -oP \"version = '\\K.*(?=')\" \\$SNAP/setup.py)\n\n\n\\$SNAP/bin/python3 -c \"import sys; from packaging import version; sys.exit(1) if\\\n  version.parse('\\$cb_installed') < version.parse('\\$cb_required') else sys.exit(0)\" || exit_code=\\$?\nif [ \"\\$exit_code\" = \"1\" ]; then\n  echo \"Certbot is version \\$cb_installed but needs to be at least \\$cb_required before\" \\\\\n    \"this plugin can be updated; will try again on next refresh.\"\n  exit 1\nfi\nEOF\n"
  },
  {
    "path": "tools/snap/generate_dnsplugins_snapcraft.sh",
    "content": "#!/bin/bash\n# Generate the snapcraft.yaml file for a DNS plugins\n# Usage: bash generate_dnsplugins_snapcraft.sh path/to/dns/plugin\n# For example, from the certbot home directory:\n#   tools/snap/generate_dnsplugins_snapcraft.sh certbot-dns-dnsimple\nset -e\n\nPLUGIN_PATH=$1\nPLUGIN=$(basename \"${PLUGIN_PATH}\")\nDESCRIPTION=$(sed -E -n \"/^description = / s/^description = ['\\\"](.*)['\\\"]/\\1/ p\" \"${PLUGIN_PATH}/pyproject.toml\")\nmkdir -p \"${PLUGIN_PATH}/snap\"\ncat <<EOF > \"${PLUGIN_PATH}/snap/snapcraft.yaml\"\n# This file is generated automatically and should not be edited manually.\nname: ${PLUGIN}\nsummary: ${DESCRIPTION}\ndescription: ${DESCRIPTION}\nconfinement: strict\ngrade: stable\nbase: core24\nadopt-info: ${PLUGIN}\n\nparts:\n  ${PLUGIN}:\n    plugin: python\n    source: .\n    override-pull: |\n        craftctl default\n        craftctl set version=\\$(grep ^version \\$SNAPCRAFT_PART_SRC/setup.py | cut -f2 -d= | tr -d \"'[:space:]\")\n    build-environment:\n      # We set this environment variable while building to try and increase the\n      # stability of fetching the rust crates needed to build the cryptography\n      # library.\n      - CARGO_NET_GIT_FETCH_WITH_CLI: \"true\"\n      # Constraints are passed through the environment variable PIP_CONSTRAINTS instead of using the\n      # parts.[part_name].constraints option available in snapcraft.yaml when the Python plugin is\n      # used. This is done to let these constraints be applied not only on the certbot package\n      # build, but also on any isolated build that pip could trigger when building wheels for\n      # dependencies. See https://github.com/certbot/certbot/pull/8443 for more info.\n      - PIP_CONSTRAINT: \\$SNAPCRAFT_PART_SRC/snap-constraints.txt\n      - SNAP_BUILD: \"True\"\n    # To build cryptography and cffi if needed\n    build-packages:\n      - gcc\n      - git\n      - build-essential\n      - libssl-dev\n      - libffi-dev\n      - python3-dev\n      - cargo\n      - pkg-config\n  certbot-metadata:\n    plugin: dump\n    source: .\n    stage: [setup.py, certbot-shared]\n    override-pull: |\n        craftctl default\n        mkdir -p \\$SNAPCRAFT_PART_SRC/certbot-shared\n\nslots:\n  certbot:\n    interface: content\n    content: certbot-1\n    read:\n      - \\$SNAP/lib/python3.12/site-packages\n\nplugs:\n  certbot-metadata:\n    interface: content\n    content: metadata-1\n    target: \\$SNAP/certbot-shared\nEOF\n"
  },
  {
    "path": "tools/sphinx-quickstart.sh",
    "content": "#!/bin/bash\n\nif [ $# -ne 1 ];  then\n  echo \"Usage: $(basename $0) project-name\"\n  exit 1\nfi\n\nPROJECT=$1\n\nyes \"n\" | sphinx-quickstart --dot _ --project $PROJECT --author \"Certbot Project\" -v 0 --release 0 --language en --suffix .rst --master index --ext-autodoc --ext-intersphinx --ext-todo --ext-coverage --ext-viewcode --extensions sphinx_rtd_theme --makefile --batchfile $PROJECT/docs\n\ncd $PROJECT/docs\nsed -i -e \"s|\\# needs_sphinx = '1.0'|needs_sphinx = '1.0'|\" conf.py\nsed -i -e \"s|intersphinx_mapping = {'https://docs.python.org/': None}|intersphinx_mapping = {\\n    'python': ('https://docs.python.org/', None),\\n    'acme': ('https://acme-python.readthedocs.io/en/latest/', None),\\n    'certbot': ('https://eff-certbot.readthedocs.io/en/stable/', None),\\n}|\" conf.py\nsed -i -e \"s|html_theme = 'alabaster'|html_theme = 'sphinx_rtd_theme'|\" conf.py\nsed -i -e \"s|# Add any paths that contain templates here, relative to this directory.|autodoc_member_order = 'bysource'\\nautodoc_default_flags = ['show-inheritance']\\n\\n# Add any paths that contain templates here, relative to this directory.|\" conf.py\nsed -i -e \"s|# The name of the Pygments (syntax highlighting) style to use.|default_role = 'py:obj'\\n\\n# The name of the Pygments (syntax highlighting) style to use.|\" conf.py\n# If the --ext-todo flag is removed from sphinx-quickstart, the line below can be removed.\nsed -i -e \"s|todo_include_todos = True|todo_include_todos = False|\" conf.py\necho \"/_build/\" >> .gitignore\necho \"=================\nAPI Documentation\n=================\n\n.. toctree::\n   :glob:\n\n   api/**\" > api.rst\nsed -i -e \"s|   :caption: Contents:|   :caption: Contents:\\n\\n.. automodule:: ${PROJECT//-/_}\\n   :members:\\n\\n.. toctree::\\n   :maxdepth: 1\\n\\n   api|\" index.rst\n\necho \"Suggested next steps:\n* Add API docs to: $PROJECT/docs/api/\n* Run: git add $PROJECT/docs\"\n"
  },
  {
    "path": "tools/venv.py",
    "content": "#!/usr/bin/env python3\n# Developer virtualenv setup for Certbot client\n\"\"\"Aids in creating a developer virtual environment for Certbot.\n\nWhen this module is run as a script, it takes the arguments that should\nbe passed to pip to install the Certbot packages as command line\narguments. If no arguments are provided, all Certbot packages and their\ndevelopment dependencies are installed. The virtual environment will be\ncreated with the name \"venv\" in the current working directory. You can\nchange the name of the virtual environment by setting the environment\nvariable VENV_NAME.\n\n\"\"\"\n\nfrom __future__ import print_function\n\nimport glob\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nimport time\n\nREQUIREMENTS = [\n    '-e acme[test]',\n    '-e certbot[all]',\n    '-e certbot-apache',\n    '-e certbot-dns-cloudflare',\n    '-e certbot-dns-digitalocean',\n    '-e certbot-dns-dnsimple',\n    '-e certbot-dns-dnsmadeeasy',\n    '-e certbot-dns-gehirn',\n    '-e certbot-dns-google',\n    '-e certbot-dns-linode',\n    '-e certbot-dns-luadns',\n    '-e certbot-dns-nsone',\n    '-e certbot-dns-ovh',\n    '-e certbot-dns-rfc2136',\n    '-e certbot-dns-route53',\n    '-e certbot-dns-sakuracloud',\n    '-e certbot-nginx',\n    '-e certbot-compatibility-test',\n    '-e certbot-ci',\n    '-e letstest',\n]\n\nif sys.platform == 'win32':\n    REQUIREMENTS.remove('-e certbot-apache')\n    REQUIREMENTS.remove('-e certbot-compatibility-test')\n\nVERSION_PATTERN = re.compile(r'^(\\d+)\\.(\\d+).*$')\n\n\nclass PythonExecutableNotFoundError(Exception):\n    pass\n\n\ndef find_python_executable() -> str:\n    \"\"\"\n    Find the relevant python executable that is of the given python major version.\n    Will test, in decreasing priority order:\n\n    * the current Python interpreter\n    * 'pythonX' executable in PATH (with X the given major version) if available\n    * 'python' executable in PATH if available\n    * Windows Python launcher 'py' executable in PATH if available\n\n    Incompatible python versions for Certbot will be evicted (e.g. Python 3\n    versions less than 3.7).\n\n    :rtype: str\n    :return: the relevant python executable path\n    :raise RuntimeError: if no relevant python executable path could be found\n    \"\"\"\n    python_executable_path = None\n\n    # First try, current python executable\n    if _check_version('{0}.{1}.{2}'.format(\n            sys.version_info[0], sys.version_info[1], sys.version_info[2])):\n        return sys.executable\n\n    # Second try, with python executables in path\n    for one_version in ('3', '',):\n        try:\n            one_python = 'python{0}'.format(one_version)\n            output = subprocess.check_output([one_python, '--version'],\n                                             universal_newlines=True, stderr=subprocess.STDOUT)\n            if _check_version(output.strip().split()[1]):\n                return subprocess.check_output([one_python, '-c',\n                                                'import sys; sys.stdout.write(sys.executable);'],\n                                               universal_newlines=True)\n        except (subprocess.CalledProcessError, OSError):\n            pass\n\n    # Last try, with Windows Python launcher\n    try:\n        output_version = subprocess.check_output(['py', '-3', '--version'],\n                                                 universal_newlines=True, stderr=subprocess.STDOUT)\n        if _check_version(output_version.strip().split()[1]):\n            return subprocess.check_output(['py', env_arg, '-c',\n                                            'import sys; sys.stdout.write(sys.executable);'],\n                                           universal_newlines=True)\n    except (subprocess.CalledProcessError, OSError):\n        pass\n\n    if not python_executable_path:\n        raise RuntimeError('Error, no compatible Python executable for Certbot could be found.')\n\n\ndef _check_version(version_str):\n    search = VERSION_PATTERN.search(version_str)\n\n    if not search:\n        return False\n\n    version = (int(search.group(1)), int(search.group(2)))\n\n    if version >= (3, 10):\n        return True\n\n    print('Incompatible python version for Certbot found: {0}'.format(version_str))\n    return False\n\n\ndef subprocess_with_print(cmd, env=None, shell=False):\n    if env is None:\n        env = os.environ\n    print('+ {0}'.format(subprocess.list2cmdline(cmd)) if isinstance(cmd, list) else cmd)\n    subprocess.check_call(cmd, env=env, shell=shell)\n\n\ndef subprocess_output_with_print(cmd, env=None, shell=False):\n    if env is None:\n        env = os.environ\n    print('+ {0}'.format(subprocess.list2cmdline(cmd)) if isinstance(cmd, list) else cmd)\n    return subprocess.check_output(cmd, env=env, shell=shell)\n\n\ndef get_venv_python_path(venv_path):\n    python_linux = os.path.join(venv_path, 'bin/python')\n    if os.path.isfile(python_linux):\n        return os.path.abspath(python_linux)\n    python_windows = os.path.join(venv_path, 'Scripts\\\\python.exe')\n    if os.path.isfile(python_windows):\n        return os.path.abspath(python_windows)\n\n    raise ValueError((\n        'Error, could not find python executable in venv path {0}: is it a valid venv ?'\n        .format(venv_path)))\n\n\ndef prepare_venv_path(venv_name):\n    \"\"\"Determines the venv path and prepares it for use.\n\n    This function cleans up any Python eggs in the current working directory\n    and ensures the venv path is available for use. The path used is the\n    VENV_NAME environment variable if it is set and venv_name otherwise. If\n    there is already a directory at the desired path, the existing directory is\n    renamed by appending a timestamp to the directory name.\n\n    :param str venv_name: The name or path at where the virtual\n        environment should be created if VENV_NAME isn't set.\n\n    :returns: path where the virtual environment should be created\n    :rtype: str\n\n    \"\"\"\n    for path in glob.glob('*.egg-info'):\n        if os.path.isdir(path):\n            shutil.rmtree(path)\n        else:\n            os.remove(path)\n\n    env_venv_name = os.environ.get('VENV_NAME')\n    if env_venv_name:\n        print('Creating venv at {0}'\n              ' as specified in VENV_NAME'.format(env_venv_name))\n        venv_name = env_venv_name\n\n    if os.path.isdir(venv_name):\n        os.rename(venv_name, '{0}.{1}.bak'.format(venv_name, int(time.time())))\n\n    return venv_name\n\n\ndef install_packages(venv_name, pip_args):\n    \"\"\"Installs packages in the given venv.\n\n    :param str venv_name: The name or path at where the virtual\n        environment should be created.\n    :param pip_args: Command line arguments that should be given to\n        pip to install packages\n    :type pip_args: `list` of `str`\n\n    \"\"\"\n    # Using the python executable from venv, we ensure to execute following commands in this venv.\n    py_venv = get_venv_python_path(venv_name)\n    command = [py_venv, os.path.abspath('tools/pip_install.py')]\n    command.extend(pip_args)\n    subprocess_with_print(command)\n\n    if os.path.isdir(os.path.join(venv_name, 'bin')):\n        # Linux/OSX specific\n        print('-------------------------------------------------------------------')\n        print('Please run the following command to activate developer environment:')\n        print('source {0}/bin/activate'.format(venv_name))\n        print('-------------------------------------------------------------------')\n    elif os.path.isdir(os.path.join(venv_name, 'Scripts')):\n        # Windows specific\n        print('---------------------------------------------------------------------------')\n        print('Please run one of the following commands to activate developer environment:')\n        print('{0}\\\\Scripts\\\\activate.bat (for Batch)'.format(venv_name))\n        print('.\\\\{0}\\\\Scripts\\\\Activate.ps1 (for Powershell)'.format(venv_name))\n        print('---------------------------------------------------------------------------')\n    else:\n        raise ValueError('Error, directory {0} is not a valid venv.'.format(venv_name))\n\n\ndef create_venv(venv_path):\n    \"\"\"Create a Python virtual environment at venv_path.\n\n    :param str venv_path: path where the venv should be created\n\n    \"\"\"\n    python = find_python_executable()\n    command = [python, '-m', 'venv', venv_path]\n    subprocess_with_print(command)\n\n\ndef main(pip_args=None):\n    venv_path = prepare_venv_path('venv')\n    create_venv(venv_path)\n\n    if not pip_args:\n        pip_args = REQUIREMENTS\n\n    install_packages(venv_path, pip_args)\n\n\nif __name__ == '__main__':\n    main(sys.argv[1:])\n"
  },
  {
    "path": "towncrier.toml",
    "content": "[tool.towncrier]\npackage = \"certbot\"\ndirectory = \"newsfragments\"\nfilename = \"certbot/CHANGELOG.md\"\ntitle_format = \"## {version} - {project_date}\"\nissue_format = \"[#{issue}](https://github.com/certbot/certbot/issues/{issue})\"\nissue_pattern = \"\\\\d+\"\nignore = []\n\n# Our three custom news fragment types, declared in order of how we'd like them\n# to appear in the changelog\n[[tool.towncrier.type]]\ndirectory = \"added\"\nname = \"Added\"\nshowcontent = true\n\n[[tool.towncrier.type]]\ndirectory = \"changed\"\nname = \"Changed\"\nshowcontent = true\n\n[[tool.towncrier.type]]\ndirectory = \"fixed\"\nname = \"Fixed\"\nshowcontent = true\n"
  },
  {
    "path": "tox.ini",
    "content": "[tox]\nenvlist = {cover,lint}-posix,mypy\n\nskipsdist = true\n\n[base]\npytest = python -m pytest {posargs}\n# Paths are listed on one line because tox seems to have inconsistent\n# behavior with substitutions that contain line continuations, see\n# https://github.com/tox-dev/tox/issues/2069 for more info.\nmypy_strict_source_paths = certbot-ci/src certbot-dns-cloudflare/src certbot-dns-digitalocean/src certbot-dns-dnsimple/src certbot-dns-dnsmadeeasy/src certbot-dns-gehirn/src certbot-dns-google/src certbot-dns-linode/src certbot-dns-luadns/src certbot-dns-nsone/src certbot-dns-ovh/src certbot-dns-rfc2136/src certbot-dns-route53/src certbot-dns-sakuracloud/src\nmypy_no_strict_source_paths = acme/src certbot/src certbot-apache/src certbot-compatibility-test/src certbot-nginx/src\nsource_paths = {[base]mypy_strict_source_paths} {[base]mypy_no_strict_source_paths}\n\n[testenv]\nplatform =\n    posix: ^(?!.*win32).*$\nsetenv =\n    PYTEST_ADDOPTS = {env:PYTEST_ADDOPTS:--numprocesses auto}\n    PYTHONHASHSEED = 0\n# The default install command is python -I -m pip install {opts} {packages}\ninstall_command = python -I {toxinidir}/tools/pip_install.py {opts} {packages}\ndeps =\n    -e acme[test]\n    -e certbot\n    -e certbot[test]\n    -e certbot-apache[dev]\n    -e certbot-dns-cloudflare\n    -e certbot-dns-digitalocean\n    -e certbot-dns-dnsimple\n    -e certbot-dns-dnsmadeeasy\n    -e certbot-dns-gehirn\n    -e certbot-dns-google\n    -e certbot-dns-linode\n    -e certbot-dns-luadns\n    -e certbot-dns-nsone\n    -e certbot-dns-ovh\n    -e certbot-dns-rfc2136\n    -e certbot-dns-route53\n    -e certbot-dns-sakuracloud\n    -e certbot-nginx\nallowlist_externals =\n    echo\n    false\n# This and the next few testenvs are a workaround for\n# https://github.com/tox-dev/tox/issues/2858.\ncommands =\n    echo \"Unrecognized environment name {envname}\"\n    false\n\n[testenv:py{,-posix}]\ncommands =\n    {[base]pytest} acme certbot certbot-dns-cloudflare certbot-dns-digitalocean certbot-dns-dnsimple certbot-dns-dnsmadeeasy certbot-dns-gehirn certbot-dns-google certbot-dns-linode certbot-dns-luadns certbot-dns-nsone certbot-dns-ovh certbot-dns-rfc2136 certbot-dns-route53 certbot-dns-sakuracloud certbot-nginx certbot-apache\n\n[testenv:py3{,10,11,12,13,14}]\ncommands = {[testenv:py]commands}\n\n[testenv:py3.{10,11,12,13,14}]\ncommands = {[testenv:py]commands}\n\n[testenv:oldest]\n# Setting basepython allows the tests to fail fast if that version of Python\n# isn't available instead of potentially trying to use a newer version of\n# Python which is unlikely to work.\n#\n# This version should be kept in sync with the one declared in\n# tools/pinning/oldest/pyproject.toml.\nbasepython = python3.10\nsetenv =\n    {[testenv:py]setenv}\n    CERTBOT_OLDEST=1\ncommands = {[testenv:py]commands}\n\n[testenv:cover]\ncoverage_report = python -m coverage report\ncommon_coverage_report_commands =\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-cloudflare/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-digitalocean/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-dnsimple/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-dnsmadeeasy/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-gehirn/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-google/*\n    {[testenv:cover]coverage_report} --fail-under 100 --include certbot-dns-linode/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-luadns/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-nsone/*\n    {[testenv:cover]coverage_report} --fail-under 98 --include certbot-dns-ovh/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-rfc2136/*\n    {[testenv:cover]coverage_report} --fail-under 94 --include certbot-dns-route53/*\n    {[testenv:cover]coverage_report} --fail-under 99 --include certbot-dns-sakuracloud/*\n    {[testenv:cover]coverage_report} --fail-under 98 --include certbot-nginx/*\ncommands =\n    {[testenv:py]commands} --cov --cov-report=\n    {[testenv:cover]coverage_report} --fail-under 100 --include acme/*\n    {[testenv:cover]coverage_report} --fail-under 95 --include certbot/*\n    {[testenv:cover]coverage_report} --fail-under 100 --include certbot-apache/*\n    {[testenv:cover]common_coverage_report_commands}\n\n# Another workaround for https://github.com/tox-dev/tox/issues/2858 in tox v4.\n[testenv:cover-posix]\ncommands = {[testenv:cover]commands}\n\n[testenv:lint{,-posix}]\ncommands =\n    python -m pylint --reports=n --rcfile=.pylintrc {[base]source_paths}\n    ruff check\n\n[testenv:mypy]\ndeps =\n    {[testenv]deps}\n    -e certbot-ci\ncommands =\n    mypy {[base]mypy_no_strict_source_paths}\n    mypy {[base]mypy_strict_source_paths} --strict\n\n[testenv:isolated-acme]\ndescription = Tests acme without any Certbot components installed\ndeps = -e acme[test]\ncommands = {[base]pytest} acme\n\n[testenv:isolated-certbot]\ndescription = Tests Certbot without any additional plugins installed\ndeps =\n    {[testenv:isolated-acme]deps}\n    -e certbot[test]\ncommands = {[base]pytest} certbot\n\n[testenv:isolated-{apache,cloudflare,digitalocean,dnsimple,dnsmadeeasy,gehirn,google,linode,luadns,nsone,ovh,rfc2136,route53,sakuracloud,nginx}]\ndescription = Tests the plugin without installing any other plugins\ndeps =\n    {[testenv:isolated-certbot]deps}\n    apache: -e certbot-apache[dev]\n    cloudflare: -e certbot-dns-cloudflare\n    digitalocean: -e certbot-dns-digitalocean\n    dnsimple: -e certbot-dns-dnsimple\n    dnsmadeeasy: -e certbot-dns-dnsmadeeasy\n    gehirn: -e certbot-dns-gehirn\n    google: -e certbot-dns-google\n    linode: -e certbot-dns-linode\n    luadns: -e certbot-dns-luadns\n    nsone: -e certbot-dns-nsone\n    ovh: -e certbot-dns-ovh\n    rfc2136: -e certbot-dns-rfc2136\n    route53: -e certbot-dns-route53\n    sakuracloud: -e certbot-dns-sakuracloud\n    nginx: -e certbot-nginx\ncommands =\n    apache: {[base]pytest} certbot-apache\n    cloudflare: {[base]pytest} certbot-dns-cloudflare\n    digitalocean: {[base]pytest} certbot-dns-digitalocean\n    dnsimple: {[base]pytest} certbot-dns-dnsimple\n    dnsmadeeasy: {[base]pytest} certbot-dns-dnsmadeeasy\n    gehirn: {[base]pytest} certbot-dns-gehirn\n    google: {[base]pytest} certbot-dns-google\n    linode: {[base]pytest} certbot-dns-linode\n    luadns: {[base]pytest} certbot-dns-luadns\n    nsone: {[base]pytest} certbot-dns-nsone\n    ovh: {[base]pytest} certbot-dns-ovh\n    rfc2136: {[base]pytest} certbot-dns-rfc2136\n    route53: {[base]pytest} certbot-dns-route53\n    sakuracloud: {[base]pytest} certbot-dns-sakuracloud\n    nginx: {[base]pytest} certbot-nginx\n\n[testenv:apacheconftest]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-apache\ncommands =\n    {toxinidir}/certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test --debian-modules\npassenv =\n    SERVER\nallowlist_externals =\n    {toxinidir}/certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test\n\n[testenv:apacheconftest-external-with-pebble]\ndescription = Run apacheconftest with pebble and Certbot outside of the tox virtual environment.\ndeps =\n    -e certbot-ci\nallowlist_externals =\n    {toxinidir}/certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test-pebble.py\ncommands =\n    {toxinidir}/certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test-pebble.py --debian-modules\n\n[testenv:apacheconftest-with-pebble]\ndeps =\n    {[testenv:apacheconftest]deps}\n    {[testenv:apacheconftest-external-with-pebble]deps}\nallowlist_externals =\n    {toxinidir}/certbot-apache/src/certbot_apache/_internal/tests/apache-conf-files/apache-conf-test-pebble.py\ncommands = {[testenv:apacheconftest-external-with-pebble]commands}\n\n[testenv:nginxroundtrip]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-apache\n    -e certbot-nginx\ncommands =\n    python certbot-compatibility-test/nginx/roundtrip.py certbot-compatibility-test/nginx/nginx-roundtrip-testdata\n\n[testenv:modification]\ndeps =\ncommands =\n    python {toxinidir}/tests/modification-check.py\n\n[testenv:apache_compat]\ndeps =\ncommands =\n    docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile .\n    docker build -t apache-compat -f certbot-compatibility-test/Dockerfile-apache .\n    docker run --rm -t apache-compat -c apache.tar.gz -vvvv\nallowlist_externals =\n    docker\npassenv =\n    DOCKER_*\n\n[testenv:nginx_compat]\ndeps =\ncommands =\n    docker build -t certbot-compatibility-test -f certbot-compatibility-test/Dockerfile .\n    docker build -t nginx-compat -f certbot-compatibility-test/Dockerfile-nginx .\n    docker run --rm -t nginx-compat -c nginx.tar.gz -vv -aie\nallowlist_externals =\n    docker\npassenv =\n    DOCKER_*\n\n[testenv:integration]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-nginx\n    -e certbot-ci\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests \\\n        --cov=acme --cov=certbot --cov=certbot_nginx --cov-report= \\\n        --cov-config=certbot-ci/src/certbot_integration_tests/.coveragerc\n    coverage report --include 'certbot/*' --show-missing --fail-under=65\n    coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74\npassenv = DOCKER_*\n\n[testenv:integration-certbot]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-ci\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests/certbot_tests \\\n        --cov=acme --cov=certbot --cov-report= \\\n        --cov-config=certbot-ci/src/certbot_integration_tests/.coveragerc\n    coverage report --include 'certbot/*' --show-missing --fail-under=62\n\n[testenv:integration-dns-rfc2136]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-dns-rfc2136\n    -e certbot-ci\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests/rfc2136_tests \\\n        --dns-server=bind \\\n        --numprocesses=1 \\\n        --cov=acme --cov=certbot --cov=certbot_dns_rfc2136 --cov-report= \\\n        --cov-config=certbot-ci/src/certbot_integration_tests/.coveragerc\n    coverage report --include 'certbot/*' --show-missing --fail-under=45\n    coverage report --include 'certbot-dns-rfc2136/*' --show-missing --fail-under=86\n\n[testenv:integration-external]\ndescription = Run integration tests with Certbot outside of the tox virtual environment.\ndeps =\n    -e certbot-ci\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests\npassenv = DOCKER_*\n\n[testenv:integration-certbot-oldest]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-ci\nbasepython =\n    {[testenv:oldest]basepython}\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests/certbot_tests\npassenv = DOCKER_*\nsetenv = {[testenv:oldest]setenv}\n\n[testenv:integration-nginx-oldest]\ndeps =\n    -e acme\n    -e certbot\n    -e certbot-nginx\n    -e certbot-ci\nbasepython =\n    {[testenv:oldest]basepython}\ncommands =\n    {[base]pytest} certbot-ci/src/certbot_integration_tests/nginx_tests\npassenv = DOCKER_*\nsetenv = {[testenv:oldest]setenv}\n\n[testenv:test-farm-apache2]\npassenv =\n    AWS_*\nsetenv = AWS_DEFAULT_REGION=us-east-1\nchangedir = letstest\ndeps = -e {toxinidir}/letstest\ncommands = {toxinidir}/tools/retry.sh letstest targets/targets.yaml {env:AWS_EC2_PEM_FILE} SET_BY_ENV scripts/test_apache2.sh --repo {toxinidir}\nallowlist_externals =\n    {toxinidir}/tools/retry.sh\n\n[testenv:validate-changelog]\ndeps = towncrier\n# we set --version here so towncrier doesn't try to guess the certbot version which requires certbot\n# to be installed\ncommands = towncrier build --draft --version 99.99.0\n"
  }
]