Update dependencies and add Let's Encrypt DNS challenges support

This commit is contained in:
Théophile Diot 2024-11-12 14:47:50 +01:00
parent 7d473beefa
commit 6d46635a5e
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
27 changed files with 1548 additions and 1943 deletions

View file

@ -12,6 +12,7 @@
- [FEATURE] Optimized the way the scheduler sends the configuration to the instances to make it faster and more reliable using a ThreadPoolExecutor
- [FEATURE] Add the possibility to set a custom timezone for every service via the `TZ` environment variable (will apply to the logs and all date fields stored in the database). If not set, it will use the local timezone of the server.
- [FEATURE] Add the possibility to run plugins job in async mode to avoid running them in order in the scheduler by setting the `async` key to `true` in the plugin job configuration (default is `false`)
- [FEATURE] Add Let's Encrypt DNS challenges support !
- [SCHEDULER] Refactor the scheduler to use the `BUNKERWEB_INSTANCES` (previously known as `OVERRIDE_INSTANCES`) environment variable instead of an integration specific system
- [AUTOCONF] Add new `NAMESPACES` environment variable to allow setting the namespaces to watch for the autoconf feature which makes it possible to use multiple autoconf instances in the same cluster while keeping the configuration separated
- [AUTOCONF] Add new `USE_KUBERNETES_FQDN` environment variable to allow using the full qualified domain name of the services in Kubernetes instead of the ip address for the hostname of instances (default is yes)
@ -25,11 +26,23 @@
- [DOCS] Updated docs for all new features and changes
- [MISC] Review security headers in the `headers` plugin to improve security
- [MISC] Updated context of `realip`'s `USE_PROXY_PROTOCOL` setting to `global` as it was always applied globally even if set only on a service
- [DEPS] Updated coreruleset-v4 version to v4.7.0
- [DEPS] Updated lua-resty-core version to v0.1.30
- [DEPS] Updated lua-resty-lrucache version to v0.15
- [DEPS] Updated Mbed TLS version to v3.6.2
## v1.5.11 - 2024/11/08
- [BUGFIX] Fix INTERCEPTED_ERROR_CODES to allow empty value
- [UI] Fix missing settings when a service is published online
- [UI] Fix instances always down in instances page
- [AUTOCONF] Fix BW env vars not retrieved
- [AUTOCONF] Fix deadlock on k8s events when there is no ingress
- [LINUX] Increase default worker dict size to avoid crash on RPI
- [MISC] Add WORKERLOCK_MEMORY_SIZE setting for worker dict size
- [MISC] Add API_TIMEOUT and API_READ_TIMEOUT settings to control API timeouts
- [DEPS] Updated coreruleset-v4 version to v4.8.0
- [DEPS] Updated coreruleset-v3 version to v3.3.7
## v1.5.10 - 2024/08/17
- [UI] Fix setup wizard bug related to certificate

View file

@ -1,3 +1,4 @@
docker==7.1.0
kubernetes==31.0.0
pytz==2024.2
urllib3<2.0.0

View file

@ -241,10 +241,11 @@ six==1.16.0 \
# via
# kubernetes
# python-dateutil
urllib3==2.2.3 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# -r requirements.in
# docker
# kubernetes
# requests

View file

@ -1,114 +1,239 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from itertools import chain
from json import dumps
from os import environ, getenv, sep
from os.path import join
from pathlib import Path
from shutil import rmtree
from subprocess import DEVNULL, STDOUT, Popen, run, PIPE
from sys import exit as sys_exit, path as sys_path
from re import MULTILINE, search
from select import select
from shutil import rmtree
from subprocess import DEVNULL, PIPE, STDOUT, Popen, run
from sys import exit as sys_exit, path as sys_path
from typing import Dict, Literal, Type, Union
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
from logger import setup_logger # type: ignore
from pydantic import ValidationError
from models import (
CloudflareProvider,
DigitalOceanProvider,
DnsimpleProvider,
DnsMadeEasyProvider,
GehirnProvider,
GoogleProvider,
LinodeProvider,
LuaDnsProvider,
NSOneProvider,
OvhProvider,
Rfc2136Provider,
Route53Provider,
SakuraCloudProvider,
ScalewayProvider,
WildcardGenerator,
)
from common_utils import bytes_hash # type: ignore
from jobs import Job # type: ignore
from logger import setup_logger # type: ignore
LOGGER = setup_logger("LETS-ENCRYPT.new", getenv("LOG_LEVEL", "INFO"))
CERTBOT_BIN = join(sep, "usr", "share", "bunkerweb", "deps", "python", "bin", "certbot")
DEPS_PATH = join(sep, "usr", "share", "bunkerweb", "deps", "python")
LOGGER_CERTBOT = setup_logger("LETS-ENCRYPT.new.certbot", getenv("LOG_LEVEL", "INFO"))
status = 0
CERTBOT_BIN = join(sep, "usr", "share", "bunkerweb", "deps", "python", "bin", "certbot")
DATA_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt", "etc")
LETS_ENCRYPT_JOBS_PATH = Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt", "jobs")
LETS_ENCRYPT_WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt")
LETS_ENCRYPT_LOGS_DIR = join(sep, "var", "log", "bunkerweb")
PLUGIN_PATH = Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt")
JOBS_PATH = PLUGIN_PATH.joinpath("jobs")
CACHE_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt")
DATA_PATH = CACHE_PATH.joinpath("etc")
WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt")
LOGS_DIR = join(sep, "var", "log", "bunkerweb", "letsencrypt")
def certbot_new(domains: str, email: str, use_letsencrypt_staging: bool = False, *, force: bool = False) -> int:
process = Popen(
[
CERTBOT_BIN,
"certonly",
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
LETS_ENCRYPT_WORK_DIR,
"--logs-dir",
LETS_ENCRYPT_LOGS_DIR,
"--manual",
"--preferred-challenges=http",
"--manual-auth-hook",
LETS_ENCRYPT_JOBS_PATH.joinpath("certbot-auth.py").as_posix(),
"--manual-cleanup-hook",
LETS_ENCRYPT_JOBS_PATH.joinpath("certbot-cleanup.py").as_posix(),
"-n",
"-d",
domains,
"--email",
email,
"--agree-tos",
"--expand",
]
+ (["--staging"] if use_letsencrypt_staging else [])
+ (["--force-renewal"] if force else []),
stdin=DEVNULL,
stderr=PIPE,
universal_newlines=True,
env=environ | {"PYTHONPATH": join(sep, "usr", "share", "bunkerweb", "deps", "python")},
)
def certbot_new(
challenge_type: Literal["dns", "http"],
domains: str,
email: str,
provider: str = None,
credentials_path: Union[str, Path] = None,
propagation: str = "default",
staging: bool = False,
force: bool = False,
) -> int:
if isinstance(credentials_path, str):
credentials_path = Path(credentials_path)
# * Building the certbot command
command = [
CERTBOT_BIN,
"certonly",
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
WORK_DIR,
"--logs-dir",
LOGS_DIR,
"-n",
"-d",
domains,
"--email",
email,
"--agree-tos",
"--expand",
]
env = environ | {"PYTHONPATH": DEPS_PATH}
if challenge_type == "dns":
# * Adding DNS challenge hooks
command.append("--preferred-challenges=dns")
# * Adding the propagation time to the command
if propagation != "default":
if not propagation.isdigit():
LOGGER.warning(f"Invalid propagation time : {propagation}, using provider's default...")
else:
command.extend([f"--dns-{provider}-propagation-seconds", propagation])
# * Adding the credentials to the command
if provider == "route53":
# ? Route53 credentials are different from the others, we need to add them to the environment
with credentials_path.open("r") as file:
for line in file:
key, value = line.strip().split("=", 1)
env[key] = value
else:
command.extend([f"--dns-{provider}-credentials", credentials_path.as_posix()])
# * Adding plugin argument
if provider == "scaleway":
# ? Scaleway plugin uses different arguments
command.extend(["--authenticator", "dns-scaleway"])
else:
command.append(f"--dns-{provider}")
elif challenge_type == "http":
# * Adding HTTP challenge hooks
command.extend(
[
"--manual",
"--preferred-challenges=http",
"--manual-auth-hook",
JOBS_PATH.joinpath("certbot-auth.py").as_posix(),
"--manual-cleanup-hook",
JOBS_PATH.joinpath("certbot-cleanup.py").as_posix(),
]
)
if staging:
command.append("--staging")
if force:
command.append("--force-renewal")
current_date = datetime.now()
process = Popen(command, stdin=DEVNULL, stderr=PIPE, universal_newlines=True, env=env)
while process.poll() is None:
if process.stderr:
for line in process.stderr:
LOGGER_CERTBOT.info(line.strip())
rlist, _, _ = select([process.stderr], [], [], 1) # 1-second timeout
if rlist:
for line in process.stderr:
LOGGER_CERTBOT.info(line.strip())
break
if datetime.now() - current_date > timedelta(seconds=5):
LOGGER.info(
"⏳ Still generating certificate(s)" + (" (this may take a while depending on the provider)" if challenge_type == "dns" else "") + "..."
)
current_date = datetime.now()
return process.returncode
status = 0
IS_MULTISITE = getenv("MULTISITE", "no") == "yes"
try:
# Check if we're using let's encrypt
use_letsencrypt = False
is_multisite = getenv("MULTISITE", "no") == "yes"
all_domains = getenv("SERVER_NAME", "").lower()
server_names = all_domains.split(" ")
servers = getenv("SERVER_NAME", "").lower() or []
if getenv("AUTO_LETS_ENCRYPT", "no") == "yes":
use_letsencrypt = True
elif is_multisite:
for first_server in server_names:
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT", "no")) == "yes":
if isinstance(servers, str):
servers = servers.split(" ")
if not servers:
LOGGER.error("There are no server names, skipping generation...")
sys_exit(0)
use_letsencrypt = False
use_letsencrypt_dns = False
if not IS_MULTISITE:
use_letsencrypt = getenv("AUTO_LETS_ENCRYPT", "no") == "yes"
use_letsencrypt_dns = getenv("LETS_ENCRYPT_CHALLENGE", "http") == "dns"
domains_server_names = {servers[0]: servers}
else:
domains_server_names = {}
for first_server in servers:
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT", "no") == "yes":
use_letsencrypt = True
break
if first_server and getenv(f"{first_server}_LETS_ENCRYPT_CHALLENGE", "http") == "dns":
use_letsencrypt_dns = True
domains_server_names[first_server] = getenv(f"{first_server}_SERVER_NAME", first_server).lower()
if not use_letsencrypt:
LOGGER.info("Let's Encrypt is not activated, skipping generation...")
sys_exit(0)
elif not all_domains:
LOGGER.warning("There are no server names, skipping generation...")
sys_exit(0)
provider_classes = {}
if use_letsencrypt_dns:
provider_classes: Dict[
str,
Union[
Type[CloudflareProvider],
Type[DigitalOceanProvider],
Type[DnsimpleProvider],
Type[DnsMadeEasyProvider],
Type[GehirnProvider],
Type[GoogleProvider],
Type[LinodeProvider],
Type[LuaDnsProvider],
Type[NSOneProvider],
Type[OvhProvider],
Type[Rfc2136Provider],
Type[Route53Provider],
Type[SakuraCloudProvider],
Type[ScalewayProvider],
],
] = {
"cloudflare": CloudflareProvider,
"digitalocean": DigitalOceanProvider,
"dnsimple": DnsimpleProvider,
"dnsmadeeasy": DnsMadeEasyProvider,
"gehirn": GehirnProvider,
"google": GoogleProvider,
"linode": LinodeProvider,
"luadns": LuaDnsProvider,
"nsone": NSOneProvider,
"ovh": OvhProvider,
"rfc2136": Rfc2136Provider,
"route53": Route53Provider,
"sakuracloud": SakuraCloudProvider,
"scaleway": ScalewayProvider,
}
JOB = Job(LOGGER)
# Restore Let's Encrypt data from db cache
# ? Restore data from db cache of certbot-renew job
JOB.restore_cache(job_name="certbot-renew")
domains_to_ask = {}
# Multisite case
if is_multisite:
domains_server_names = {}
for first_server in server_names:
if not first_server or getenv(f"{first_server}_AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT", "no")) != "yes":
continue
domains_server_names[first_server] = getenv(f"{first_server}_SERVER_NAME", first_server).lower()
# Singlesite case
else:
domains_server_names = {server_names[0]: all_domains}
proc = run(
[
CERTBOT_BIN,
@ -116,87 +241,285 @@ try:
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
LETS_ENCRYPT_WORK_DIR,
WORK_DIR,
"--logs-dir",
LETS_ENCRYPT_LOGS_DIR,
LOGS_DIR,
],
stdin=DEVNULL,
stdout=PIPE,
stderr=STDOUT,
text=True,
env=environ | {"PYTHONPATH": join(sep, "usr", "share", "bunkerweb", "deps", "python")},
env=environ | {"PYTHONPATH": DEPS_PATH},
check=False,
)
stdout = proc.stdout
WILDCARD_GENERATOR = WildcardGenerator()
credential_paths = set()
generated_domains = set()
domains_to_ask = {}
if proc.returncode != 0:
LOGGER.error(f"Error while checking certificates :\n{proc.stdout}")
domains_to_ask = {domain: True for domain in server_names}
else:
certificate_blocks = stdout.split("Certificate Name: ")[1:]
for first_server, domains in domains_server_names.items():
generated_domains.update(domains.split(" "))
if getenv(f"{first_server}_USE_LETS_ENCRYPT_WILDCARD", getenv("USE_LETS_ENCRYPT_WILDCARD", "no")) == "yes":
wildcards = WildcardGenerator.get_wildcards_from_domains((first_server,))
first_server = wildcards[0].lstrip("*.")
domains = set(wildcards)
else:
domains = set(domains.split(" "))
current_domains = search(rf"Domains: {first_server}(?P<domains>.*)\n\s*Expiry Date: (?P<expiry_date>.*)$$", stdout, MULTILINE)
if not current_domains:
domains_to_ask[first_server] = False
certificate_block = None
for block in certificate_blocks:
if block.startswith(f"{first_server}\n"):
certificate_block = block
break
if not certificate_block:
domains_to_ask[first_server] = True
LOGGER.warning(f"Certificate block for {first_server} not found, asking new certificate...")
continue
elif set(f"{first_server}{current_domains.groupdict()['domains']}".strip().split(" ")) != set(domains.split(" ")):
try:
cert_domains = search(r"Domains: (?P<domains>.*)\n\s*Expiry Date: (?P<expiry_date>.*)\n", certificate_block, MULTILINE)
except Exception as e:
LOGGER.error(f"[{first_server}] Error while parsing certificate block: {e}")
continue
if not cert_domains:
LOGGER.error(f"[{first_server}] Failed to parse domains and expiry date from certificate block.")
continue
cert_domains_list = cert_domains.group("domains").strip().split()
cert_domains_set = set(cert_domains_list)
if cert_domains_set != domains:
domains_to_ask[first_server] = True
LOGGER.warning(f"Domains for {first_server} are not the same as in the certificate, asking new certificate...")
domains_to_ask[first_server] = True
continue
elif "TEST_CERT" in current_domains.groupdict()["expiry_date"] and getenv(f"{first_server}_"):
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
use_letsencrypt_staging = getenv(f"{first_server}_USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")) == "yes"
if ("TEST_CERT" in current_domains.groupdict()["expiry_date"] and not use_letsencrypt_staging) or (
"TEST_CERT" not in current_domains.groupdict()["expiry_date"] and use_letsencrypt_staging
):
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
is_test_cert = "TEST_CERT" in cert_domains.group("expiry_date")
if (is_test_cert and not use_letsencrypt_staging) or (not is_test_cert and use_letsencrypt_staging):
domains_to_ask[first_server] = True
LOGGER.info(f"Certificates already exists for domain(s) {domains}")
LOGGER.warning(f"Certificate environment (staging/production) changed for {first_server}, asking new certificate...")
continue
letsencrypt_challenge = getenv(f"{first_server}_LETS_ENCRYPT_CHALLENGE", getenv("LETS_ENCRYPT_CHALLENGE", "http"))
letsencrypt_provider = getenv(f"{first_server}_LETS_ENCRYPT_DNS_PROVIDER", getenv("LETS_ENCRYPT_DNS_PROVIDER", ""))
current_provider = search(rf"DNS-01 challenge: {letsencrypt_provider}", certificate_block, MULTILINE)
if letsencrypt_challenge == "dns":
if letsencrypt_provider and (not current_provider or current_provider.group(1) != letsencrypt_provider):
domains_to_ask[first_server] = True
LOGGER.warning(f"Provider for {first_server} is not the same as in the certificate, asking new certificate...")
continue
elif current_provider and letsencrypt_challenge == "http":
domains_to_ask[first_server] = True
LOGGER.warning(f"{first_server} is no longer using DNS challenge, asking new certificate...")
continue
domains_to_ask[first_server] = False
LOGGER.info(f"[{first_server}] Certificates already exist for domain(s) {domains}, expiry date: {cert_domains.group('expiry_date')}")
for first_server, domains in domains_server_names.items():
if first_server not in domains_to_ask:
# * Getting all the necessary data
data = {
"email": getenv(f"{first_server}_EMAIL_LETS_ENCRYPT", getenv("EMAIL_LETS_ENCRYPT", "")) or f"contact@{first_server}",
"challenge": getenv(f"{first_server}_LETS_ENCRYPT_CHALLENGE", getenv("LETS_ENCRYPT_CHALLENGE", "http")),
"staging": getenv(f"{first_server}_USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")) == "yes",
"use_wildcard": getenv(f"{first_server}_USE_LETS_ENCRYPT_WILDCARD", getenv("USE_LETS_ENCRYPT_WILDCARD", "no")) == "yes",
"provider": getenv(f"{first_server}_LETS_ENCRYPT_DNS_PROVIDER", getenv("LETS_ENCRYPT_DNS_PROVIDER", "")),
"propagation": getenv(f"{first_server}_LETS_ENCRYPT_DNS_PROPAGATION", getenv("LETS_ENCRYPT_DNS_PROPAGATION", "default")),
"credential_items": {},
}
if (not data["use_wildcard"] and not domains_to_ask.get(first_server)) or (
data["use_wildcard"] and not domains_to_ask.get(WILDCARD_GENERATOR.get_wildcards_from_domains((first_server,))[0].lstrip("*."))
):
continue
real_email = getenv(f"{first_server}_EMAIL_LETS_ENCRYPT", getenv("EMAIL_LETS_ENCRYPT", f"contact@{first_server}"))
if not real_email:
real_email = f"contact@{first_server}"
# * Getting the DNS provider data if necessary
if data["challenge"] == "dns":
credential_key = f"{first_server}_LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" if IS_MULTISITE else "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM"
for env_key, env_value in environ.items():
if env_value and env_key.startswith(credential_key):
key, value = env_value.split(" ", 1)
data["credential_items"][key.lower()] = value
use_letsencrypt_staging = getenv(f"{first_server}_USE_LETS_ENCRYPT_STAGING", getenv("USE_LETS_ENCRYPT_STAGING", "no")) == "yes"
LOGGER.debug(f"Data for service {first_server} : {dumps(data)}")
LOGGER.info(f"Asking certificates for domain(s) : {domains} (email = {real_email}) to Let's Encrypt {'staging ' if use_letsencrypt_staging else ''}...")
if certbot_new(domains.replace(" ", ","), real_email, use_letsencrypt_staging, force=domains_to_ask[first_server]) != 0:
# * Checking if the DNS data is valid
if data["challenge"] == "dns":
if not data["provider"]:
LOGGER.warning(
f"No provider found for service {first_server} (available providers : {', '.join(provider_classes.keys())}), skipping certificate(s) generation..." # noqa: E501
)
continue
elif data["provider"] not in provider_classes:
LOGGER.warning(
f"Provider {data['provider']} not found for service {first_server} (available providers : {', '.join(provider_classes.keys())}), skipping certificate(s) generation..." # noqa: E501
)
continue
elif not data["credential_items"]:
LOGGER.warning(f"No credentials items found for service {first_server} (you should have at least one), skipping certificate(s) generation...")
continue
# * Validating the credentials
try:
provider = provider_classes[data["provider"]](**data["credential_items"])
except ValidationError as ve:
LOGGER.error(f"Error while validating credentials for service {first_server} :\n{ve}")
continue
content = provider.get_formatted_credentials()
else:
content = b"http_challenge"
# * Adding the domains to Wildcard Generator if necessary
file_type = provider.get_file_type() if data["challenge"] == "dns" else "txt"
file_path = (first_server, f"credentials.{file_type}")
if data["use_wildcard"]:
group = f"{data['provider'] if data['challenge'] == 'dns' else 'http'}_{bytes_hash(content, algorithm='sha1')}"
LOGGER.info(
f"Service {first_server} is using wildcard, "
+ ("the propagation time will be the provider's default and " if data["challenge"] == "dns" else "")
+ "the email will be the same as the first domain that created the group..."
)
WILDCARD_GENERATOR.extend(group, domains.split(" "), data["email"], data["staging"])
file_path = (f"{group}.{file_type}",)
# * Generating the credentials file
credentials_path = CACHE_PATH.joinpath(*file_path)
if data["challenge"] == "dns":
if not credentials_path.is_file():
cached, err = JOB.cache_file(
credentials_path.name, content, job_name="certbot-renew", service_id=first_server if not data["use_wildcard"] else ""
)
if not cached:
LOGGER.error(f"Error while saving service {first_server}'s credentials file in cache : {err}")
continue
LOGGER.info(f"Successfully saved service {first_server}'s credentials file in cache")
elif data["use_wildcard"]:
LOGGER.info(f"Service {first_server}'s wildcard credentials file has already been generated")
else:
old_content = credentials_path.read_bytes()
if old_content != content:
LOGGER.warning(f"Service {first_server}'s credentials file is outdated, updating it...")
cached, err = JOB.cache_file(credentials_path.name, content, job_name="certbot-renew", service_id=first_server)
if not cached:
LOGGER.error(f"Error while updating service {first_server}'s credentials file in cache : {err}")
continue
LOGGER.info(f"Successfully updated service {first_server}'s credentials file in cache")
else:
LOGGER.info(f"Service {first_server}'s credentials file is up to date")
credential_paths.add(credentials_path)
credentials_path.chmod(0o600) # ? Setting the permissions to 600 (this is important to avoid warnings from certbot)
if data["use_wildcard"]:
continue
domains = domains.replace(" ", ",")
LOGGER.info(
f"Asking certificates for domain(s) : {domains} (email = {data['email']}){' using staging' if data['staging'] else ''} with {data['challenge']} challenge..."
)
if (
certbot_new(
data["challenge"],
domains,
data["email"],
data["provider"],
credentials_path,
data["propagation"],
data["staging"],
domains_to_ask[first_server],
)
!= 0
):
status = 2
LOGGER.error(f"Certificate generation failed for domain(s) {domains} ...")
continue
else:
status = 1 if status == 0 else status
LOGGER.info(f"Certificate generation succeeded for domain(s) : {domains}")
# Remove old certificates
generated_domains.update(domains.split(","))
# * Generating the wildcards if necessary
wildcards = WILDCARD_GENERATOR.get_wildcards()
if wildcards:
for group, data in wildcards.items():
if not data:
continue
# * Generating the certificate from the generated credentials
provider = group.split("_", 1)[0]
email = data.pop("email")
credentials_file = CACHE_PATH.joinpath(f"{group}.{provider_classes[provider].get_file_type() if provider in provider_classes else 'txt'}")
for key, domains in data.items():
if not domains:
continue
staging = key == "staging"
LOGGER.info(
f"Asking wildcard certificates for domain(s) : {domains} (email = {email}){' using staging ' if staging else ''} with {'dns' if provider in provider_classes else 'http'} challenge..."
)
if (
certbot_new(
"dns" if provider in provider_classes else "http",
domains,
email,
provider,
credentials_file,
staging=staging,
)
!= 0
):
status = 2
LOGGER.error(f"Certificate generation failed for domain(s) {domains} ...")
else:
status = 1 if status == 0 else status
LOGGER.info(f"Certificate generation succeeded for domain(s) : {domains}")
generated_domains.update(domains.split(","))
else:
LOGGER.info("No wildcard domains found, skipping wildcard certificate(s) generation...")
# * Clearing all missing credentials files
for file in CACHE_PATH.glob("**/*"):
if "etc" in file.parts or not file.is_file() or file.suffix not in (".ini", ".env", ".json"):
continue
# ? If the file is not in the wildcard groups, remove it
if file not in credential_paths:
LOGGER.debug(f"Removing old credentials file {file}")
JOB.del_cache(file.name, job_name="certbot-renew", service_id=file.parent.name if file.parent.name != "letsencrypt" else "")
# * Clearing all no longer needed certificates
if getenv("LETS_ENCRYPT_CLEAR_OLD_CERTS", "no") == "yes":
LOGGER.info("Clear old certificates is activated, removing old / no longer used certificates...")
for elem in chain(DATA_PATH.glob("archive/*"), DATA_PATH.glob("live/*"), DATA_PATH.glob("renewal/*")):
if elem.name.replace(".conf", "") not in generated_domains and elem.name != "README":
cert_name = elem.name.replace(".conf", "")
if cert_name not in generated_domains and cert_name not in domains_to_ask and elem.name != "README":
LOGGER.warning(f"Removing old certificate {elem}")
if elem.is_dir():
rmtree(elem, ignore_errors=True)
else:
elem.unlink(missing_ok=True)
# Save Let's Encrypt data to db cache
# * Save data to db cache
if DATA_PATH.is_dir() and list(DATA_PATH.iterdir()):
cached, err = JOB.cache_dir(DATA_PATH, job_name="certbot-renew")
if not cached:
LOGGER.error(f"Error while saving Let's Encrypt data to db cache : {err}")
LOGGER.error(f"Error while saving data to db cache : {err}")
else:
LOGGER.info("Successfully saved Let's Encrypt data to db cache")
LOGGER.info("Successfully saved data to db cache")
except SystemExit as e:
status = e.code
except BaseException as e:
status = 3
LOGGER.error(f"Exception while running certbot-new.py :\n{e}")
except:
status = 1
LOGGER.exception("Exception while running certbot-new.py")
sys_exit(status)

View file

@ -6,14 +6,7 @@ from pathlib import Path
from subprocess import DEVNULL, PIPE, Popen
from sys import exit as sys_exit, path as sys_path
for deps_path in [
join(sep, "usr", "share", "bunkerweb", *paths)
for paths in (
("deps", "python"),
("utils",),
("db",),
)
]:
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
@ -21,25 +14,29 @@ from logger import setup_logger # type: ignore
from jobs import Job # type: ignore
LOGGER = setup_logger("LETS-ENCRYPT.renew", getenv("LOG_LEVEL", "INFO"))
LIB_PATH = Path(sep, "var", "lib", "bunkerweb", "letsencrypt")
CERTBOT_BIN = join(sep, "usr", "share", "bunkerweb", "deps", "python", "bin", "certbot")
DEPS_PATH = join(sep, "usr", "share", "bunkerweb", "deps", "python")
LOGGER_CERTBOT = setup_logger("LETS-ENCRYPT.renew.certbot", getenv("LOG_LEVEL", "INFO"))
status = 0
CERTBOT_BIN = join(sep, "usr", "share", "bunkerweb", "deps", "python", "bin", "certbot")
DATA_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt", "etc")
LETS_ENCRYPT_WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt")
LETS_ENCRYPT_LOGS_DIR = join(sep, "var", "log", "bunkerweb")
CACHE_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt")
DATA_PATH = CACHE_PATH.joinpath("etc")
WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt")
LOGS_DIR = join(sep, "var", "log", "bunkerweb", "letsencrypt")
try:
# Check if we're using let's encrypt
use_letsencrypt = False
if getenv("MULTISITE", "no") == "yes":
if getenv("MULTISITE", "no") == "no":
use_letsencrypt = getenv("AUTO_LETS_ENCRYPT", "no") == "yes"
else:
for first_server in getenv("SERVER_NAME", "").split(" "):
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT", "no") == "yes":
use_letsencrypt = True
break
elif getenv("AUTO_LETS_ENCRYPT", "no") == "yes":
use_letsencrypt = True
if not use_letsencrypt:
LOGGER.info("Let's Encrypt is not activated, skipping renew...")
@ -55,14 +52,14 @@ try:
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
LETS_ENCRYPT_WORK_DIR,
WORK_DIR,
"--logs-dir",
LETS_ENCRYPT_LOGS_DIR,
LOGS_DIR,
],
stdin=DEVNULL,
stderr=PIPE,
universal_newlines=True,
env=environ | {"PYTHONPATH": join(sep, "usr", "share", "bunkerweb", "deps", "python")},
env=environ | {"PYTHONPATH": DEPS_PATH},
)
while process.poll() is None:
if process.stderr:

View file

@ -0,0 +1,235 @@
# -*- coding: utf-8 -*-
from pathlib import Path
from sys import path as sys_path
from typing import Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict
# Define paths
LIB_PATH = Path("/var/lib/bunkerweb/letsencrypt")
PYTHON_PATH = LIB_PATH / "python"
# Add to sys.path if not already present
python_path_str = PYTHON_PATH.as_posix()
if python_path_str not in sys_path:
sys_path.append(python_path_str)
class Provider(BaseModel):
"""Base class for DNS providers."""
model_config = ConfigDict(extra="allow")
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials to be written to a file."""
return "\n".join(f"{key} = {value}" for key, value in self.model_dump(exclude={"file_type"}).items()).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["ini"]:
"""Return the file type that the credentials should be written to."""
return "ini"
class CloudflareProvider(Provider):
"""Cloudflare DNS provider."""
dns_cloudflare_api_token: str
class DigitalOceanProvider(Provider):
"""DigitalOcean DNS provider."""
dns_digitalocean_token: str
class DnsimpleProvider(Provider):
"""DNSimple DNS provider."""
dns_dnsimple_token: str
class DnsMadeEasyProvider(Provider):
"""DNS Made Easy DNS provider."""
dns_dnsmadeeasy_api_key: str
dns_dnsmadeeasy_secret_key: str
class GehirnProvider(Provider):
"""Gehirn DNS provider."""
dns_gehirn_api_token: str
dns_gehirn_api_secret: str
class GoogleProvider(Provider):
"""Google Cloud DNS provider."""
type: str = "service_account"
project_id: str
private_key_id: str
private_key: str
client_email: str
client_id: str
auth_uri: str = "https://accounts.google.com/o/oauth2/auth"
token_uri: str = "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: str = "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: str
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials in JSON format."""
return self.model_dump_json(indent=2, exclude={"file_type"}).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["json"]:
"""Return the file type that the credentials should be written to."""
return "json"
class LinodeProvider(Provider):
"""Linode DNS provider."""
dns_linode_key: str
dns_linode_version: str = "4"
class LuaDnsProvider(Provider):
"""LuaDns DNS provider."""
dns_luadns_email: str
dns_luadns_token: str
class NSOneProvider(Provider):
"""NS1 DNS provider."""
dns_nsone_api_key: str
class OvhProvider(Provider):
"""OVH DNS provider."""
dns_ovh_endpoint: str = "ovh-eu"
dns_ovh_application_key: str
dns_ovh_application_secret: str
dns_ovh_consumer_key: str
class Rfc2136Provider(Provider):
"""RFC 2136 DNS provider."""
dns_rfc2136_server: str
dns_rfc2136_port: Optional[str] = None
dns_rfc2136_name: str
dns_rfc2136_secret: str
dns_rfc2136_algorithm: str = "HMAC-MD5"
dns_rfc2136_sign_query: str = "false"
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials, excluding defaults."""
return "\n".join(f"{key} = {value}" for key, value in self.model_dump(exclude={"file_type"}, exclude_defaults=True).items()).encode("utf-8")
class Route53Provider(Provider):
"""AWS Route 53 DNS provider."""
aws_access_key_id: str
aws_secret_access_key: str
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials in environment variable format."""
return "\n".join(f"{key.upper()}={value!r}" for key, value in self.model_dump(exclude={"file_type"}).items()).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["env"]:
"""Return the file type that the credentials should be written to."""
return "env"
class SakuraCloudProvider(Provider):
"""Sakura Cloud DNS provider."""
dns_sakuracloud_api_token: str
dns_sakuracloud_api_secret: str
class ScalewayProvider(Provider):
"""Scaleway DNS provider."""
dns_scaleway_application_token: str
class WildcardGenerator:
def __init__(self):
self.__domain_groups = {}
self.__wildcards = {}
def __generate_wildcards(self, staging: bool = False):
self.__wildcards.clear()
_type = "staging" if staging else "prod"
# * Loop through all the domains and generate wildcards
for group, types in self.__domain_groups.items():
if group not in self.__wildcards:
self.__wildcards[group] = {"staging": set(), "prod": set(), "email": types["email"]}
for domain in types[_type]:
parts = domain.split(".")
# ? Only take subdomains into account for wildcards generation
if len(parts) > 2:
suffix = ".".join(parts[1:])
# ? If the suffix is not already in the wildcards, add it
if suffix not in self.__wildcards[group][_type]:
self.__wildcards[group][_type].add(f"*.{suffix}")
self.__wildcards[group][_type].add(suffix)
continue
# ? Add the raw domain to the wildcards
self.__wildcards[group][_type].add(domain)
def extend(self, group: str, domains: List[str], email: str, staging: bool = False):
if group not in self.__domain_groups:
self.__domain_groups[group] = {"staging": set(), "prod": set(), "email": email}
for domain in domains:
if domain := domain.strip():
self.__domain_groups[group]["staging" if staging else "prod"].add(domain)
self.__generate_wildcards(staging)
def get_wildcards(self) -> Dict[str, Dict[Literal["staging", "prod", "email"], str]]:
ret_data = {}
for group, data in self.__wildcards.items():
ret_data[group] = {"email": data["email"]}
for _type, content in data.items():
if _type in ("staging", "prod"):
# ? Sort domains while favoring wildcards first
ret_data[group][_type] = ",".join(sorted(content, key=lambda x: x[0] != "*"))
return ret_data
@staticmethod
def get_wildcards_from_domains(domains: List[str]) -> List[str]:
wildcards = set()
for domain in domains:
parts = domain.split(".")
# ? Only take subdomains into account for wildcards generation
if len(parts) > 2:
suffix = ".".join(parts[1:])
# ? If the suffix is not already in the wildcards, add it
if suffix not in wildcards:
wildcards.add(f"*.{suffix}")
wildcards.add(suffix)
continue
# ? Add the raw domain to the wildcards
wildcards.add(domain)
return sorted(wildcards, key=lambda x: x[0] != "*")
__all__ = (
"CloudflareProvider",
"DigitalOceanProvider",
"GoogleProvider",
"LinodeProvider",
"OvhProvider",
"Rfc2136Provider",
"Route53Provider",
"ScalewayProvider",
)

View file

@ -43,6 +43,8 @@ end
function letsencrypt:init()
local ret_ok, ret_err = true, "success"
local wildcard_servers = {}
if has_variable("AUTO_LETS_ENCRYPT", "yes") then
local multisite, err = get_variable("MULTISITE", false)
if not multisite then
@ -50,26 +52,72 @@ function letsencrypt:init()
end
if multisite == "yes" then
local vars
vars, err = get_multiple_variables({ "AUTO_LETS_ENCRYPT", "SERVER_NAME" })
vars, err = get_multiple_variables({
"AUTO_LETS_ENCRYPT",
"LETS_ENCRYPT_CHALLENGE",
"LETS_ENCRYPT_DNS_PROVIDER",
"USE_LETS_ENCRYPT_WILDCARD",
"SERVER_NAME",
})
if not vars then
return self:ret(false, "can't get AUTO_LETS_ENCRYPT variables : " .. err)
return self:ret(false, "can't get required variables : " .. err)
end
local credential_items
credential_items, err = get_multiple_variables({ "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" })
if not credential_items then
return self:ret(false, "can't get credential items : " .. err)
end
for server_name, multisite_vars in pairs(vars) do
if multisite_vars["AUTO_LETS_ENCRYPT"] == "yes" and server_name ~= "global" then
local check, data = read_files({
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/privkey.pem",
})
if not check then
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
if
multisite_vars["AUTO_LETS_ENCRYPT"] == "yes"
and server_name ~= "global"
and (
multisite_vars["LETS_ENCRYPT_CHALLENGE"] == "http"
or (
multisite_vars["LETS_ENCRYPT_CHALLENGE"] == "dns"
and multisite_vars["LETS_ENCRYPT_DNS_PROVIDER"] ~= ""
and credential_items[server_name]
)
)
then
local data
if multisite_vars["USE_LETS_ENCRYPT_WILDCARD"] == "yes" then
for part in server_name:gmatch("%S+") do
wildcard_servers[part] = true
end
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
data = self.datastore:get("plugin_letsencrypt_" .. server_name, true)
else
check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
for part in server_name:gmatch("%S+") do
wildcard_servers[part] = false
end
end
if not data then
-- Load certificate
local check
check, data = read_files({
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name .. "/privkey.pem",
})
if not check then
self.logger:log(ERR, "error while loading data : " .. err)
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error loading data"
ret_err = "error reading files"
else
if multisite_vars["USE_LETS_ENCRYPT_WILDCARD"] == "yes" then
check, err = self:load_data(data, server_name)
else
check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
end
if not check then
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
end
end
end
@ -80,9 +128,29 @@ function letsencrypt:init()
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local use_wildcard
use_wildcard, err = get_variable("USE_LETS_ENCRYPT_WILDCARD", false)
if not use_wildcard then
return self:ret(false, "can't get USE_LETS_ENCRYPT_WILDCARD variable : " .. err)
end
server_name = server_name:match("%S+")
if use_wildcard == "yes" then
for part in server_name:gmatch("%S+") do
wildcard_servers[part] = true
end
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
else
for part in server_name:gmatch("%S+") do
wildcard_servers[part] = false
end
end
local check, data = read_files({
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name:match("%S+") .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt/etc/live/" .. server_name:match("%S+") .. "/privkey.pem",
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/privkey.pem",
})
if not check then
self.logger:log(ERR, "error while reading files : " .. data)
@ -100,6 +168,12 @@ function letsencrypt:init()
else
ret_err = "let's encrypt is not used"
end
local ok, err = self.datastore:set("plugin_letsencrypt_wildcard_servers", wildcard_servers, nil, true)
if not ok then
return self:ret(false, "error while setting wildcard servers into datastore : " .. err)
end
return self:ret(ret_ok, ret_err)
end
@ -108,6 +182,17 @@ function letsencrypt:ssl_certificate()
if not server_name then
return self:ret(false, "can't get server_name : " .. err)
end
local wildcard_servers, err = self.datastore:get("plugin_letsencrypt_wildcard_servers", true)
if not wildcard_servers then
return self:ret(false, "can't get wildcard servers : " .. err)
end
if wildcard_servers[server_name] then
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
end
local data
data, err = self.datastore:get("plugin_letsencrypt_" .. server_name, true)
if not data and err ~= "not found" then

View file

@ -23,6 +23,70 @@
"regex": "^([^@ \\t\\r\\n]+@[^@ \\t\\r\\n]+\\.[^@ \\t\\r\\n]+)?$",
"type": "text"
},
"LETS_ENCRYPT_CHALLENGE": {
"context": "multisite",
"default": "http",
"help": "The challenge type to use for Let's Encrypt (http or dns).",
"id": "lets-encrypt-challenge",
"label": "Challenge Type",
"regex": "^(http|dns)$",
"type": "select",
"select": ["http", "dns"]
},
"LETS_ENCRYPT_DNS_PROVIDER": {
"context": "multisite",
"default": "",
"help": "The DNS provider to use for DNS challenges.",
"id": "auto-lets-encrypt-dns-provider",
"label": "DNS Provider",
"regex": "^(cloudflare|digitalocean|dnsimple|dnsmadeeasy|gehirn|google|linode|luadns|nsone|ovh|rfc2136|route53|sakuracloud|scaleway)?$",
"type": "select",
"select": [
"",
"cloudflare",
"digitalocean",
"dnsimple",
"dnsmadeeasy",
"gehirn",
"google",
"linode",
"luadns",
"nsone",
"ovh",
"rfc2136",
"route53",
"sakuracloud",
"scaleway"
]
},
"LETS_ENCRYPT_DNS_PROPAGATION": {
"context": "multisite",
"default": "default",
"help": "The time to wait for DNS propagation in seconds for DNS challenges.",
"id": "lets-encrypt-dns-propagation",
"label": "DNS Propagation",
"regex": "^(default|\\d+)$",
"type": "text"
},
"LETS_ENCRYPT_DNS_CREDENTIAL_ITEM": {
"context": "multisite",
"default": "",
"help": "Configuration item that will be added to the credentials.ini file for the DNS provider (e.g. 'cloudflare_api_token 123456') for DNS challenges.",
"id": "lets-encrypt-dns-credential-item",
"label": "Credential Item",
"regex": "^(\\w+ .+)?$",
"type": "password",
"multiple": "lets-encrypt-dns-credential-item"
},
"USE_LETS_ENCRYPT_WILDCARD": {
"context": "multisite",
"default": "no",
"help": "Create wildcard certificates for all domains. This allows a single certificate to secure multiple subdomains.",
"id": "use-lets-encrypt-wildcard",
"label": "Wildcard Certificates",
"regex": "^(yes|no)$",
"type": "check"
},
"USE_LETS_ENCRYPT_STAGING": {
"context": "multisite",
"default": "no",

View file

@ -0,0 +1,166 @@
# -*- coding: utf-8 -*-
from io import BytesIO
from logging import getLogger
from os.path import sep
from pathlib import Path
from shutil import rmtree
from tarfile import open as tar_open
from traceback import format_exc
from typing import Tuple
from uuid import uuid4
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
def extract_cache(folder_path, cache_files):
folder_path.mkdir(parents=True, exist_ok=True)
for cache_file in cache_files:
if cache_file["file_name"].endswith(".tgz") and cache_file["file_name"].startswith("folder:"):
with tar_open(fileobj=BytesIO(cache_file["data"]), mode="r:gz") as tar:
try:
tar.extractall(folder_path, filter="fully_trusted")
except TypeError:
tar.extractall(folder_path)
def retrieve_certificates_info(folder_paths: Tuple[Path, Path]) -> dict:
certificates = {
"domain": [],
"common_name": [],
"issuer": [],
"issuer_server": [],
"valid_from": [],
"valid_to": [],
"serial_number": [],
"fingerprint": [],
"version": [],
"challenge": [],
"authenticator": [],
"key_type": [],
}
for folder_path in folder_paths:
for cert_file in folder_path.joinpath("live").glob("*/fullchain.pem"):
domain = cert_file.parent.name
certificates["domain"].append(domain)
# Default values
cert_info = {
"common_name": "Unknown",
"issuer": "Unknown",
"issuer_server": "Unknown",
"valid_from": None,
"valid_to": None,
"serial_number": "Unknown",
"fingerprint": "Unknown",
"version": "Unknown",
"challenge": "Unknown",
"authenticator": "Unknown",
"key_type": "Unknown",
}
# * Parsing the certificate
try:
cert = x509.load_pem_x509_certificate(cert_file.read_bytes(), default_backend())
# ? Getting the subject
subject = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if subject:
cert_info["common_name"] = subject[0].value
# ? Getting the issuer
issuer = cert.issuer.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if issuer:
cert_info["issuer"] = issuer[0].value
# ? Getting the validity period
cert_info["valid_from"] = cert.not_valid_before.strftime("%d-%m-%Y %H:%M:%S UTC")
cert_info["valid_to"] = cert.not_valid_after.strftime("%d-%m-%Y %H:%M:%S UTC")
# ? Getting the serial number
cert_info["serial_number"] = str(cert.serial_number)
# ? Getting the fingerprint
cert_info["fingerprint"] = cert.fingerprint(hashes.SHA256()).hex()
# ? Getting the version
cert_info["version"] = cert.version.name
except BaseException:
print(f"Error while parsing certificate {cert_file}: {format_exc()}", flush=True)
# * Parsing the renewal configuration
try:
renewal_file = folder_path.joinpath("renewal", f"{domain}.conf")
if renewal_file.exists():
with renewal_file.open("r") as f:
for line in f:
if line.startswith("pref_challs = "):
cert_info["challenge"] = line.split(" = ")[1].strip().split(",")[0]
elif line.startswith("authenticator = "):
cert_info["authenticator"] = line.split(" = ")[1].strip()
elif line.startswith("server = "):
cert_info["issuer_server"] = line.split(" = ")[1].strip()
elif line.startswith("key_type = "):
cert_info["key_type"] = line.split(" = ")[1].strip()
except BaseException:
print(f"Error while parsing renewal configuration {renewal_file}: {format_exc()}", flush=True)
# Append values to corresponding lists in certificates dictionary
for key in cert_info:
certificates[key].append(cert_info[key])
return certificates
def pre_render(app, *args, **kwargs):
logger = getLogger("UI")
ret = {
"list_certificates": {
"data": {
"domain": [],
"common_name": [],
"issuer": [],
"issuer_server": [],
"valid_from": [],
"valid_to": [],
"serial_number": [],
"fingerprint": [],
"version": [],
"challenge": [],
"authenticator": [],
"key_type": [],
},
"order": {
"column": 5,
"dir": "asc",
},
"svg_color": "primary",
"col-size": "col-12",
},
}
root_folder = Path(sep, "var", "tmp", "bunkerweb", "ui")
try:
# ? Fetching Let's Encrypt cache files
regular_cache_files = kwargs["db"].get_jobs_cache_files(job_name="certbot-renew")
# ? Extracting cache files
folder_path = root_folder.joinpath("letsencrypt", str(uuid4()))
regular_le_folder = folder_path.joinpath("regular")
extract_cache(regular_le_folder, regular_cache_files)
# ? We retrieve the certificates from the cache files by parsing the content of the .pem files
ret["list_certificates"]["data"] = retrieve_certificates_info((regular_le_folder,))
except BaseException as e:
logger.debug(format_exc())
logger.error(f"Failed to get Let's Encrypt certificates: {e}")
ret["error"] = str(e)
finally:
if folder_path:
rmtree(root_folder, ignore_errors=True)
return ret

View file

@ -1,380 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from itertools import chain
from json import dumps
from os import environ, getenv, sep
from os.path import join
from pathlib import Path
from shutil import rmtree
from subprocess import DEVNULL, STDOUT, Popen
from sys import exit as sys_exit, path as sys_path
from typing import Dict, List, Literal, Type, Union
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
from common_utils import bytes_hash # type: ignore
from jobs import Job # type: ignore
from logger import setup_logger # type: ignore
LOGGER = setup_logger("LETS-ENCRYPT-DNS.new", getenv("LOG_LEVEL", "INFO"))
LIB_PATH = Path(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
deps_path = LIB_PATH.joinpath("python").as_posix()
if deps_path not in sys_path:
sys_path.append(deps_path)
CERTBOT_BIN = LIB_PATH.joinpath("python", "bin", "certbot")
LOGGER_CERTBOT = setup_logger("LETS-ENCRYPT-DNS.new.certbot", getenv("LOG_LEVEL", "INFO"))
status = 0
PLUGIN_PATH = Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt_dns")
JOBS_PATH = PLUGIN_PATH.joinpath("jobs")
CACHE_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt_dns")
DATA_PATH = CACHE_PATH.joinpath("etc")
WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
LOGS_DIR = join(sep, "var", "log", "bunkerweb", "letsencrypt_dns")
class WildcardGenerator:
def __init__(self):
self.__domain_groups = {}
self.__wildcards = {}
def __generate_wildcards(self, staging: bool = False):
self.__wildcards.clear()
_type = "staging" if staging else "prod"
# * Loop through all the domains and generate wildcards
for group, types in self.__domain_groups.items():
if group not in self.__wildcards:
self.__wildcards[group] = {"staging": set(), "prod": set(), "email": types["email"]}
for domain in types[_type]:
parts = domain.split(".")
# ? Only take subdomains into account for wildcards generation
if len(parts) > 2:
suffix = ".".join(parts[1:])
# ? If the suffix is not already in the wildcards, add it
if suffix not in self.__wildcards[group][_type]:
self.__wildcards[group][_type].add(f"*.{suffix}")
self.__wildcards[group][_type].add(suffix)
continue
# ? Add the raw domain to the wildcards
self.__wildcards[group][_type].add(domain)
def extend(self, group: str, domains: List[str], email: str, staging: bool = False):
if group not in self.__domain_groups:
self.__domain_groups[group] = {"staging": set(), "prod": set(), "email": email}
for domain in domains:
if domain := domain.strip():
self.__domain_groups[group]["staging" if staging else "prod"].add(domain)
self.__generate_wildcards(staging)
def get_wildcards(self) -> Dict[str, Dict[Literal["staging", "prod", "email"], str]]:
ret_data = {}
for group, data in self.__wildcards.items():
ret_data[group] = {"email": data["email"]}
for _type, content in data.items():
if _type in ("staging", "prod"):
# ? Sort domains while favoring wildcards first
ret_data[group][_type] = ",".join(sorted(content, key=lambda x: x[0] != "*"))
return ret_data
def certbot_new(provider: str, credentials_path: Union[str, Path], domains: str, email: str, propagation: str = "default", staging: bool = False) -> int:
if isinstance(credentials_path, str):
credentials_path = Path(credentials_path)
# * Building the certbot command
command = [
CERTBOT_BIN,
"certonly",
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
WORK_DIR,
"--logs-dir",
LOGS_DIR,
"--preferred-challenges=dns",
"-n",
"-d",
domains,
"--email",
email,
"--agree-tos",
"--expand",
]
# * Adding the propagation time to the command
if propagation != "default":
if not propagation.isdigit():
LOGGER.warning(f"Invalid propagation time : {propagation}, using provider's default...")
else:
command.extend([f"--dns-{provider}-propagation-seconds", propagation])
env = environ | {"PYTHONPATH": deps_path}
# * Adding the credentials to the command
if provider == "route53":
# ? Route53 credentials are different from the others, we need to add them to the environment
with credentials_path.open("r") as file:
for line in file:
key, value = line.strip().split("=", 1)
env[key] = value
else:
command.extend([f"--dns-{provider}-credentials", credentials_path.as_posix()])
# * Adding plugin argument
if provider == "scaleway":
# ? Scaleway plugin uses a different argument
command.extend(["--authenticator", "dns-scaleway"])
else:
command.append(f"--dns-{provider}")
if staging:
command.append("--staging")
current_date = datetime.now()
process = Popen(command, stdin=DEVNULL, stderr=STDOUT, universal_newlines=True, env=env)
while process.poll() is None:
if datetime.now() - current_date > timedelta(seconds=5):
LOGGER.info("⏳ Still generating certificate(s)...")
current_date = datetime.now()
return process.returncode
IS_MULTISITE = getenv("MULTISITE", "no") == "yes"
try:
servers = getenv("SERVER_NAME", "").lower() or []
if isinstance(servers, str):
servers = servers.split(" ")
if not servers:
LOGGER.error("There are no server names, skipping generation...")
sys_exit(0)
use_letsencrypt_dns = False
if not IS_MULTISITE:
servers = [servers[0]]
use_letsencrypt_dns = getenv("AUTO_LETS_ENCRYPT_DNS", "no") == "yes"
else:
for first_server in servers:
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT_DNS", "no") == "yes":
use_letsencrypt_dns = True
break
if not use_letsencrypt_dns:
LOGGER.info("Let's Encrypt DNS is not activated, skipping generation...")
sys_exit(0)
elif not CERTBOT_BIN.is_file():
LOGGER.error("Additional dependencies not installed, skipping certificate(s) generation...")
sys_exit(2)
from pydantic import ValidationError
from models import (
CloudflareProvider,
DigitalOceanProvider,
GoogleProvider,
LinodeProvider,
OvhProvider,
Rfc2136Provider,
Route53Provider,
ScalewayProvider,
)
PROVIDER_CLASSES: Dict[
str,
Union[
Type[CloudflareProvider],
Type[DigitalOceanProvider],
Type[GoogleProvider],
Type[LinodeProvider],
Type[OvhProvider],
Type[Rfc2136Provider],
Type[Route53Provider],
Type[ScalewayProvider],
],
] = {
"cloudflare": CloudflareProvider,
"digitalocean": DigitalOceanProvider,
"google": GoogleProvider,
"linode": LinodeProvider,
"ovh": OvhProvider,
"rfc2136": Rfc2136Provider,
"route53": Route53Provider,
"scaleway": ScalewayProvider,
}
JOB = Job(LOGGER)
# ? Restore data from db cache of dns-certbot-renew job
JOB.restore_cache(job_name="dns-certbot-renew")
WILDCARD_GENERATOR = WildcardGenerator()
credential_paths = set()
generated_domains = set()
for first_server in servers:
if getenv(f"{first_server}_AUTO_LETS_ENCRYPT_DNS", getenv("AUTO_LETS_ENCRYPT_DNS", "no")) == "no":
LOGGER.info(f"Skipping certificate(s) generation for {first_server} because it is not enabled")
continue
elif getenv(f"{first_server}_AUTO_LETS_ENCRYPT", getenv("AUTO_LETS_ENCRYPT", "no")) == "yes":
LOGGER.warning(f"Skipping certificate(s) generation for {first_server} because it is using regular Let's Encrypt")
continue
# * Getting all the necessary data
data = {
"domains": getenv(f"{first_server}_SERVER_NAME", getenv("SERVER_NAME", "")).lower() or first_server,
"email": getenv(f"{first_server}_LETS_ENCRYPT_DNS_EMAIL", getenv("LETS_ENCRYPT_DNS_EMAIL", "")) or f"contact@{first_server}",
"staging": getenv(f"{first_server}_USE_LETS_ENCRYPT_DNS_STAGING", getenv("USE_LETS_ENCRYPT_DNS_STAGING", "no")) == "yes",
"provider": getenv(f"{first_server}_LETS_ENCRYPT_DNS_PROVIDER", getenv("LETS_ENCRYPT_DNS_PROVIDER", "")),
"use_wildcard": getenv(f"{first_server}_USE_LETS_ENCRYPT_DNS_WILDCARD", getenv("USE_LETS_ENCRYPT_DNS_WILDCARD", "no")) == "yes",
"propagation": getenv(f"{first_server}_LETS_ENCRYPT_DNS_PROPAGATION", getenv("LETS_ENCRYPT_DNS_PROPAGATION", "default")),
"credential_items": {},
}
for env_key, env_value in environ.items():
if env_value and env_key.startswith(f"{first_server}_LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" if IS_MULTISITE else "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM"):
key, value = env_value.split(" ", 1)
data["credential_items"][key.lower()] = value
LOGGER.debug(f"Data for service {first_server} : {dumps(data)}")
# * Checking if the data is valid
if not data["provider"]:
LOGGER.warning(
f"No provider found for service {first_server} (available providers : {', '.join(PROVIDER_CLASSES.keys())}), skipping certificate(s) generation..." # noqa: E501
)
continue
elif not data["credential_items"]:
LOGGER.warning(f"No credentials items found for service {first_server} (you should have at least one), skipping certificate(s) generation...")
continue
# * Validating the credentials
try:
provider = PROVIDER_CLASSES[data["provider"]](**data["credential_items"])
except ValidationError as ve:
LOGGER.error(f"Error while validating credentials for service {first_server} :\n{ve}")
continue
content = provider.get_formatted_credentials()
# * Adding the domains to Wildcard Generator if necessary
file_path = (first_server, f"credentials.{provider.get_file_type()}")
if data["use_wildcard"]:
group = f"{data['provider']}_{bytes_hash(content, algorithm='sha1')}"
LOGGER.info(
f"Service {first_server} is using wildcard, the propagation time will be the provider's default "
+ "and the email will be the same as the first domain that created the group..."
)
WILDCARD_GENERATOR.extend(group, data["domains"].strip().split(" "), data["email"], data["staging"])
file_path = (f"{group}.{provider.get_file_type()}",)
# * Generating the credentials file
credentials_path = CACHE_PATH.joinpath(*file_path)
if not credentials_path.is_file():
cached, err = JOB.cache_file(
credentials_path.name, content, job_name="dns-certbot-renew", service_id=first_server if not data["use_wildcard"] else ""
)
if not cached:
LOGGER.error(f"Error while saving service {first_server}'s credentials file in cache : {err}")
continue
LOGGER.info(f"Successfully saved service {first_server}'s credentials file in cache")
elif data["use_wildcard"]:
LOGGER.info(f"Service {first_server}'s wildcard credentials file has already been generated")
else:
old_content = credentials_path.read_bytes()
if old_content != content:
LOGGER.warning(f"Service {first_server}'s credentials file is outdated, updating it...")
cached, err = JOB.cache_file(credentials_path.name, content, job_name="dns-certbot-renew", service_id=first_server)
if not cached:
LOGGER.error(f"Error while updating service {first_server}'s credentials file in cache : {err}")
continue
LOGGER.info(f"Successfully updated service {first_server}'s credentials file in cache")
else:
LOGGER.info(f"Service {first_server}'s credentials file is up to date")
credential_paths.add(credentials_path)
credentials_path.chmod(0o600) # ? Setting the permissions to 600 (this is important to avoid warnings from certbot)
if data["use_wildcard"]:
continue
domains = data["domains"].replace(" ", ",")
LOGGER.info(f"Asking certificates for domain(s) : {domains} (email = {data['email']}) {'using staging ' if data['staging'] else ''}...")
if certbot_new(data["provider"], credentials_path, domains, data["email"], data["propagation"], data["staging"]) != 0:
status = 2
LOGGER.error(f"Certificate generation failed for domain(s) {data['domains']} ...")
else:
status = 1 if status == 0 else status
LOGGER.info(f"Certificate generation succeeded for domain(s) : {data['domains']}")
generated_domains.update(data["domains"].split(","))
# * Generating the wildcards if necessary
wildcards = WILDCARD_GENERATOR.get_wildcards()
if wildcards:
for group, data in wildcards.items():
if not data:
continue
# * Generating the certificate from the generated credentials
provider = group.split("_", 1)[0]
email = data.pop("email")
credentials_file = CACHE_PATH.joinpath(f"{group}.{PROVIDER_CLASSES[provider].get_file_type()}")
for key, domains in data.items():
if not domains:
continue
staging = key == "staging"
LOGGER.info(f"Asking wildcard certificates for domain(s) : {domains} (email = {email}) {'using staging ' if staging else ''}...")
if certbot_new(provider, credentials_file, domains, email, staging=staging) != 0:
status = 2
LOGGER.error(f"Certificate generation failed for domain(s) {domains} ...")
else:
status = 1 if status == 0 else status
LOGGER.info(f"Certificate generation succeeded for domain(s) : {domains}")
generated_domains.update(domains.split(","))
else:
LOGGER.info("No wildcard domains found, skipping wildcard certificate(s) generation...")
# * Clearing all missing credentials files
for file in CACHE_PATH.glob("**/*"):
if "etc" in file.parts or not file.is_file() or file.suffix not in (".ini", ".env", ".json"):
continue
# ? If the file is not in the wildcard groups, remove it
if file not in credential_paths:
LOGGER.debug(f"Removing old credentials file {file}")
JOB.del_cache(file.name, job_name="dns-certbot-renew", service_id=file.parent.name if file.parent.name != "letsencrypt_dns" else "")
# * Clearing all no longer needed certificates
if getenv("LETS_ENCRYPT_DNS_CLEAR_OLD_CERTS", "no") == "yes":
LOGGER.info("Clear old certificates is activated, removing old / no longer used certificates...")
for elem in chain(DATA_PATH.glob("archive/*"), DATA_PATH.glob("live/*"), DATA_PATH.glob("renewal/*")):
if elem.name.replace(".conf", "") not in generated_domains and elem.name != "README":
LOGGER.warning(f"Removing old certificate {elem}")
if elem.is_dir():
rmtree(elem, ignore_errors=True)
else:
elem.unlink(missing_ok=True)
# * Save data to db cache
if DATA_PATH.is_dir() and list(DATA_PATH.iterdir()):
cached, err = JOB.cache_dir(DATA_PATH, job_name="dns-certbot-renew")
if not cached:
LOGGER.error(f"Error while saving data to db cache : {err}")
else:
LOGGER.info("Successfully saved data to db cache")
except SystemExit as e:
status = e.code
except:
status = 1
LOGGER.exception("Exception while running certbot-new.py")
sys_exit(status)

View file

@ -1,91 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from os import environ, getenv, sep
from os.path import join
from pathlib import Path
from subprocess import DEVNULL, PIPE, Popen
from sys import exit as sys_exit, path as sys_path
from traceback import format_exc
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
from logger import setup_logger # type: ignore
from jobs import Job # type: ignore
LOGGER = setup_logger("LETS-ENCRYPT-DNS.renew", getenv("LOG_LEVEL", "INFO"))
LOGGER_CERTBOT = setup_logger("LETS-ENCRYPT-DNS.renew.certbot", getenv("LOG_LEVEL", "INFO"))
status = 0
LIB_PATH = Path(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
PLUGIN_PATH = Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt_dns")
JOBS_PATH = PLUGIN_PATH.joinpath("jobs")
DATA_PATH = Path(sep, "var", "cache", "bunkerweb", "letsencrypt_dns", "etc")
WORK_DIR = join(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
LOGS_DIR = join(sep, "var", "log", "bunkerweb", "letsencrypt_dns")
deps_path = LIB_PATH.joinpath("python")
CERTBOT_BIN = deps_path.joinpath("bin", "certbot")
try:
# Check if we're using let's encrypt
use_letsencrypt_dns = False
if not getenv("MULTISITE", "no") == "yes":
use_letsencrypt_dns = getenv("AUTO_LETS_ENCRYPT_DNS", "no") == "yes"
else:
for first_server in getenv("SERVER_NAME", "").split(" "):
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT_DNS", "no") == "yes":
use_letsencrypt_dns = True
break
if not use_letsencrypt_dns:
LOGGER.info("Let's Encrypt DNS is not activated, skipping generation...")
sys_exit(0)
elif not CERTBOT_BIN.is_file():
LOGGER.error("Additional dependencies not installed, skipping certificate(s) generation...")
sys_exit(2)
JOB = Job(LOGGER)
process = Popen(
[
CERTBOT_BIN,
"renew",
"--no-random-sleep-on-renew",
"--config-dir",
DATA_PATH.as_posix(),
"--work-dir",
WORK_DIR,
"--logs-dir",
LOGS_DIR,
],
stdin=DEVNULL,
stderr=PIPE,
universal_newlines=True,
env=environ | {"PYTHONPATH": deps_path.as_posix()},
)
while process.poll() is None:
if process.stderr:
for line in process.stderr:
LOGGER_CERTBOT.info(line.strip())
if process.returncode != 0:
status = 2
LOGGER.error("Certificates renewal failed")
# Save Let's Encrypt DNS data to db cache
if DATA_PATH.is_dir() and list(DATA_PATH.iterdir()):
cached, err = JOB.cache_dir(DATA_PATH)
if not cached:
LOGGER.error(f"Error while saving Let's Encrypt DNS data to db cache : {err}")
else:
LOGGER.info("Successfully saved Let's Encrypt DNS data to db cache")
except SystemExit as e:
status = e.code
except:
status = 2
LOGGER.error(f"Exception while running certbot-renew.py :\n{format_exc()}")
sys_exit(status)

View file

@ -1,167 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime, timedelta
from os import environ, getenv, sep
from os.path import join
from pathlib import Path
from shutil import rmtree
from subprocess import STDOUT, Popen, PIPE, run
from sys import exit as sys_exit, path as sys_path, version_info
for deps_path in [join(sep, "usr", "share", "bunkerweb", *paths) for paths in (("deps", "python"), ("utils",), ("db",))]:
if deps_path not in sys_path:
sys_path.append(deps_path)
from Database import Database # type: ignore
from logger import setup_logger # type: ignore
LOGGER = setup_logger("LETS-ENCRYPT-DNS.install-deps", getenv("LOG_LEVEL", "INFO"))
status = 0
PLUGIN_PATH = Path(sep, "usr", "share", "bunkerweb", "core", "letsencrypt_dns")
LIB_PATH = Path(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
PIP_PATH = LIB_PATH.joinpath("pip")
PYTHON_DEPS_PATH = LIB_PATH.joinpath("python")
try:
# * Check if we're using let's encrypt DNS
all_domains = getenv("SERVER_NAME", "")
if not all_domains:
LOGGER.warning("There are no server names, skipping additional dependencies installation...")
sys_exit(0)
use_letsencrypt_dns = False
is_multisite = getenv("MULTISITE", "no") == "yes"
server_names = all_domains.split(" ")
if not is_multisite:
use_letsencrypt_dns = getenv("AUTO_LETS_ENCRYPT_DNS", "no") == "yes"
else:
for first_server in server_names:
if first_server and getenv(f"{first_server}_AUTO_LETS_ENCRYPT_DNS", getenv("AUTO_LETS_ENCRYPT_DNS", "no")) == "yes":
use_letsencrypt_dns = True
break
if not use_letsencrypt_dns:
LOGGER.info("Let's Encrypt DNS is not activated, skipping additional dependencies installation...")
sys_exit(0)
if PYTHON_DEPS_PATH.is_dir() and list(PYTHON_DEPS_PATH.iterdir()):
LOGGER.info("Additional dependencies already installed, checking for updates...")
deps = {}
with PLUGIN_PATH.joinpath("requirements.in").open("r") as f:
for line in f:
if (line := line.strip()) and not line.startswith("#"):
package, version = line.split("==")
deps[package] = version
all_deps_up_to_date = True
process = Popen(
["python3", "-m", "pip", "freeze", "--local"],
stdout=PIPE,
stderr=STDOUT,
universal_newlines=True,
env=environ | {"PYTHONPATH": PYTHON_DEPS_PATH.as_posix()},
)
while process.poll() is None:
if process.stdout is not None:
for line in process.stdout:
if (line := line.strip()) and not line.startswith("#"):
split = line.split("==")
if len(split) != 2:
continue
package, version = split
if package in deps and deps[package] != version:
LOGGER.info(f"⚠️ {package} is outdated: {version} -> {deps[package]}")
all_deps_up_to_date = False
if process.returncode != 0:
LOGGER.error("❌ Error while checking additional python dependencies, updating just in case...")
elif all_deps_up_to_date:
LOGGER.info("✅ All additional dependencies are up to date")
sys_exit(0)
else:
LOGGER.warning("Some additional dependencies are outdated, updating...")
rmtree(PYTHON_DEPS_PATH, ignore_errors=True)
else:
LOGGER.info("Deps path not found, installing additional dependencies...")
PYTHON_DEPS_PATH.mkdir(parents=True, exist_ok=True)
pip_cmd = ["python3", "-m", "pip"]
cmd_env = environ | {"PYTHONPATH": PYTHON_DEPS_PATH.as_posix()}
if PIP_PATH.joinpath("usr", "local", "bin").is_dir() and PIP_PATH.joinpath("usr", "local", "lib").is_dir():
pip_cmd = [PIP_PATH.joinpath("usr", "local", "bin", "pip3").as_posix()]
cmd_env["PYTHONPATH"] += ":" + PIP_PATH.joinpath("usr", "local", "lib", f"python{version_info.major}.{version_info.minor}", "site-packages").as_posix()
else:
process = run(pip_cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
if process.returncode != 0:
LOGGER.warning("Pip is not installed, installing pip locally...")
process = Popen(
["python3", "-m", "ensurepip", "--root", PIP_PATH.as_posix()],
stdout=PIPE,
stderr=STDOUT,
universal_newlines=True,
)
while process.poll() is None:
if process.stdout is not None:
for line in process.stdout:
LOGGER.debug(line.strip())
if process.returncode != 0:
LOGGER.error("❌ Error while ensuring pip is up to date")
sys_exit(1)
LOGGER.info("✅ Pip installed successfully")
pip_cmd = [PIP_PATH.joinpath("usr", "local", "bin", "pip3").as_posix()]
cmd_env["PYTHONPATH"] += (
":" + PIP_PATH.joinpath("usr", "local", "lib", f"python{version_info.major}.{version_info.minor}", "site-packages").as_posix()
)
LOGGER.info("Installing additional python dependencies...")
current_date = datetime.now()
process = Popen(
pip_cmd
+ [
"install",
"--no-cache-dir",
"--require-hashes",
"--ignore-installed",
"--target",
PYTHON_DEPS_PATH.as_posix(),
"-r",
PLUGIN_PATH.joinpath("requirements.txt").as_posix(),
],
stdout=PIPE,
stderr=STDOUT,
universal_newlines=True,
env=cmd_env,
)
while process.poll() is None:
if process.stdout is not None:
for line in process.stdout:
if datetime.now() - current_date > timedelta(seconds=5):
LOGGER.info("⏳ Still installing additional python dependencies...")
current_date = datetime.now()
LOGGER.debug(line.strip())
if process.returncode != 0:
LOGGER.error("❌ Error while installing additional python dependencies")
sys_exit(1)
LOGGER.info("✅ Additional dependencies installed successfully")
except SystemExit as e:
status = e.code
except:
status = 1
LOGGER.exception("Exception while running install-dependencies.py")
sys_exit(status)

View file

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
from os.path import sep
from pathlib import Path
from sys import path as sys_path
from typing import Literal, Optional
LIB_PATH = Path(sep, "var", "lib", "bunkerweb", "letsencrypt_dns")
PYTHON_PATH = LIB_PATH.joinpath("python")
if PYTHON_PATH.as_posix() not in sys_path:
sys_path.append(PYTHON_PATH.as_posix())
from pydantic import BaseModel, ConfigDict
class Provider(BaseModel):
"""Base class for DNS providers."""
# ? Allow extra fields in the model in case there are additional fields that are not defined in the model.
model_config = ConfigDict(extra="allow")
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials to be written to a file."""
return "\n".join(f"{key} = {value}" for key, value in self.model_dump(exclude={"file_type"}).items()).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["ini"]:
"""Return the file type that the credentials should be written to."""
return "ini"
class CloudflareProvider(Provider):
dns_cloudflare_api_token: str
class DigitalOceanProvider(Provider):
dns_digitalocean_token: str
class GoogleProvider(Provider):
type: str = "service_account"
project_id: str
private_key_id: str
private_key: str
client_email: str
client_id: str
auth_uri: str = "https://accounts.google.com/o/oauth2/auth"
token_uri: str = "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: str = "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: str
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials to be written to a file."""
return self.model_dump_json(indent=2, exclude={"file_type"}).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["json"]:
"""Return the file type that the credentials should be written to."""
return "json"
class LinodeProvider(Provider):
dns_linode_key: str
dns_linode_version: str = "4"
class OvhProvider(Provider):
dns_ovh_endpoint: str = "ovh-eu"
dns_ovh_application_key: str
dns_ovh_application_secret: str
dns_ovh_consumer_key: str
class Rfc2136Provider(Provider):
dns_rfc2136_server: str
dns_rfc2136_port: Optional[str] = None
dns_rfc2136_name: str
dns_rfc2136_secret: str
dns_rfc2136_algorithm: str = "HMAC-MD5"
dns_rfc2136_sign_query: str = "false"
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials to be written to a file."""
# ? Return the formatted credentials as a string. The default values are excluded as they are not required.
return "\n".join(f"{key} = {value}" for key, value in self.model_dump(exclude={"file_type"}, exclude_defaults=True).items()).encode("utf-8")
class Route53Provider(Provider):
aws_access_key_id: str
aws_secret_access_key: str
def get_formatted_credentials(self) -> bytes:
"""Return the formatted credentials to be written to a file."""
# ? Return the formatted credentials as a string. The keys are converted to uppercase and the values are represented as a string.
return "\n".join(f"{key.upper()}={value!r}" for key, value in self.model_dump(exclude={"file_type"}).items()).encode("utf-8")
@staticmethod
def get_file_type() -> Literal["env"]:
"""Return the file type that the credentials should be written to."""
return "env"
class ScalewayProvider(Provider):
dns_scaleway_application_token: str
__ALL__ = (
"CloudflareProvider",
"DigitalOceanProvider",
"GoogleProvider",
"LinodeProvider",
"OvhProvider",
"Rfc2136Provider",
"Route53Provider",
"ScalewayProvider",
)

View file

@ -1,194 +0,0 @@
local class = require("middleclass")
local plugin = require("bunkerweb.plugin")
local ssl = require("ngx.ssl")
local utils = require("bunkerweb.utils")
local letsencrypt_dns = class("letsencrypt_dns", plugin)
-- luacheck: globals ngx
local ngx = ngx
local ERR = ngx.ERR
local parse_pem_cert = ssl.parse_pem_cert
local parse_pem_priv_key = ssl.parse_pem_priv_key
local ssl_server_name = ssl.server_name
local get_variable = utils.get_variable
local get_multiple_variables = utils.get_multiple_variables
local has_variable = utils.has_variable
local has_not_variable = utils.has_not_variable
local read_files = utils.read_files
function letsencrypt_dns:initialize(ctx)
-- Call parent initialize
plugin.initialize(self, "letsencrypt_dns", ctx)
end
function letsencrypt_dns:set()
local https_configured = self.variables["AUTO_LETS_ENCRYPT_DNS"]
if https_configured == "yes" then
self.ctx.bw.https_configured = "yes"
end
return self:ret(true, "set https_configured to " .. https_configured)
end
function letsencrypt_dns:init()
local ret_ok, ret_err = true, "success"
if has_variable("AUTO_LETS_ENCRYPT_DNS", "yes") and has_not_variable("LETS_ENCRYPT_DNS_PROVIDER", "") then
local multisite, err = get_variable("MULTISITE", false)
if not multisite then
return self:ret(false, "can't get MULTISITE variable : " .. err)
end
if multisite == "yes" then
local vars
vars, err = get_multiple_variables({
"AUTO_LETS_ENCRYPT_DNS",
"LETS_ENCRYPT_DNS_PROVIDER",
"USE_LETS_ENCRYPT_DNS_WILDCARD",
"SERVER_NAME",
})
if not vars then
return self:ret(false, "can't get required variables : " .. err)
end
local credential_items
credential_items, err = get_multiple_variables({ "LETS_ENCRYPT_DNS_CREDENTIAL_ITEM" })
if not credential_items then
return self:ret(false, "can't get credential items : " .. err)
end
for server_name, multisite_vars in pairs(vars) do
if
multisite_vars["AUTO_LETS_ENCRYPT_DNS"] == "yes"
and multisite_vars["LETS_ENCRYPT_DNS_PROVIDER"] ~= ""
and credential_items[server_name]
and server_name ~= "global"
then
local data
if multisite_vars["USE_LETS_ENCRYPT_DNS_WILDCARD"] == "yes" then
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
data = self.datastore:get("plugin_letsencrypt_dns_" .. server_name, true)
end
if not data then
-- Load certificate
local check
check, data = read_files({
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/privkey.pem",
})
if not check then
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
if multisite_vars["USE_LETS_ENCRYPT_DNS_WILDCARD"] == "yes" then
check, err = self:load_data(data, server_name)
else
check, err = self:load_data(data, multisite_vars["SERVER_NAME"])
end
if not check then
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
end
end
end
end
else
local server_name
server_name, err = get_variable("SERVER_NAME", false)
if not server_name then
return self:ret(false, "can't get SERVER_NAME variable : " .. err)
end
local use_wildcard
use_wildcard, err = get_variable("USE_LETS_ENCRYPT_DNS_WILDCARD", false)
if not use_wildcard then
return self:ret(false, "can't get USE_LETS_ENCRYPT_DNS_WILDCARD variable : " .. err)
end
server_name = server_name:match("%S+")
if use_wildcard == "yes" then
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
end
local check, data = read_files({
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/fullchain.pem",
"/var/cache/bunkerweb/letsencrypt_dns/etc/live/" .. server_name .. "/privkey.pem",
})
if not check then
self.logger:log(ERR, "error while reading files : " .. data)
ret_ok = false
ret_err = "error reading files"
else
check, err = self:load_data(data, server_name)
if not check then
self.logger:log(ERR, "error while loading data : " .. err)
ret_ok = false
ret_err = "error loading data"
end
end
end
else
ret_err = "let's encrypt dns is not used"
end
return self:ret(ret_ok, ret_err)
end
function letsencrypt_dns:ssl_certificate()
local server_name, err = ssl_server_name()
if not server_name then
return self:ret(false, "can't get server_name : " .. err)
end
local use_wildcard
use_wildcard, err = get_variable("USE_LETS_ENCRYPT_DNS_WILDCARD", false)
if not use_wildcard then
return self:ret(false, "can't get USE_LETS_ENCRYPT_DNS_WILDCARD variable : " .. err)
end
if use_wildcard == "yes" then
local parts = {}
for part in server_name:gmatch("[^.]+") do
table.insert(parts, part)
end
server_name = table.concat(parts, ".", 2)
end
local data
data, err = self.datastore:get("plugin_letsencrypt_dns_" .. server_name, true)
if not data and err ~= "not found" then
return self:ret(
false,
"error while getting plugin_letsencrypt_dns_" .. server_name .. " from datastore : " .. err
)
elseif data then
return self:ret(true, "certificate/key data found", data)
end
return self:ret(true, "let's encrypt dns is not used")
end
function letsencrypt_dns:load_data(data, server_name)
-- Load certificate
local cert_chain, err = parse_pem_cert(data[1])
if not cert_chain then
return false, "error while parsing pem cert : " .. err
end
-- Load key
local priv_key
priv_key, err = parse_pem_priv_key(data[2])
if not priv_key then
return false, "error while parsing pem priv key : " .. err
end
-- Cache data
for key in server_name:gmatch("%S+") do
local cache_key = "plugin_letsencrypt_dns_" .. key
local ok
ok, err = self.datastore:set(cache_key, { cert_chain, priv_key }, nil, true)
if not ok then
return false, "error while setting data into datastore : " .. err
end
end
return true
end
return letsencrypt_dns

View file

@ -1,113 +0,0 @@
{
"id": "letsencrypt_dns",
"name": "Let's Encrypt DNS",
"description": "Automatic creation, renewal and configuration of Let's Encrypt certificates using DNS challenges.",
"version": "0.7",
"stream": "yes",
"settings": {
"AUTO_LETS_ENCRYPT_DNS": {
"context": "multisite",
"default": "no",
"help": "Activate automatic Let's Encrypt DNS.",
"id": "auto-lets-encrypt-dns",
"label": "Automatic Let's Encrypt Dns",
"regex": "^(yes|no)$",
"type": "check"
},
"LETS_ENCRYPT_DNS_EMAIL": {
"context": "multisite",
"default": "",
"help": "The email address to use for Let's Encrypt notifications.",
"id": "lets-encrypt-dns-email",
"label": "Email Address for Notifications",
"regex": "^([^@ \\t\\r\\n]+@[^@ \\t\\r\\n]+\\.[^@ \\t\\r\\n]+)?$",
"type": "text"
},
"USE_LETS_ENCRYPT_DNS_STAGING": {
"context": "multisite",
"default": "no",
"help": "Use the Let's Encrypt staging environment.",
"id": "use-lets-encrypt-dns-staging",
"label": "Use Let's Encrypt DNS Staging",
"regex": "^(yes|no)$",
"type": "check"
},
"LETS_ENCRYPT_DNS_PROVIDER": {
"context": "multisite",
"default": "",
"help": "The DNS provider to use for DNS challenges.",
"id": "auto-lets-encrypt-dns-provider",
"label": "DNS Provider",
"regex": "^(cloudflare|digitalocean|google|linode|ovh|rfc2136|route53|scaleway)?$",
"type": "select",
"select": [
"",
"cloudflare",
"digitalocean",
"google",
"linode",
"ovh",
"rfc2136",
"route53",
"scaleway"
]
},
"USE_LETS_ENCRYPT_DNS_WILDCARD": {
"context": "multisite",
"default": "yes",
"help": "Create wildcard certificates for all domains using DNS challenges.",
"id": "use-lets-encrypt-dns-wildcard",
"label": "Wildcard Certificates",
"regex": "^(yes|no)$",
"type": "check"
},
"LETS_ENCRYPT_DNS_PROPAGATION": {
"context": "multisite",
"default": "default",
"help": "The time to wait for DNS propagation in seconds.",
"id": "lets-encrypt-dns-propagation",
"label": "DNS Propagation",
"regex": "^(default|\\d+)$",
"type": "text"
},
"LETS_ENCRYPT_DNS_CREDENTIAL_ITEM": {
"context": "multisite",
"default": "",
"help": "Configuration item that will be added to the credentials.ini file for the DNS provider (e.g. 'cloudflare_api_token 123456').",
"id": "lets-encrypt-dns-credential-item",
"label": "Credential Item",
"regex": "^(\\w+ .+)?$",
"type": "password",
"multiple": "lets-encrypt-dns-credential-item"
},
"LETS_ENCRYPT_DNS_CLEAR_OLD_CERTS": {
"context": "global",
"default": "no",
"help": "Clear old certificates when renewing.",
"id": "lets-encrypt-dns-clear-old-certs",
"label": "Clear old certificates when they are no longer needed",
"regex": "^(yes|no)$",
"type": "check"
}
},
"jobs": [
{
"name": "install-lets-encrypt-dns-dependencies",
"file": "install-lets-encrypt-dns-dependencies.py",
"every": "once",
"reload": false
},
{
"name": "dns-certbot-new",
"file": "dns-certbot-new.py",
"every": "once",
"reload": false
},
{
"name": "dns-certbot-renew",
"file": "dns-certbot-renew.py",
"every": "day",
"reload": false
}
]
}

View file

@ -1,9 +0,0 @@
certbot-dns-cloudflare==2.11.0
certbot-dns-digitalocean==2.11.0
certbot-dns-google==2.11.0
certbot-dns-linode==2.11.0
certbot-dns-ovh==2.11.0
certbot-dns-rfc2136==2.11.0
certbot-dns-route53==2.11.0
certbot-dns-scaleway==0.0.7
pydantic==2.9.2

View file

@ -1,686 +0,0 @@
#
# This file is autogenerated by pip-compile with Python 3.9
# by the following command:
#
# pip-compile --allow-unsafe --generate-hashes --strip-extras requirements.in
#
acme==2.11.0 \
--hash=sha256:23213ac3074a78862b219e0a30e141fd53238a8bdcf0668bd4dea59b28873fb8 \
--hash=sha256:f4950015cf52ff0de12f37fc28034c7710aca63f64f1696253d2f6cb9f22645e
# via
# certbot
# certbot-dns-cloudflare
# certbot-dns-digitalocean
# certbot-dns-google
# certbot-dns-linode
# certbot-dns-ovh
# certbot-dns-rfc2136
# certbot-dns-route53
# certbot-dns-scaleway
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
attrs==24.2.0 \
--hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
# via jsonlines
beautifulsoup4==4.12.3 \
--hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \
--hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed
# via dns-lexicon
boto3==1.35.24 \
--hash=sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b \
--hash=sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40
# via certbot-dns-route53
botocore==1.35.24 \
--hash=sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b \
--hash=sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3
# via
# boto3
# s3transfer
cachetools==5.5.0 \
--hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \
--hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a
# via google-auth
certbot==2.11.0 \
--hash=sha256:257ae1cb0a534373ca50dd807c9ae96f27660e41379c45afb9b50cab0e6a7a97 \
--hash=sha256:dc4e0a48bcb09448d60362170ca1047cc9a81966da0dd35135f2561f0ea7d5b1
# via
# certbot-dns-cloudflare
# certbot-dns-digitalocean
# certbot-dns-google
# certbot-dns-linode
# certbot-dns-ovh
# certbot-dns-rfc2136
# certbot-dns-route53
# certbot-dns-scaleway
certbot-dns-cloudflare==2.11.0 \
--hash=sha256:2a3e06a692add6aacdc2dfe7fada695483f5fbf4fed073eabd36f3d7745f0c7a \
--hash=sha256:42788044840328de1fe85ea32df1254823f1452e0479a60445fd364f8234a4a9
# via -r requirements.in
certbot-dns-digitalocean==2.11.0 \
--hash=sha256:30e9543baef204e110dacb1e6adf9b1fd04777a14a204bd3cdbfb100c6f6e32a \
--hash=sha256:d5166fc7eb3b3e8a8de4b43e7485d60eda4225db1d525b6f096949d1487e1c6c
# via -r requirements.in
certbot-dns-google==2.11.0 \
--hash=sha256:6af70452913e472f74788e76375ff00a6c427e59c896d7b982732ba33a09a199 \
--hash=sha256:de5fd15b4b60e652ea41e556fccf09376fdc7f881ec7544cb1e25176b2a1a5bf
# via -r requirements.in
certbot-dns-linode==2.11.0 \
--hash=sha256:4727015830ff048e925d2acee26e9bd727e03cb3ceb29d228d45c6602b6f964f \
--hash=sha256:60848af4c336928f0b069d350accae5abd5896118c36920247972b6759aa1ba6
# via -r requirements.in
certbot-dns-ovh==2.11.0 \
--hash=sha256:1a9ccd1d987c0448dd9050a3ac43558569a6d887f2fcde148f2699c4dc624a26 \
--hash=sha256:6be4feb03782bf2dc876319df0a54ee567241d777132546785b1f7c072e6a2df
# via -r requirements.in
certbot-dns-rfc2136==2.11.0 \
--hash=sha256:413a80c09e3a00162d9f7833cb2f5ed3690ae0833e09be84c795b7ee5a357c4b \
--hash=sha256:aaf9f6b387359734b4138f179f96f480889d9a4e2e44fae60c9ebe4d8715f567
# via -r requirements.in
certbot-dns-route53==2.11.0 \
--hash=sha256:2492bd62fbe514a259d4a0b3455d576b267b9a82f26f396af136d9ec2a9c0ba8 \
--hash=sha256:5fd11e3546175574ccc51aaeccb19860e00c633f9bbeb0c4d033bac5553b15bd
# via -r requirements.in
certbot-dns-scaleway==0.0.7 \
--hash=sha256:999dda5b8689277facb77e1757b8a6b207baeecc0ded0c27aea2c51331affc92 \
--hash=sha256:bc0833ed71a5cd314a93f8d02144c54e17066ee1e2b950ed868b14b2211f5e9e
# via -r requirements.in
certifi==2024.8.30 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
--hash=sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9
# via requests
cffi==1.17.1 \
--hash=sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8 \
--hash=sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2 \
--hash=sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1 \
--hash=sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8 \
--hash=sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36 \
--hash=sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17 \
--hash=sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf \
--hash=sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed \
--hash=sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702 \
--hash=sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6 \
--hash=sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d \
--hash=sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b \
--hash=sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e \
--hash=sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be \
--hash=sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c \
--hash=sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683 \
--hash=sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8 \
--hash=sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1 \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655 \
--hash=sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67 \
--hash=sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595 \
--hash=sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41 \
--hash=sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6 \
--hash=sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401 \
--hash=sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6 \
--hash=sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3 \
--hash=sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e \
--hash=sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4 \
--hash=sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964 \
--hash=sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c \
--hash=sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576 \
--hash=sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0 \
--hash=sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3 \
--hash=sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662 \
--hash=sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd \
--hash=sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f \
--hash=sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5 \
--hash=sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14 \
--hash=sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d \
--hash=sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9 \
--hash=sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7 \
--hash=sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382 \
--hash=sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a \
--hash=sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e \
--hash=sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a \
--hash=sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99 \
--hash=sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87 \
--hash=sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b
# via cryptography
charset-normalizer==3.3.2 \
--hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
--hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
--hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
--hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
--hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
--hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
--hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
--hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
--hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
--hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
--hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
--hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
--hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
--hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
--hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
--hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
--hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
--hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
--hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
--hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
--hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
--hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
--hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
--hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
--hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
--hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
--hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
--hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
--hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
--hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
--hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
--hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
--hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
--hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
--hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
--hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
--hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
--hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
--hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
--hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
--hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
--hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
--hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
--hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
--hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
--hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
--hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
--hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
--hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
--hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
--hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
--hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
--hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
--hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
--hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
--hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
--hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
--hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
--hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
--hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
--hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
--hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
--hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
--hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
--hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
--hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
--hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
--hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
--hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
--hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
--hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
--hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
--hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
--hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
--hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
--hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
--hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
--hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
--hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
--hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
--hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
--hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
--hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
--hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
--hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
--hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
--hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
--hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
--hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
--hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
# via requests
cloudflare==2.19.4 \
--hash=sha256:3b6000a01a237c23bccfdf6d20256ea5111ec74a826ae9e74f9f0e5bb5b2383f
# via certbot-dns-cloudflare
configargparse==1.7 \
--hash=sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b \
--hash=sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1
# via certbot
configobj==5.0.9 \
--hash=sha256:03c881bbf23aa07bccf1b837005975993c4ab4427ba57f959afdd9d1a2386848
# via certbot
cryptography==43.0.1 \
--hash=sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494 \
--hash=sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806 \
--hash=sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d \
--hash=sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062 \
--hash=sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2 \
--hash=sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4 \
--hash=sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1 \
--hash=sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85 \
--hash=sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84 \
--hash=sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042 \
--hash=sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d \
--hash=sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962 \
--hash=sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2 \
--hash=sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa \
--hash=sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d \
--hash=sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365 \
--hash=sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96 \
--hash=sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47 \
--hash=sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d \
--hash=sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d \
--hash=sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c \
--hash=sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb \
--hash=sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277 \
--hash=sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172 \
--hash=sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034 \
--hash=sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a \
--hash=sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289
# via
# acme
# certbot
# dns-lexicon
# josepy
# pyopenssl
distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via certbot
dns-lexicon==3.18.0 \
--hash=sha256:aabe320093b4f9a7f7e0e430551ae49c38c9cf99b45ef7e28da238c50106b1a0 \
--hash=sha256:c2b1005a6621a2ec648131d96ec61304b90b98842af9ff62b1840ddf9d0e2c26
# via
# certbot-dns-linode
# certbot-dns-ovh
dnspython==2.6.1 \
--hash=sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50 \
--hash=sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc
# via
# certbot-dns-rfc2136
# dns-lexicon
filelock==3.16.1 \
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
# via tldextract
google-api-core==2.20.0 \
--hash=sha256:ef0591ef03c30bb83f79b3d0575c3f31219001fc9c5cf37024d08310aeffed8a \
--hash=sha256:f74dff1889ba291a4b76c5079df0711810e2d9da81abfdc99957bc961c1eb28f
# via google-api-python-client
google-api-python-client==2.146.0 \
--hash=sha256:41f671be10fa077ee5143ee9f0903c14006d39dc644564f4e044ae96b380bf68 \
--hash=sha256:b1e62c9889c5ef6022f11d30d7ef23dc55100300f0e8aaf8aa09e8e92540acad
# via certbot-dns-google
google-auth==2.35.0 \
--hash=sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f \
--hash=sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a
# via
# certbot-dns-google
# google-api-core
# google-api-python-client
# google-auth-httplib2
google-auth-httplib2==0.2.0 \
--hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \
--hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d
# via google-api-python-client
googleapis-common-protos==1.65.0 \
--hash=sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63 \
--hash=sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0
# via google-api-core
httplib2==0.22.0 \
--hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \
--hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81
# via
# google-api-python-client
# google-auth-httplib2
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via
# requests
# tldextract
importlib-metadata==8.5.0 \
--hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \
--hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7
# via
# certbot
# dns-lexicon
jmespath==1.0.1 \
--hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \
--hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe
# via
# boto3
# botocore
josepy==1.14.0 \
--hash=sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd \
--hash=sha256:d2b36a30f316269f3242f4c2e45e15890784178af5ec54fa3e49cf9234ee22e0
# via
# acme
# certbot
jsonlines==4.0.0 \
--hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \
--hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55
# via cloudflare
jsonpickle==3.3.0 \
--hash=sha256:287c12143f35571ab00e224fa323aa4b090d5a7f086f5f494d7ee9c7eb1a380a \
--hash=sha256:ab467e601e5b1a1cd76f1819d014795165da071744ef30bf3786e9bc549de25a
# via python-digitalocean
mock==5.1.0 \
--hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \
--hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d
# via certbot-dns-scaleway
parsedatetime==2.6 \
--hash=sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455 \
--hash=sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b
# via certbot
proto-plus==1.24.0 \
--hash=sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445 \
--hash=sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12
# via google-api-core
protobuf==5.28.2 \
--hash=sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132 \
--hash=sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f \
--hash=sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece \
--hash=sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0 \
--hash=sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f \
--hash=sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0 \
--hash=sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276 \
--hash=sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7 \
--hash=sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3 \
--hash=sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36 \
--hash=sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d
# via
# google-api-core
# googleapis-common-protos
# proto-plus
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.1 \
--hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \
--hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c
# via google-auth
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
pydantic==2.9.2 \
--hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \
--hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12
# via -r requirements.in
pydantic-core==2.23.4 \
--hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \
--hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \
--hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \
--hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \
--hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \
--hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \
--hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \
--hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \
--hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \
--hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \
--hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \
--hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \
--hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \
--hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \
--hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \
--hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \
--hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \
--hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \
--hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \
--hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \
--hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \
--hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \
--hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \
--hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \
--hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \
--hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \
--hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \
--hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \
--hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \
--hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \
--hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \
--hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \
--hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \
--hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \
--hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \
--hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \
--hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \
--hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \
--hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \
--hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \
--hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \
--hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \
--hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \
--hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \
--hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \
--hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \
--hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \
--hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \
--hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \
--hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \
--hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \
--hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \
--hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \
--hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \
--hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \
--hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \
--hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \
--hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \
--hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \
--hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \
--hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \
--hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \
--hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \
--hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \
--hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \
--hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \
--hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \
--hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \
--hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \
--hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \
--hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \
--hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \
--hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \
--hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \
--hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \
--hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \
--hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \
--hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \
--hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \
--hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \
--hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \
--hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \
--hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \
--hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \
--hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \
--hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \
--hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \
--hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \
--hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607
# via pydantic
pyopenssl==24.2.1 \
--hash=sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95 \
--hash=sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d
# via
# acme
# josepy
pyotp==2.9.0 \
--hash=sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63 \
--hash=sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612
# via dns-lexicon
pyparsing==3.1.4 \
--hash=sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c \
--hash=sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032
# via httplib2
pyrfc3339==1.1 \
--hash=sha256:67196cb83b470709c580bb4738b83165e67c6cc60e1f2e4f286cfcb402a926f4 \
--hash=sha256:81b8cbe1519cdb79bed04910dd6fa4e181faf8c88dff1e1b987b5f7ab23a5b1a
# via
# acme
# certbot
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via botocore
python-digitalocean==1.17.0 \
--hash=sha256:0032168e022e85fca314eb3f8dfaabf82087f2ed40839eb28f1eeeeca5afb1fa \
--hash=sha256:107854fde1aafa21774e8053cf253b04173613c94531f75d5a039ad770562b24
# via certbot-dns-digitalocean
pytz==2024.2 \
--hash=sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a \
--hash=sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725
# via
# acme
# certbot
# pyrfc3339
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via
# cloudflare
# dns-lexicon
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via
# acme
# certbot-dns-scaleway
# cloudflare
# dns-lexicon
# google-api-core
# python-digitalocean
# requests-file
# requests-mock
# tldextract
requests-file==2.1.0 \
--hash=sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658 \
--hash=sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c
# via tldextract
requests-mock==1.12.1 \
--hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \
--hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401
# via certbot-dns-scaleway
rsa==4.9 \
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
# via google-auth
s3transfer==0.10.2 \
--hash=sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6 \
--hash=sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69
# via importlib-metadata
# The following packages are considered to be unsafe in a requirements file:
setuptools==75.1.0 \
--hash=sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2 \
--hash=sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538
# via boto3
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via python-dateutil
soupsieve==2.6 \
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
# via beautifulsoup4
tldextract==5.1.2 \
--hash=sha256:4dfc4c277b6b97fa053899fcdb892d2dc27295851ab5fac4e07797b6a21b2e46 \
--hash=sha256:c9e17f756f05afb5abac04fe8f766e7e70f9fe387adb1859f0f52408ee060200
# via dns-lexicon
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# pydantic
# pydantic-core
uritemplate==4.1.1 \
--hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \
--hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e
# via google-api-python-client
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# botocore
# requests
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
# via
# acme
# certbot
# certbot-dns-cloudflare
# certbot-dns-digitalocean
# certbot-dns-google
# certbot-dns-linode
# certbot-dns-ovh
# certbot-dns-rfc2136
# certbot-dns-route53
# certbot-dns-scaleway

View file

@ -4,3 +4,4 @@ python-dotenv==1.0.1
pytz==2024.2
redis==5.2.0
requests==2.32.3
urllib3<2.0.0

View file

@ -210,7 +210,9 @@ requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via -r requirements.in
urllib3==2.2.3 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
# via requests
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# -r requirements.in
# requests

View file

@ -4,4 +4,5 @@ pip-tools==7.4.1
pip-upgrader==1.4.15
setuptools==75.3.0
tomli==2.0.2
wheel==0.44.0
urllib3<2.0.0
wheel==0.45.0

View file

@ -197,19 +197,21 @@ toposort==1.10 \
--hash=sha256:bfbb479c53d0a696ea7402601f4e693c97b0367837c8898bc6471adfca37a6bd \
--hash=sha256:cbdbc0d0bee4d2695ab2ceec97fe0679e9c10eab4b2a87a9372b929e70563a87
# via pip-compile-multi
urllib3==2.2.3 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
# via requests
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# -r requirements-deps.in
# requests
wheel==0.45.0 \
--hash=sha256:52f0baa5e6522155090a09c6bd95718cc46956d1b51d537ea5454249edb671c7 \
--hash=sha256:a57353941a3183b3d5365346b567a260a0602a0f8a635926a7dede41b94c674a
# via
# -r requirements-deps.in
# pip-tools
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
zipp==3.21.0 \
--hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
--hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
# via
# -r requirements-deps.in
# pip-tools

View file

@ -1,4 +1,5 @@
pip==24.3.1
pip-tools==7.4.1
setuptools==75.3.0
wheel==0.44.0
urllib3<2.0.0
wheel==0.45.0

View file

@ -48,15 +48,19 @@ tomli==2.0.2 \
# via
# build
# pip-tools
wheel==0.44.0 \
--hash=sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f \
--hash=sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via -r requirements.in
wheel==0.45.0 \
--hash=sha256:52f0baa5e6522155090a09c6bd95718cc46956d1b51d537ea5454249edb671c7 \
--hash=sha256:a57353941a3183b3d5365346b567a260a0602a0f8a635926a7dede41b94c674a
# via
# -r requirements.in
# pip-tools
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
zipp==3.21.0 \
--hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
--hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
# via
# -r requirements.in
# pip-tools

View file

@ -1,6 +1,21 @@
certbot==3.0.0
certbot-dns-cloudflare==3.0.0
certbot-dns-digitalocean==3.0.0
certbot-dns-dnsimple==3.0.0
certbot-dns-dnsmadeeasy==3.0.0
certbot-dns-gehirn==3.0.0
certbot-dns-google==3.0.0
certbot-dns-linode==3.0.0
certbot-dns-luadns==3.0.0
certbot-dns-nsone==3.0.0
certbot-dns-ovh==3.0.0
certbot-dns-rfc2136==3.0.0
certbot-dns-route53==3.0.0
certbot-dns-sakuracloud==3.0.0
certbot-dns-scaleway==0.0.7
cryptography==43.0.3
maxminddb==2.6.2
pydantic==2.9.2
python-magic==0.4.27
requests==2.32.3
schedule==1.2.2

View file

@ -7,10 +7,122 @@
acme==3.0.0 \
--hash=sha256:2bb48ad117b4190600e9fb5c372e3b658cd320bc445d5dba493be20db490d9e4 \
--hash=sha256:e8ebff13b8ae85d2a700466a01c01ce68da72cec8e82031fd94b9aeb4966c0f1
# via certbot
# via
# 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-dns-scaleway
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
attrs==24.2.0 \
--hash=sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346 \
--hash=sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2
# via jsonlines
beautifulsoup4==4.12.3 \
--hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \
--hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed
# via dns-lexicon
boto3==1.35.57 \
--hash=sha256:9edf49640c79a05b0a72f4c2d1e24dfc164344b680535a645f455ac624dc3680 \
--hash=sha256:db58348849a5af061f0f5ec9c3b699da5221ca83354059fdccb798e3ddb6b62a
# via certbot-dns-route53
botocore==1.35.57 \
--hash=sha256:92ddd02469213766872cb2399269dd20948f90348b42bf08379881d5e946cc34 \
--hash=sha256:d96306558085baf0bcb3b022d7a8c39c93494f031edb376694d2b2dcd0e81327
# via
# boto3
# s3transfer
cachetools==5.5.0 \
--hash=sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292 \
--hash=sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a
# via google-auth
certbot==3.0.0 \
--hash=sha256:18b01f12b6278d19bad416fb9435a7e192b37a5081528473031193502f920c97 \
--hash=sha256:480014af69ac507c1ff4b892d23159a98d8d2ff3f70aedc3e6e56cf170878f5b
# via
# -r requirements.in
# 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-dns-scaleway
certbot-dns-cloudflare==3.0.0 \
--hash=sha256:5a2540997a676e7bbb426953734d29dddb4cee8940b85fa4c5c019b88647f995 \
--hash=sha256:e7b008c9631c44d1124905ff9e6027ee7ea265b2140b37d245d1a321c9d2c686
# via -r requirements.in
certbot-dns-digitalocean==3.0.0 \
--hash=sha256:17eaee8033686a85414ee248c705402cd5c80c9b2bb2fd27019e08852dce618d \
--hash=sha256:56febedb1ec16338fb72444c2a56ebaeabccb624c32dad97fc4b7070d79d07ac
# via -r requirements.in
certbot-dns-dnsimple==3.0.0 \
--hash=sha256:4bb52867286741c46298aa2b49d1e45141c8b674fd4ddf7d6561b3c464e043b9 \
--hash=sha256:e8609d66614dab0a70a464a4f3a1f886863435bbdf50039651b4c42c5d80ba8b
# via -r requirements.in
certbot-dns-dnsmadeeasy==3.0.0 \
--hash=sha256:9753c369e779eb15465915d215a536e47cc5de124a554a8bba6e33dfc7324b7c \
--hash=sha256:9d986c6a2bbde6c42c24eb3cdfa7791194ef88a71dcf4e5f22e3907017b8d0ae
# via -r requirements.in
certbot-dns-gehirn==3.0.0 \
--hash=sha256:b1ee3a12cc70afeeb060ce9e34f367b3b4d3b211a7ca1e60a04865c75598edbd \
--hash=sha256:f612e2a5f5e9327365272e88aa8eb9a544d2dc4d0869bc628296addc678ad782
# via -r requirements.in
certbot-dns-google==3.0.0 \
--hash=sha256:12a3bfce2f1a18406a6e333c46420a7a33c0fe0e23c49017a0cc6c9bc305e84d \
--hash=sha256:b2206dfaaa7bc34b64f9f3876ff0844bd96c7c702564c61456a2f3b21ac78786
# via -r requirements.in
certbot-dns-linode==3.0.0 \
--hash=sha256:32bbbc54aee72b1a0d075f4c18cda5a5e54d4f672a8e060f825c04ec332ab179 \
--hash=sha256:d0c43c01542b1b49b147055204a6950da972268d173734c6cbfe4ac679f9bfa8
# via -r requirements.in
certbot-dns-luadns==3.0.0 \
--hash=sha256:0895a1f8a5dc2cbcd4793a8f4fa8fa3d42494b8402216500dbbfdc3287bc538f \
--hash=sha256:fc914ad69b4d16f637153b80a40afc559354189899b45329c0d993dab8f84b1e
# via -r requirements.in
certbot-dns-nsone==3.0.0 \
--hash=sha256:11831fe028355e7d5b7ad7ef51c268afbc3d316c330e586fcb8af05a61efba81 \
--hash=sha256:504c167bccaa746d74ae3c078e5e2cbc7d4998ddb0074f3829e930239f1745b0
# via -r requirements.in
certbot-dns-ovh==3.0.0 \
--hash=sha256:49e05c4a23acbc1bd6ed82e6d9fec2b1e9190ee89480ec975b0535b479894015 \
--hash=sha256:7f115b2428c6e68aa35b29220ceeebdd2d3ac28131e361edaf6589c46203d45c
# via -r requirements.in
certbot-dns-rfc2136==3.0.0 \
--hash=sha256:08219637dac3b8c9215e76855ddd98fde7bc2f100fb3c89c49bf11c530abb0c5 \
--hash=sha256:140b02d5a2db3fef9c88d00c59a39ef34b8447b83228db17224f3a5d54c518aa
# via -r requirements.in
certbot-dns-route53==3.0.0 \
--hash=sha256:4d234462963ed56a4161bda039c24bd8c984e999f42c26fb2e18263416220ecf \
--hash=sha256:ea47ff36513478e28c590276c3605b4f592d9b9938d36f2231484c63b38249e2
# via -r requirements.in
certbot-dns-sakuracloud==3.0.0 \
--hash=sha256:635eac33dc3c99e2eabb0ab124007d23559d54ac7bfd473712b917ee7e17f07e \
--hash=sha256:65cea7b612fe62badfe3fe0ded67cb2a6613ced68bbdc55a5c55c722e8cc6844
# via -r requirements.in
certbot-dns-scaleway==0.0.7 \
--hash=sha256:999dda5b8689277facb77e1757b8a6b207baeecc0ded0c27aea2c51331affc92 \
--hash=sha256:bc0833ed71a5cd314a93f8d02144c54e17066ee1e2b950ed868b14b2211f5e9e
# via -r requirements.in
certifi==2024.8.30 \
--hash=sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8 \
@ -192,6 +304,9 @@ charset-normalizer==3.4.0 \
--hash=sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079 \
--hash=sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482
# via requests
cloudflare==2.19.4 \
--hash=sha256:3b6000a01a237c23bccfdf6d20256ea5111ec74a826ae9e74f9f0e5bb5b2383f
# via certbot-dns-cloudflare
configargparse==1.7 \
--hash=sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b \
--hash=sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1
@ -231,26 +346,97 @@ cryptography==43.0.3 \
# -r requirements.in
# acme
# certbot
# dns-lexicon
# josepy
# pyopenssl
distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via certbot
dns-lexicon==3.18.0 \
--hash=sha256:aabe320093b4f9a7f7e0e430551ae49c38c9cf99b45ef7e28da238c50106b1a0 \
--hash=sha256:c2b1005a6621a2ec648131d96ec61304b90b98842af9ff62b1840ddf9d0e2c26
# via
# certbot-dns-dnsimple
# certbot-dns-dnsmadeeasy
# certbot-dns-gehirn
# certbot-dns-linode
# certbot-dns-luadns
# certbot-dns-nsone
# certbot-dns-ovh
# certbot-dns-sakuracloud
dnspython==2.7.0 \
--hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 \
--hash=sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1
# via
# certbot-dns-rfc2136
# dns-lexicon
filelock==3.16.1 \
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
# via tldextract
google-api-core==2.22.0 \
--hash=sha256:26f8d76b96477db42b55fd02a33aae4a42ec8b86b98b94969b7333a2c828bf35 \
--hash=sha256:a6652b6bd51303902494998626653671703c420f6f4c88cfd3f50ed723e9d021
# via google-api-python-client
google-api-python-client==2.151.0 \
--hash=sha256:4427b2f47cd88b0355d540c2c52215f68c337f3bc9d6aae1ceeae4525977504c \
--hash=sha256:a9d26d630810ed4631aea21d1de3e42072f98240aaf184a8a1a874a371115034
# via certbot-dns-google
google-auth==2.36.0 \
--hash=sha256:51a15d47028b66fd36e5c64a82d2d57480075bccc7da37cde257fc94177a61fb \
--hash=sha256:545e9618f2df0bcbb7dcbc45a546485b1212624716975a1ea5ae8149ce769ab1
# via
# certbot-dns-google
# google-api-core
# google-api-python-client
# google-auth-httplib2
google-auth-httplib2==0.2.0 \
--hash=sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05 \
--hash=sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d
# via google-api-python-client
googleapis-common-protos==1.65.0 \
--hash=sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63 \
--hash=sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0
# via google-api-core
httplib2==0.22.0 \
--hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \
--hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81
# via
# google-api-python-client
# google-auth-httplib2
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
# via requests
# via
# requests
# tldextract
importlib-metadata==8.5.0 \
--hash=sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b \
--hash=sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7
# via certbot
# via
# certbot
# dns-lexicon
jmespath==1.0.1 \
--hash=sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980 \
--hash=sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe
# via
# boto3
# botocore
josepy==1.14.0 \
--hash=sha256:308b3bf9ce825ad4d4bba76372cf19b5dc1c2ce96a9d298f9642975e64bd13dd \
--hash=sha256:d2b36a30f316269f3242f4c2e45e15890784178af5ec54fa3e49cf9234ee22e0
# via
# acme
# certbot
jsonlines==4.0.0 \
--hash=sha256:0c6d2c09117550c089995247f605ae4cf77dd1533041d366351f6f298822ea74 \
--hash=sha256:185b334ff2ca5a91362993f42e83588a360cf95ce4b71a73548502bda52a7c55
# via cloudflare
jsonpickle==3.4.2 \
--hash=sha256:2efa2778859b6397d5804b0a98d52cd2a7d9a70fcb873bc5a3ca5acca8f499ba \
--hash=sha256:fd6c273278a02b3b66e3405db3dd2f4dbc8f4a4a3123bfcab3045177c6feb9c3
# via python-digitalocean
maxminddb==2.6.2 \
--hash=sha256:058ca89789bc1770fe58d02a88272ca91dabeef9f3fe0011fe506484355f1804 \
--hash=sha256:05e873eb82281cef6e787bd40bd1d58b2e496a21b3689346f0d0420988b3cbb1 \
@ -322,26 +508,171 @@ maxminddb==2.6.2 \
--hash=sha256:f412a54f87ef9083911c334267188d3d1b14f2591eac94b94ca32528f21d5f25 \
--hash=sha256:fb38aa94e76a87785b654c035f9f3ee39b74a98e9beea9a10b1aa62abdcc4cbd
# via -r requirements.in
mock==5.1.0 \
--hash=sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744 \
--hash=sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d
# via certbot-dns-scaleway
parsedatetime==2.6 \
--hash=sha256:4cb368fbb18a0b7231f4d76119165451c8d2e35951455dfee97c62a87b04d455 \
--hash=sha256:cb96edd7016872f58479e35879294258c71437195760746faffedb692aef000b
# via certbot
proto-plus==1.25.0 \
--hash=sha256:c91fc4a65074ade8e458e95ef8bac34d4008daa7cce4a12d6707066fca648961 \
--hash=sha256:fbb17f57f7bd05a68b7707e745e26528b0b3c34e378db91eef93912c54982d91
# via google-api-core
protobuf==5.28.3 \
--hash=sha256:0c4eec6f987338617072592b97943fdbe30d019c56126493111cf24344c1cc24 \
--hash=sha256:135658402f71bbd49500322c0f736145731b16fc79dc8f367ab544a17eab4535 \
--hash=sha256:27b246b3723692bf1068d5734ddaf2fccc2cdd6e0c9b47fe099244d80200593b \
--hash=sha256:3e6101d095dfd119513cde7259aa703d16c6bbdfae2554dfe5cfdbe94e32d548 \
--hash=sha256:3fa2de6b8b29d12c61911505d893afe7320ce7ccba4df913e2971461fa36d584 \
--hash=sha256:64badbc49180a5e401f373f9ce7ab1d18b63f7dd4a9cdc43c92b9f0b481cef7b \
--hash=sha256:70585a70fc2dd4818c51287ceef5bdba6387f88a578c86d47bb34669b5552c36 \
--hash=sha256:712319fbdddb46f21abb66cd33cb9e491a5763b2febd8f228251add221981135 \
--hash=sha256:91fba8f445723fcf400fdbe9ca796b19d3b1242cd873907979b9ed71e4afe868 \
--hash=sha256:a3f6857551e53ce35e60b403b8a27b0295f7d6eb63d10484f12bc6879c715687 \
--hash=sha256:cee1757663fa32a1ee673434fcf3bf24dd54763c79690201208bafec62f19eed
# via
# google-api-core
# googleapis-common-protos
# proto-plus
pyasn1==0.6.1 \
--hash=sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629 \
--hash=sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034
# via
# pyasn1-modules
# rsa
pyasn1-modules==0.4.1 \
--hash=sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd \
--hash=sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c
# via google-auth
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
# via cffi
pydantic==2.9.2 \
--hash=sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f \
--hash=sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12
# via -r requirements.in
pydantic-core==2.23.4 \
--hash=sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36 \
--hash=sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05 \
--hash=sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071 \
--hash=sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327 \
--hash=sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c \
--hash=sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36 \
--hash=sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29 \
--hash=sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744 \
--hash=sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d \
--hash=sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec \
--hash=sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e \
--hash=sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e \
--hash=sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577 \
--hash=sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232 \
--hash=sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863 \
--hash=sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6 \
--hash=sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368 \
--hash=sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480 \
--hash=sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2 \
--hash=sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2 \
--hash=sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6 \
--hash=sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769 \
--hash=sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d \
--hash=sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2 \
--hash=sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84 \
--hash=sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166 \
--hash=sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271 \
--hash=sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5 \
--hash=sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb \
--hash=sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13 \
--hash=sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323 \
--hash=sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556 \
--hash=sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665 \
--hash=sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef \
--hash=sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb \
--hash=sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119 \
--hash=sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126 \
--hash=sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510 \
--hash=sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b \
--hash=sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87 \
--hash=sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f \
--hash=sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc \
--hash=sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8 \
--hash=sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21 \
--hash=sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f \
--hash=sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6 \
--hash=sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658 \
--hash=sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b \
--hash=sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3 \
--hash=sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb \
--hash=sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59 \
--hash=sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24 \
--hash=sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9 \
--hash=sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3 \
--hash=sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd \
--hash=sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753 \
--hash=sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55 \
--hash=sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad \
--hash=sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a \
--hash=sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605 \
--hash=sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e \
--hash=sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b \
--hash=sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433 \
--hash=sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8 \
--hash=sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07 \
--hash=sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728 \
--hash=sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0 \
--hash=sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327 \
--hash=sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555 \
--hash=sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64 \
--hash=sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6 \
--hash=sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea \
--hash=sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b \
--hash=sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df \
--hash=sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e \
--hash=sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd \
--hash=sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068 \
--hash=sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3 \
--hash=sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040 \
--hash=sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12 \
--hash=sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916 \
--hash=sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f \
--hash=sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f \
--hash=sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801 \
--hash=sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231 \
--hash=sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5 \
--hash=sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8 \
--hash=sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee \
--hash=sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607
# via pydantic
pyopenssl==24.2.1 \
--hash=sha256:4247f0dbe3748d560dcbb2ff3ea01af0f9a1a001ef5f7c4c647956ed8cbf0e95 \
--hash=sha256:967d5719b12b243588573f39b0c677637145c7a1ffedcd495a487e58177fbb8d
# via
# acme
# josepy
pyotp==2.9.0 \
--hash=sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63 \
--hash=sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612
# via dns-lexicon
pyparsing==3.2.0 \
--hash=sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84 \
--hash=sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c
# via httplib2
pyrfc3339==2.0.1 \
--hash=sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d \
--hash=sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b
# via
# acme
# certbot
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
# via botocore
python-digitalocean==1.17.0 \
--hash=sha256:0032168e022e85fca314eb3f8dfaabf82087f2ed40839eb28f1eeeeca5afb1fa \
--hash=sha256:107854fde1aafa21774e8053cf253b04173613c94531f75d5a039ad770562b24
# via certbot-dns-digitalocean
python-magic==0.4.27 \
--hash=sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b \
--hash=sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3
@ -352,12 +683,93 @@ pytz==2024.2 \
# via
# acme
# certbot
pyyaml==6.0.2 \
--hash=sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086 \
--hash=sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e \
--hash=sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133 \
--hash=sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5 \
--hash=sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484 \
--hash=sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68 \
--hash=sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a \
--hash=sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf \
--hash=sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99 \
--hash=sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8 \
--hash=sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85 \
--hash=sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19 \
--hash=sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc \
--hash=sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a \
--hash=sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1 \
--hash=sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317 \
--hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c \
--hash=sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631 \
--hash=sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d \
--hash=sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652 \
--hash=sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5 \
--hash=sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e \
--hash=sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706 \
--hash=sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563 \
--hash=sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083 \
--hash=sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180 \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e \
--hash=sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f \
--hash=sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725 \
--hash=sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774 \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5 \
--hash=sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d \
--hash=sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290 \
--hash=sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44 \
--hash=sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4 \
--hash=sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba \
--hash=sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12 \
--hash=sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4
# via
# cloudflare
# dns-lexicon
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
# via
# -r requirements.in
# acme
# certbot-dns-scaleway
# cloudflare
# dns-lexicon
# google-api-core
# python-digitalocean
# requests-file
# requests-mock
# tldextract
requests-file==2.1.0 \
--hash=sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658 \
--hash=sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c
# via tldextract
requests-mock==1.12.1 \
--hash=sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563 \
--hash=sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401
# via certbot-dns-scaleway
rsa==4.9 \
--hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \
--hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21
# via google-auth
s3transfer==0.10.3 \
--hash=sha256:263ed587a5803c6c708d3ce44dc4dfedaab4c1a32e8329bab818933d79ddcf5d \
--hash=sha256:4f50ed74ab84d474ce614475e0b8d5047ff080810aac5d01ea25231cfc944b0c
# via boto3
schedule==1.2.2 \
--hash=sha256:15fe9c75fe5fd9b9627f3f19cc0ef1420508f9f9a46f45cd0769ef75ede5f0b7 \
--hash=sha256:5bef4a2a0183abf44046ae0d164cadcac21b1db011bdd8102e4a0c1e91e06a7d
@ -368,13 +780,51 @@ setuptools==75.3.0 \
--hash=sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd \
--hash=sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686
# via -r requirements.in
urllib3==2.2.3 \
--hash=sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac \
--hash=sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9
# via requests
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
six==1.16.0 \
--hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
--hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
# via python-dateutil
soupsieve==2.6 \
--hash=sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb \
--hash=sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9
# via beautifulsoup4
tldextract==5.1.3 \
--hash=sha256:78de310cc2ca018692de5ddf320f9d6bd7c5cf857d0fd4f2175f0cdf4440ea75 \
--hash=sha256:d43c7284c23f5dc8a42fd0fee2abede2ff74cc622674e4cb07f514ab3330c338
# via dns-lexicon
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
# via
# pydantic
# pydantic-core
uritemplate==4.1.1 \
--hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \
--hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e
# via google-api-python-client
urllib3==1.26.20 \
--hash=sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e \
--hash=sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32
# via
# botocore
# requests
zipp==3.21.0 \
--hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
--hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
# via
# 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-dns-scaleway

View file

@ -11,4 +11,4 @@ python_dateutil==2.9.0.post0
qrcode==8.0
regex==2024.11.6
user_agents==2.2.0
werkzeug==3.1.2
werkzeug==3.1.3

View file

@ -37,9 +37,9 @@ beautifulsoup4==4.12.3 \
--hash=sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051 \
--hash=sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed
# via -r requirements.in
blinker==1.8.2 \
--hash=sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01 \
--hash=sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83
blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
# via
# flask
# flask-principal
@ -404,9 +404,9 @@ user-agents==2.2.0 \
--hash=sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7 \
--hash=sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26
# via -r requirements.in
werkzeug==3.1.2 \
--hash=sha256:4f7d1a5de312c810a8a2c6f0b47e9f6a7cffb7c8322def35e4d4d9841ff85597 \
--hash=sha256:f471a4cd167233077e9d2a8190c3471c5bc520c636a9e3c1e9300c33bced03bc
werkzeug==3.1.3 \
--hash=sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e \
--hash=sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746
# via
# -r requirements.in
# flask
@ -415,9 +415,9 @@ wtforms==3.2.1 \
--hash=sha256:583bad77ba1dd7286463f21e11aa3043ca4869d03575921d1a1698d0715e0fd4 \
--hash=sha256:df3e6b70f3192e92623128123ec8dca3067df9cfadd43d59681e210cfb8d4682
# via flask-wtf
zipp==3.20.2 \
--hash=sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350 \
--hash=sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29
zipp==3.21.0 \
--hash=sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4 \
--hash=sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931
# via importlib-metadata
zope-event==5.0 \
--hash=sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26 \