Initial YakPanel commit
This commit is contained in:
11
class/sewer/__init__.py
Normal file
11
class/sewer/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from .client import Client # noqa: F401
|
||||
|
||||
from .dns_providers import BaseDns # noqa: F401
|
||||
from .dns_providers import AuroraDns # noqa: F401
|
||||
from .dns_providers import CloudFlareDns # noqa: F401
|
||||
from .dns_providers import AcmeDnsDns # noqa: F401
|
||||
from .dns_providers import AliyunDns # noqa:F401
|
||||
from .dns_providers import HurricaneDns # noqa:F401
|
||||
from .dns_providers import RackspaceDns # noqa:F401
|
||||
from .dns_providers import DNSPodDns
|
||||
from .dns_providers import DuckDNSDns
|
||||
7
class/sewer/__version__.py
Normal file
7
class/sewer/__version__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
__title__ = "sewer"
|
||||
__description__ = "Sewer is a programmatic Lets Encrypt(ACME) client"
|
||||
__url__ = "https://github.com/komuw/sewer"
|
||||
__version__ = "0.7.2"
|
||||
__author__ = "komuW"
|
||||
__author_email__ = "komuw05@gmail.com"
|
||||
__license__ = "MIT"
|
||||
333
class/sewer/cli.py
Normal file
333
class/sewer/cli.py
Normal file
@@ -0,0 +1,333 @@
|
||||
import os
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
from . import Client
|
||||
from . import __version__ as sewer_version
|
||||
from .config import ACME_DIRECTORY_URL_STAGING, ACME_DIRECTORY_URL_PRODUCTION
|
||||
|
||||
|
||||
def main():
|
||||
r"""
|
||||
Usage:
|
||||
1. To get a new certificate:
|
||||
CLOUDFLARE_EMAIL=example@example.com \
|
||||
CLOUDFLARE_API_KEY=api-key \
|
||||
sewer \
|
||||
--model cloudflare \
|
||||
--domain example.com \
|
||||
--action run
|
||||
|
||||
2. To renew a certificate:
|
||||
CLOUDFLARE_EMAIL=example@example.com \
|
||||
CLOUDFLARE_API_KEY=api-key \
|
||||
sewer \
|
||||
--account_key /path/to/your/account.key \
|
||||
--model cloudflare \
|
||||
--domain example.com \
|
||||
--action renew
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="sewer",
|
||||
description=r"""Sewer is a Let's Encrypt(ACME) client.
|
||||
Example usage::
|
||||
CLOUDFLARE_EMAIL=example@example.com \
|
||||
CLOUDFLARE_API_KEY=api-key \
|
||||
sewer \
|
||||
--model cloudflare \
|
||||
--domain example.com \
|
||||
--action run""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version="%(prog)s {version}".format(version=sewer_version.__version__),
|
||||
help="The currently installed sewer version.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--account_key",
|
||||
type=argparse.FileType("r"),
|
||||
required=False,
|
||||
help="The path to your letsencrypt/acme account key. \
|
||||
eg: --account_key /home/myaccount.key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--certificate_key",
|
||||
type=argparse.FileType("r"),
|
||||
required=False,
|
||||
help="The path to your certificate key. \
|
||||
eg: --certificate_key /home/mycertificate.key",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=[
|
||||
"cloudflare",
|
||||
"aurora",
|
||||
"acmedns",
|
||||
"aliyun",
|
||||
"hurricane",
|
||||
"rackspace",
|
||||
"dnspod",
|
||||
"duckdns",
|
||||
],
|
||||
help="The name of the model provider that you want to use.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The domain/subdomain name for which \
|
||||
you want to get/renew certificate for. \
|
||||
wildcards are also supported \
|
||||
eg: --domain example.com",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alt_domains",
|
||||
type=str,
|
||||
required=False,
|
||||
default=[],
|
||||
nargs="*",
|
||||
help="A list of alternative domain/subdomain name/s(if any) for which \
|
||||
you want to get/renew certificate for. \
|
||||
eg: --alt_domains www.example.com blog.example.com",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--bundle_name",
|
||||
type=str,
|
||||
required=False,
|
||||
help="The name to use for certificate \
|
||||
certificate key and account key. Default is name of domain.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--endpoint",
|
||||
type=str,
|
||||
required=False,
|
||||
default="production",
|
||||
choices=["production", "staging"],
|
||||
help="Whether to use letsencrypt/acme production/live endpoints \
|
||||
or staging endpoints. production endpoints are used by default. \
|
||||
eg: --endpoint staging",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--email",
|
||||
type=str,
|
||||
required=False,
|
||||
help="Email to be used for registration and recovery. \
|
||||
eg: --email me@example.com",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--action",
|
||||
type=str,
|
||||
required=True,
|
||||
choices=["run", "renew"],
|
||||
help="The action that you want to perform. \
|
||||
Either run (get a new certificate) or renew (renew a certificate). \
|
||||
eg: --action run",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out_dir",
|
||||
type=str,
|
||||
required=False,
|
||||
default=os.getcwd(),
|
||||
help="""The dir where the certificate and keys file will be stored.
|
||||
default: The directory you run sewer command.
|
||||
eg: --out_dir /data/ssl/
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--loglevel",
|
||||
type=str,
|
||||
required=False,
|
||||
default="INFO",
|
||||
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||
help="The log level to output log messages at. \
|
||||
eg: --loglevel DEBUG",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
dns_provider = args.dns
|
||||
domain = args.domain
|
||||
alt_domains = args.alt_domains
|
||||
action = args.action
|
||||
account_key = args.account_key
|
||||
certificate_key = args.certificate_key
|
||||
bundle_name = args.bundle_name
|
||||
endpoint = args.endpoint
|
||||
email = args.email
|
||||
loglevel = args.loglevel
|
||||
out_dir = args.out_dir
|
||||
|
||||
# Make sure the output dir user specified is writable
|
||||
if not os.access(out_dir, os.W_OK):
|
||||
raise OSError("The dir '{0}' is not writable".format(out_dir))
|
||||
|
||||
logger = logging.getLogger()
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter("%(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
if not logger.handlers:
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(loglevel)
|
||||
|
||||
if account_key:
|
||||
account_key = account_key.read()
|
||||
if certificate_key:
|
||||
certificate_key = certificate_key.read()
|
||||
if bundle_name:
|
||||
file_name = bundle_name
|
||||
else:
|
||||
file_name = "{0}".format(domain)
|
||||
if endpoint == "staging":
|
||||
ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_STAGING
|
||||
else:
|
||||
ACME_DIRECTORY_URL = ACME_DIRECTORY_URL_PRODUCTION
|
||||
|
||||
if dns_provider == "cloudflare":
|
||||
from . import CloudFlareDns
|
||||
|
||||
try:
|
||||
CLOUDFLARE_EMAIL = os.environ["CLOUDFLARE_EMAIL"]
|
||||
CLOUDFLARE_API_KEY = os.environ["CLOUDFLARE_API_KEY"]
|
||||
|
||||
dns_class = CloudFlareDns(
|
||||
CLOUDFLARE_EMAIL=CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY=CLOUDFLARE_API_KEY
|
||||
)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
|
||||
elif dns_provider == "aurora":
|
||||
from . import AuroraDns
|
||||
|
||||
try:
|
||||
AURORA_API_KEY = os.environ["AURORA_API_KEY"]
|
||||
AURORA_SECRET_KEY = os.environ["AURORA_SECRET_KEY"]
|
||||
|
||||
dns_class = AuroraDns(
|
||||
AURORA_API_KEY=AURORA_API_KEY, AURORA_SECRET_KEY=AURORA_SECRET_KEY
|
||||
)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
|
||||
elif dns_provider == "acmedns":
|
||||
from . import AcmeDnsDns
|
||||
|
||||
try:
|
||||
ACME_DNS_API_USER = os.environ["ACME_DNS_API_USER"]
|
||||
ACME_DNS_API_KEY = os.environ["ACME_DNS_API_KEY"]
|
||||
ACME_DNS_API_BASE_URL = os.environ["ACME_DNS_API_BASE_URL"]
|
||||
|
||||
dns_class = AcmeDnsDns(
|
||||
ACME_DNS_API_USER=ACME_DNS_API_USER,
|
||||
ACME_DNS_API_KEY=ACME_DNS_API_KEY,
|
||||
ACME_DNS_API_BASE_URL=ACME_DNS_API_BASE_URL,
|
||||
)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
elif dns_provider == "aliyun":
|
||||
from . import AliyunDns
|
||||
|
||||
try:
|
||||
aliyun_ak = os.environ["ALIYUN_AK_ID"]
|
||||
aliyun_secret = os.environ["ALIYUN_AK_SECRET"]
|
||||
aliyun_endpoint = os.environ.get("ALIYUN_ENDPOINT", "cn-beijing")
|
||||
dns_class = AliyunDns(aliyun_ak, aliyun_secret, aliyun_endpoint)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
elif dns_provider == "hurricane":
|
||||
from . import HurricaneDns
|
||||
|
||||
try:
|
||||
he_username = os.environ["HURRICANE_USERNAME"]
|
||||
he_password = os.environ["HURRICANE_PASSWORD"]
|
||||
dns_class = HurricaneDns(he_username, he_password)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
elif dns_provider == "rackspace":
|
||||
from . import RackspaceDns
|
||||
|
||||
try:
|
||||
RACKSPACE_USERNAME = os.environ["RACKSPACE_USERNAME"]
|
||||
RACKSPACE_API_KEY = os.environ["RACKSPACE_API_KEY"]
|
||||
dns_class = RackspaceDns(RACKSPACE_USERNAME, RACKSPACE_API_KEY)
|
||||
logger.info("chosen_dns_prover. Using {0} as model provider. ".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
elif dns_provider == "dnspod":
|
||||
from . import DNSPodDns
|
||||
|
||||
try:
|
||||
DNSPOD_ID = os.environ["DNSPOD_ID"]
|
||||
DNSPOD_API_KEY = os.environ["DNSPOD_API_KEY"]
|
||||
dns_class = DNSPodDns(DNSPOD_ID, DNSPOD_API_KEY)
|
||||
logger.info("chosen_dns_prover. Using {0} as model provider. ".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
elif dns_provider == "duckdns":
|
||||
from . import DuckDNSDns
|
||||
|
||||
try:
|
||||
duckdns_token = os.environ["DUCKDNS_TOKEN"]
|
||||
|
||||
dns_class = DuckDNSDns(duckdns_token=duckdns_token)
|
||||
logger.info("chosen_dns_provider. Using {0} as model provider.".format(dns_provider))
|
||||
except KeyError as e:
|
||||
logger.error("ERROR:: Please supply {0} as an environment variable.".format(str(e)))
|
||||
raise
|
||||
else:
|
||||
raise ValueError("The model provider {0} is not recognised.".format(dns_provider))
|
||||
|
||||
client = Client(
|
||||
domain_name=domain,
|
||||
dns_class=dns_class,
|
||||
domain_alt_names=alt_domains,
|
||||
contact_email=email,
|
||||
account_key=account_key,
|
||||
certificate_key=certificate_key,
|
||||
ACME_DIRECTORY_URL=ACME_DIRECTORY_URL,
|
||||
LOG_LEVEL=loglevel,
|
||||
)
|
||||
certificate_key = client.certificate_key
|
||||
account_key = client.account_key
|
||||
|
||||
# prepare file path
|
||||
account_key_file_path = os.path.join(out_dir, "{0}.account.key".format(file_name))
|
||||
crt_file_path = os.path.join(out_dir, "{0}.crt".format(file_name))
|
||||
crt_key_file_path = os.path.join(out_dir, "{0}.key".format(file_name))
|
||||
|
||||
# write out account_key in out_dir directory
|
||||
with open(account_key_file_path, "w") as account_file:
|
||||
account_file.write(account_key)
|
||||
logger.info("account key succesfully written to {0}.".format(account_key_file_path))
|
||||
|
||||
if action == "renew":
|
||||
message = "Certificate Succesfully renewed. The certificate, certificate key and account key have been saved in the current directory"
|
||||
certificate = client.renew()
|
||||
else:
|
||||
message = "Certificate Succesfully issued. The certificate, certificate key and account key have been saved in the current directory"
|
||||
certificate = client.cert()
|
||||
|
||||
# write out certificate and certificate key in out_dir directory
|
||||
with open(crt_file_path, "w") as certificate_file:
|
||||
certificate_file.write(certificate)
|
||||
with open(crt_key_file_path, "w") as certificate_key_file:
|
||||
certificate_key_file.write(certificate_key)
|
||||
|
||||
logger.info("certificate succesfully written to {0}.".format(crt_file_path))
|
||||
logger.info("certificate key succesfully written to {0}.".format(crt_key_file_path))
|
||||
|
||||
logger.info("the_end. {0}".format(message))
|
||||
727
class/sewer/client.py
Normal file
727
class/sewer/client.py
Normal file
@@ -0,0 +1,727 @@
|
||||
import time
|
||||
import copy
|
||||
import json
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import binascii
|
||||
import platform
|
||||
import sys
|
||||
import requests
|
||||
import OpenSSL
|
||||
import cryptography
|
||||
|
||||
from . import __version__ as sewer_version
|
||||
from .config import ACME_DIRECTORY_URL_PRODUCTION
|
||||
try:
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
except:pass
|
||||
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""
|
||||
todo: improve documentation.
|
||||
|
||||
usage:
|
||||
import sewer
|
||||
dns_class = sewer.CloudFlareDns(CLOUDFLARE_EMAIL='example@example.com',
|
||||
CLOUDFLARE_API_KEY='nsa-grade-api-key')
|
||||
|
||||
1. to create a new certificate.
|
||||
client = sewer.Client(domain_name='example.com',
|
||||
dns_class=dns_class)
|
||||
certificate = client.cert()
|
||||
certificate_key = client.certificate_key
|
||||
account_key = client.account_key
|
||||
|
||||
with open('certificate.crt', 'w') as certificate_file:
|
||||
certificate_file.write(certificate)
|
||||
|
||||
with open('certificate.key', 'w') as certificate_key_file:
|
||||
certificate_key_file.write(certificate_key)
|
||||
|
||||
|
||||
2. to renew a certificate:
|
||||
with open('account_key.key', 'r') as account_key_file:
|
||||
account_key = account_key_file.read()
|
||||
|
||||
client = sewer.Client(domain_name='example.com',
|
||||
dns_class=dns_class,
|
||||
account_key=account_key)
|
||||
certificate = client.renew()
|
||||
certificate_key = client.certificate_key
|
||||
|
||||
todo:
|
||||
- handle more exceptions
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
domain_name,
|
||||
dns_class,
|
||||
domain_alt_names=None,
|
||||
contact_email=None,
|
||||
account_key=None,
|
||||
certificate_key=None,
|
||||
bits=2048,
|
||||
digest="sha256",
|
||||
ACME_REQUEST_TIMEOUT=7,
|
||||
ACME_AUTH_STATUS_WAIT_PERIOD=8,
|
||||
ACME_AUTH_STATUS_MAX_CHECKS=3,
|
||||
ACME_DIRECTORY_URL=ACME_DIRECTORY_URL_PRODUCTION,
|
||||
LOG_LEVEL="INFO",
|
||||
):
|
||||
|
||||
self.domain_name = domain_name
|
||||
self.dns_class = dns_class
|
||||
if not domain_alt_names:
|
||||
domain_alt_names = []
|
||||
self.domain_alt_names = domain_alt_names
|
||||
self.domain_alt_names = list(set(self.domain_alt_names))
|
||||
self.contact_email = contact_email
|
||||
self.bits = bits
|
||||
self.digest = digest
|
||||
self.ACME_REQUEST_TIMEOUT = ACME_REQUEST_TIMEOUT
|
||||
self.ACME_AUTH_STATUS_WAIT_PERIOD = ACME_AUTH_STATUS_WAIT_PERIOD
|
||||
self.ACME_AUTH_STATUS_MAX_CHECKS = ACME_AUTH_STATUS_MAX_CHECKS
|
||||
self.ACME_DIRECTORY_URL = ACME_DIRECTORY_URL
|
||||
self.LOG_LEVEL = LOG_LEVEL.upper()
|
||||
|
||||
self.logger = logging.getLogger()
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter("%(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
if not self.logger.handlers:
|
||||
self.logger.addHandler(handler)
|
||||
self.logger.setLevel(self.LOG_LEVEL)
|
||||
|
||||
try:
|
||||
self.all_domain_names = copy.copy(self.domain_alt_names)
|
||||
self.all_domain_names.insert(0, self.domain_name)
|
||||
self.domain_alt_names = list(set(self.domain_alt_names))
|
||||
|
||||
self.User_Agent = self.get_user_agent()
|
||||
acme_endpoints = self.get_acme_endpoints().json()
|
||||
self.ACME_GET_NONCE_URL = acme_endpoints["newNonce"]
|
||||
self.ACME_TOS_URL = acme_endpoints["meta"]["termsOfService"]
|
||||
self.ACME_KEY_CHANGE_URL = acme_endpoints["keyChange"]
|
||||
self.ACME_NEW_ACCOUNT_URL = acme_endpoints["newAccount"]
|
||||
self.ACME_NEW_ORDER_URL = acme_endpoints["newOrder"]
|
||||
self.ACME_REVOKE_CERT_URL = acme_endpoints["revokeCert"]
|
||||
|
||||
# unique account identifier
|
||||
# https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2
|
||||
self.kid = None
|
||||
|
||||
self.certificate_key = certificate_key or self.create_certificate_key()
|
||||
self.csr = self.create_csr()
|
||||
|
||||
if not account_key:
|
||||
self.account_key = self.create_account_key()
|
||||
self.PRIOR_REGISTERED = False
|
||||
else:
|
||||
self.account_key = account_key
|
||||
self.PRIOR_REGISTERED = True
|
||||
|
||||
self.logger.info(
|
||||
"intialise_success, sewer_version={0}, domain_names={1}, acme_server={2}".format(
|
||||
sewer_version.__version__,
|
||||
self.all_domain_names,
|
||||
self.ACME_DIRECTORY_URL[:20] + "...",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.error("Unable to intialise client. error={0}".format(str(e)))
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def log_response(response):
|
||||
"""
|
||||
renders response as json or as a string
|
||||
"""
|
||||
# TODO: use this to handle all response logs.
|
||||
try:
|
||||
log_body = response.json()
|
||||
except ValueError:
|
||||
log_body = response.content[:30]
|
||||
return log_body
|
||||
|
||||
@staticmethod
|
||||
def get_user_agent():
|
||||
return "python-requests/{requests_version} ({system}: {machine}) sewer {sewer_version} ({sewer_url})".format(
|
||||
requests_version=requests.__version__,
|
||||
system=platform.system(),
|
||||
machine=platform.machine(),
|
||||
sewer_version=sewer_version.__version__,
|
||||
sewer_url=sewer_version.__url__,
|
||||
)
|
||||
|
||||
def get_acme_endpoints(self):
|
||||
self.logger.debug("get_acme_endpoints")
|
||||
headers = {"User-Agent": self.User_Agent}
|
||||
get_acme_endpoints = requests.get(
|
||||
self.ACME_DIRECTORY_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
|
||||
)
|
||||
self.logger.debug(
|
||||
"get_acme_endpoints_response. status_code={0}".format(get_acme_endpoints.status_code)
|
||||
)
|
||||
if get_acme_endpoints.status_code not in [200, 201]:
|
||||
raise ValueError(
|
||||
"Error while getting Acme endpoints: status_code={status_code} response={response}".format(
|
||||
status_code=get_acme_endpoints.status_code,
|
||||
response=self.log_response(get_acme_endpoints),
|
||||
)
|
||||
)
|
||||
return get_acme_endpoints
|
||||
|
||||
def create_certificate_key(self):
|
||||
self.logger.debug("create_certificate_key")
|
||||
return self.create_key().decode()
|
||||
|
||||
def create_account_key(self):
|
||||
self.logger.debug("create_account_key")
|
||||
return self.create_key().decode()
|
||||
|
||||
def create_key(self, key_type=OpenSSL.crypto.TYPE_RSA):
|
||||
key = OpenSSL.crypto.PKey()
|
||||
key.generate_key(key_type, self.bits)
|
||||
private_key = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
|
||||
return private_key
|
||||
|
||||
def create_csr(self):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
|
||||
The CSR is sent in the base64url-encoded version of the DER format. (NB: this
|
||||
field uses base64url, and does not include headers, it is different from PEM.)
|
||||
"""
|
||||
self.logger.debug("create_csr")
|
||||
X509Req = OpenSSL.crypto.X509Req()
|
||||
X509Req.get_subject().CN = self.domain_name
|
||||
|
||||
if self.domain_alt_names:
|
||||
SAN = "DNS:{0}, ".format(self.domain_name).encode("utf8") + ", ".join(
|
||||
"DNS:" + i for i in self.domain_alt_names
|
||||
).encode("utf8")
|
||||
else:
|
||||
SAN = "DNS:{0}".format(self.domain_name).encode("utf8")
|
||||
|
||||
X509Req.add_extensions(
|
||||
[
|
||||
OpenSSL.crypto.X509Extension(
|
||||
"subjectAltName".encode("utf8"), critical=False, value=SAN
|
||||
)
|
||||
]
|
||||
)
|
||||
pk = OpenSSL.crypto.load_privatekey(
|
||||
OpenSSL.crypto.FILETYPE_PEM, self.certificate_key.encode()
|
||||
)
|
||||
X509Req.set_pubkey(pk)
|
||||
X509Req.set_version(2)
|
||||
X509Req.sign(pk, self.digest)
|
||||
return OpenSSL.crypto.dump_certificate_request(OpenSSL.crypto.FILETYPE_ASN1, X509Req)
|
||||
|
||||
def acme_register(self):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.3
|
||||
The server creates an account and stores the public key used to
|
||||
verify the JWS (i.e., the "jwk" element of the JWS header) to
|
||||
authenticate future requests from the account.
|
||||
The server returns this account object in a 201 (Created) response, with the account URL
|
||||
in a Location header field.
|
||||
This account URL will be used in subsequest requests to ACME, as the "kid" value in the acme header.
|
||||
If the server already has an account registered with the provided
|
||||
account key, then it MUST return a response with a 200 (OK) status
|
||||
code and provide the URL of that account in the Location header field.
|
||||
If there is an existing account with the new key
|
||||
provided, then the server SHOULD use status code 409 (Conflict) and
|
||||
provide the URL of that account in the Location header field
|
||||
"""
|
||||
self.logger.info("acme_register")
|
||||
if self.PRIOR_REGISTERED:
|
||||
payload = {"onlyReturnExisting": True}
|
||||
elif self.contact_email:
|
||||
payload = {
|
||||
"termsOfServiceAgreed": True,
|
||||
"contact": ["mailto:{0}".format(self.contact_email)],
|
||||
}
|
||||
else:
|
||||
payload = {"termsOfServiceAgreed": True}
|
||||
|
||||
url = self.ACME_NEW_ACCOUNT_URL
|
||||
acme_register_response = self.make_signed_acme_request(url=url, payload=payload)
|
||||
self.logger.debug(
|
||||
"acme_register_response. status_code={0}. response={1}".format(
|
||||
acme_register_response.status_code, self.log_response(acme_register_response)
|
||||
)
|
||||
)
|
||||
|
||||
if acme_register_response.status_code not in [201, 200, 409]:
|
||||
raise ValueError(
|
||||
"Error while registering: status_code={status_code} response={response}".format(
|
||||
status_code=acme_register_response.status_code,
|
||||
response=self.log_response(acme_register_response),
|
||||
)
|
||||
)
|
||||
|
||||
kid = acme_register_response.headers["Location"]
|
||||
setattr(self, "kid", kid)
|
||||
|
||||
self.logger.info("acme_register_success")
|
||||
return acme_register_response
|
||||
|
||||
def apply_for_cert_issuance(self):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
|
||||
The order object returned by the server represents a promise that if
|
||||
the client fulfills the server's requirements before the "expires"
|
||||
time, then the server will be willing to finalize the order upon
|
||||
request and issue the requested certificate. In the order object,
|
||||
any authorization referenced in the "authorizations" array whose
|
||||
status is "pending" represents an authorization transaction that the
|
||||
client must complete before the server will issue the certificate.
|
||||
|
||||
Once the client believes it has fulfilled the server's requirements,
|
||||
it should send a POST request to the order resource's finalize URL.
|
||||
The POST body MUST include a CSR:
|
||||
|
||||
The date values seem to be ignored by LetsEncrypt although they are
|
||||
in the ACME draft spec; https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
|
||||
"""
|
||||
self.logger.info("apply_for_cert_issuance")
|
||||
identifiers = []
|
||||
for domain_name in self.all_domain_names:
|
||||
identifiers.append({"type": "model", "value": domain_name})
|
||||
|
||||
payload = {"identifiers": identifiers}
|
||||
url = self.ACME_NEW_ORDER_URL
|
||||
apply_for_cert_issuance_response = self.make_signed_acme_request(url=url, payload=payload)
|
||||
self.logger.debug(
|
||||
"apply_for_cert_issuance_response. status_code={0}. response={1}".format(
|
||||
apply_for_cert_issuance_response.status_code,
|
||||
self.log_response(apply_for_cert_issuance_response),
|
||||
)
|
||||
)
|
||||
|
||||
if apply_for_cert_issuance_response.status_code != 201:
|
||||
raise ValueError(
|
||||
"Error applying for certificate issuance: status_code={status_code} response={response}".format(
|
||||
status_code=apply_for_cert_issuance_response.status_code,
|
||||
response=self.log_response(apply_for_cert_issuance_response),
|
||||
)
|
||||
)
|
||||
|
||||
apply_for_cert_issuance_response_json = apply_for_cert_issuance_response.json()
|
||||
finalize_url = apply_for_cert_issuance_response_json["finalize"]
|
||||
authorizations = apply_for_cert_issuance_response_json["authorizations"]
|
||||
|
||||
self.logger.info("apply_for_cert_issuance_success")
|
||||
return authorizations, finalize_url
|
||||
|
||||
def get_identifier_authorization(self, url):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5
|
||||
When a client receives an order from the server it downloads the
|
||||
authorization resources by sending GET requests to the indicated
|
||||
URLs. If the client initiates authorization using a request to the
|
||||
new authorization resource, it will have already received the pending
|
||||
authorization object in the response to that request.
|
||||
|
||||
This is also where we get the challenges/tokens.
|
||||
"""
|
||||
self.logger.info("get_identifier_authorization")
|
||||
headers = {"User-Agent": self.User_Agent}
|
||||
get_identifier_authorization_response = requests.get(
|
||||
url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
|
||||
)
|
||||
self.logger.debug(
|
||||
"get_identifier_authorization_response. status_code={0}. response={1}".format(
|
||||
get_identifier_authorization_response.status_code,
|
||||
self.log_response(get_identifier_authorization_response),
|
||||
)
|
||||
)
|
||||
if get_identifier_authorization_response.status_code not in [200, 201]:
|
||||
raise ValueError(
|
||||
"Error getting identifier authorization: status_code={status_code} response={response}".format(
|
||||
status_code=get_identifier_authorization_response.status_code,
|
||||
response=self.log_response(get_identifier_authorization_response),
|
||||
)
|
||||
)
|
||||
res = get_identifier_authorization_response.json()
|
||||
domain = res["identifier"]["value"]
|
||||
wildcard = res.get("wildcard")
|
||||
if wildcard:
|
||||
domain = "*." + domain
|
||||
|
||||
for i in res["challenges"]:
|
||||
if i["type"] == "model-01":
|
||||
dns_challenge = i
|
||||
dns_token = dns_challenge["token"]
|
||||
dns_challenge_url = dns_challenge["url"]
|
||||
identifier_auth = {
|
||||
"domain": domain,
|
||||
"url": url,
|
||||
"wildcard": wildcard,
|
||||
"dns_token": dns_token,
|
||||
"dns_challenge_url": dns_challenge_url,
|
||||
}
|
||||
|
||||
self.logger.debug(
|
||||
"get_identifier_authorization_success. identifier_auth={0}".format(identifier_auth)
|
||||
)
|
||||
self.logger.info("get_identifier_authorization_success")
|
||||
return identifier_auth
|
||||
|
||||
def get_keyauthorization(self, dns_token):
|
||||
self.logger.debug("get_keyauthorization")
|
||||
acme_header_jwk_json = json.dumps(
|
||||
self.get_acme_header("GET_THUMBPRINT")["jwk"], sort_keys=True, separators=(",", ":")
|
||||
)
|
||||
acme_thumbprint = self.calculate_safe_base64(
|
||||
hashlib.sha256(acme_header_jwk_json.encode("utf8")).digest()
|
||||
)
|
||||
acme_keyauthorization = "{0}.{1}".format(dns_token, acme_thumbprint)
|
||||
base64_of_acme_keyauthorization = self.calculate_safe_base64(
|
||||
hashlib.sha256(acme_keyauthorization.encode("utf8")).digest()
|
||||
)
|
||||
|
||||
return acme_keyauthorization, base64_of_acme_keyauthorization
|
||||
|
||||
def check_authorization_status(self, authorization_url, desired_status=None):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1
|
||||
To check on the status of an authorization, the client sends a GET(polling)
|
||||
request to the authorization URL, and the server responds with the
|
||||
current authorization object.
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-8.2
|
||||
Clients SHOULD NOT respond to challenges until they believe that the
|
||||
server's queries will succeed. If a server's initial validation
|
||||
query fails, the server SHOULD retry[intended to address things like propagation delays in
|
||||
HTTP/DNS provisioning] the query after some time.
|
||||
The server MUST provide information about its retry state to the
|
||||
client via the "errors" field in the challenge and the Retry-After
|
||||
"""
|
||||
self.logger.info("check_authorization_status")
|
||||
desired_status = desired_status or ["pending", "valid","invalid"]
|
||||
number_of_checks = 0
|
||||
while True:
|
||||
headers = {"User-Agent": self.User_Agent}
|
||||
check_authorization_status_response = requests.get(
|
||||
authorization_url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
|
||||
)
|
||||
a_auth = check_authorization_status_response.json()
|
||||
print(type(a_auth),a_auth)
|
||||
authorization_status = a_auth["status"]
|
||||
number_of_checks = number_of_checks + 1
|
||||
self.logger.debug(
|
||||
"check_authorization_status_response. status_code={0}. response={1}".format(
|
||||
check_authorization_status_response.status_code,
|
||||
self.log_response(check_authorization_status_response),
|
||||
)
|
||||
)
|
||||
if number_of_checks == self.ACME_AUTH_STATUS_MAX_CHECKS:
|
||||
raise StopIteration(
|
||||
"Checks done={0}. Max checks allowed={1}. Interval between checks={2}seconds. >>>> {3}".format(
|
||||
number_of_checks,
|
||||
self.ACME_AUTH_STATUS_MAX_CHECKS,
|
||||
self.ACME_AUTH_STATUS_WAIT_PERIOD,
|
||||
json.dumps(a_auth)
|
||||
)
|
||||
)
|
||||
print(authorization_status,desired_status)
|
||||
if authorization_status in desired_status:
|
||||
if authorization_status == "invalid":
|
||||
try:
|
||||
import panelLets
|
||||
if 'error' in a_auth['challenges'][0]:
|
||||
ret_title = a_auth['challenges'][0]['error']['detail']
|
||||
elif 'error' in a_auth['challenges'][1]:
|
||||
ret_title = a_auth['challenges'][1]['error']['detail']
|
||||
elif 'error' in a_auth['challenges'][2]:
|
||||
ret_title = a_auth['challenges'][2]['error']['detail']
|
||||
else:
|
||||
ret_title = str(a_auth)
|
||||
ret_title = panelLets.panelLets().get_error(ret_title)
|
||||
except:
|
||||
ret_title = str(a_auth)
|
||||
raise StopIteration(
|
||||
"{0} >>>> {1}".format(
|
||||
ret_title,
|
||||
json.dumps(a_auth)
|
||||
)
|
||||
)
|
||||
break
|
||||
else:
|
||||
# for any other status, sleep then retry
|
||||
time.sleep(self.ACME_AUTH_STATUS_WAIT_PERIOD)
|
||||
|
||||
self.logger.info("check_authorization_status_success")
|
||||
return check_authorization_status_response
|
||||
|
||||
def respond_to_challenge(self, acme_keyauthorization, dns_challenge_url):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.5.1
|
||||
To prove control of the identifier and receive authorization, the
|
||||
client needs to respond with information to complete the challenges.
|
||||
The server is said to "finalize" the authorization when it has
|
||||
completed one of the validations, by assigning the authorization a
|
||||
status of "valid" or "invalid".
|
||||
|
||||
Usually, the validation process will take some time, so the client
|
||||
will need to poll the authorization resource to see when it is finalized.
|
||||
To check on the status of an authorization, the client sends a GET(polling)
|
||||
request to the authorization URL, and the server responds with the
|
||||
current authorization object.
|
||||
"""
|
||||
self.logger.info("respond_to_challenge")
|
||||
payload = {"keyAuthorization": "{0}".format(acme_keyauthorization)}
|
||||
respond_to_challenge_response = self.make_signed_acme_request(dns_challenge_url, payload)
|
||||
self.logger.debug(
|
||||
"respond_to_challenge_response. status_code={0}. response={1}".format(
|
||||
respond_to_challenge_response.status_code,
|
||||
self.log_response(respond_to_challenge_response),
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.info("respond_to_challenge_success")
|
||||
return respond_to_challenge_response
|
||||
|
||||
def send_csr(self, finalize_url):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-7.4
|
||||
Once the client believes it has fulfilled the server's requirements,
|
||||
it should send a POST request(include a CSR) to the order resource's finalize URL.
|
||||
A request to finalize an order will result in error if the order indicated does not have status "pending",
|
||||
if the CSR and order identifiers differ, or if the account is not authorized for the identifiers indicated in the CSR.
|
||||
The CSR is sent in the base64url-encoded version of the DER format(OpenSSL.crypto.FILETYPE_ASN1)
|
||||
|
||||
A valid request to finalize an order will return the order to be finalized.
|
||||
The client should begin polling the order by sending a
|
||||
GET request to the order resource to obtain its current state.
|
||||
"""
|
||||
self.logger.info("send_csr")
|
||||
payload = {"csr": self.calculate_safe_base64(self.csr)}
|
||||
send_csr_response = self.make_signed_acme_request(url=finalize_url, payload=payload)
|
||||
self.logger.debug(
|
||||
"send_csr_response. status_code={0}. response={1}".format(
|
||||
send_csr_response.status_code, self.log_response(send_csr_response)
|
||||
)
|
||||
)
|
||||
|
||||
if send_csr_response.status_code not in [200, 201]:
|
||||
raise ValueError(
|
||||
"Error sending csr: status_code={status_code} response={response}".format(
|
||||
status_code=send_csr_response.status_code,
|
||||
response=self.log_response(send_csr_response),
|
||||
)
|
||||
)
|
||||
send_csr_response_json = send_csr_response.json()
|
||||
certificate_url = send_csr_response_json["certificate"]
|
||||
|
||||
self.logger.info("send_csr_success")
|
||||
return certificate_url
|
||||
|
||||
def download_certificate(self, certificate_url):
|
||||
self.logger.info("download_certificate")
|
||||
|
||||
download_certificate_response = self.make_signed_acme_request(
|
||||
certificate_url, payload="DOWNLOAD_Z_CERTIFICATE"
|
||||
)
|
||||
self.logger.debug(
|
||||
"download_certificate_response. status_code={0}. response={1}".format(
|
||||
download_certificate_response.status_code,
|
||||
self.log_response(download_certificate_response),
|
||||
)
|
||||
)
|
||||
|
||||
if download_certificate_response.status_code not in [200, 201]:
|
||||
raise ValueError(
|
||||
"Error fetching signed certificate: status_code={status_code} response={response}".format(
|
||||
status_code=download_certificate_response.status_code,
|
||||
response=self.log_response(download_certificate_response),
|
||||
)
|
||||
)
|
||||
|
||||
pem_certificate = download_certificate_response.content.decode("utf-8")
|
||||
|
||||
self.logger.info("download_certificate_success")
|
||||
return pem_certificate
|
||||
|
||||
def sign_message(self, message):
|
||||
self.logger.debug("sign_message")
|
||||
pk = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, self.account_key.encode())
|
||||
return OpenSSL.crypto.sign(pk, message.encode("utf8"), self.digest)
|
||||
|
||||
def get_nonce(self):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.4
|
||||
Each request to an ACME server must include a fresh unused nonce
|
||||
in order to protect against replay attacks.
|
||||
"""
|
||||
self.logger.debug("get_nonce")
|
||||
headers = {"User-Agent": self.User_Agent}
|
||||
response = requests.get(
|
||||
self.ACME_GET_NONCE_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False
|
||||
)
|
||||
nonce = response.headers["Replay-Nonce"]
|
||||
return nonce
|
||||
|
||||
@staticmethod
|
||||
def stringfy_items(payload):
|
||||
"""
|
||||
method that takes a dictionary and then converts any keys or values
|
||||
in that are of type bytes into unicode strings.
|
||||
This is necessary esp if you want to then turn that dict into a json string.
|
||||
"""
|
||||
if isinstance(payload, str):
|
||||
return payload
|
||||
|
||||
for k, v in payload.items():
|
||||
if isinstance(k, bytes):
|
||||
k = k.decode("utf-8")
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode("utf-8")
|
||||
payload[k] = v
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def calculate_safe_base64(un_encoded_data):
|
||||
"""
|
||||
takes in a string or bytes
|
||||
returns a string
|
||||
"""
|
||||
if sys.version_info[0] == 3:
|
||||
if isinstance(un_encoded_data, str):
|
||||
un_encoded_data = un_encoded_data.encode("utf8")
|
||||
r = base64.urlsafe_b64encode(un_encoded_data).rstrip(b"=")
|
||||
return r.decode("utf8")
|
||||
|
||||
def get_acme_header(self, url):
|
||||
"""
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme#section-6.2
|
||||
The JWS Protected Header MUST include the following fields:
|
||||
- "alg" (Algorithm)
|
||||
- "jwk" (JSON Web Key, only for requests to new-account and revoke-cert resources)
|
||||
- "kid" (Key ID, for all other requests). gotten from self.ACME_NEW_ACCOUNT_URL
|
||||
- "nonce". gotten from self.ACME_GET_NONCE_URL
|
||||
- "url"
|
||||
"""
|
||||
self.logger.debug("get_acme_header")
|
||||
header = {"alg": "RS256", "nonce": self.get_nonce(), "url": url}
|
||||
|
||||
if url in [self.ACME_NEW_ACCOUNT_URL, self.ACME_REVOKE_CERT_URL, "GET_THUMBPRINT"]:
|
||||
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
|
||||
self.account_key.encode(),
|
||||
password=None,
|
||||
backend=cryptography.hazmat.backends.default_backend(),
|
||||
)
|
||||
public_key_public_numbers = private_key.public_key().public_numbers()
|
||||
# private key public exponent in hex format
|
||||
exponent = "{0:x}".format(public_key_public_numbers.e)
|
||||
exponent = "0{0}".format(exponent) if len(exponent) % 2 else exponent
|
||||
# private key modulus in hex format
|
||||
modulus = "{0:x}".format(public_key_public_numbers.n)
|
||||
jwk = {
|
||||
"kty": "RSA",
|
||||
"e": self.calculate_safe_base64(binascii.unhexlify(exponent)),
|
||||
"n": self.calculate_safe_base64(binascii.unhexlify(modulus)),
|
||||
}
|
||||
header["jwk"] = jwk
|
||||
else:
|
||||
header["kid"] = self.kid
|
||||
print('h:',url,header)
|
||||
return header
|
||||
|
||||
def make_signed_acme_request(self, url, payload):
|
||||
self.logger.debug("make_signed_acme_request")
|
||||
headers = {"User-Agent": self.User_Agent}
|
||||
payload = self.stringfy_items(payload)
|
||||
|
||||
if payload in ["GET_Z_CHALLENGE", "DOWNLOAD_Z_CERTIFICATE"]:
|
||||
response = requests.get(url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers,verify=False)
|
||||
else:
|
||||
payload64 = self.calculate_safe_base64(json.dumps(payload))
|
||||
protected = self.get_acme_header(url)
|
||||
protected64 = self.calculate_safe_base64(json.dumps(protected))
|
||||
signature = self.sign_message(message="{0}.{1}".format(protected64, payload64)) # bytes
|
||||
signature64 = self.calculate_safe_base64(signature) # str
|
||||
data = json.dumps(
|
||||
{"protected": protected64, "payload": payload64, "signature": signature64}
|
||||
)
|
||||
headers.update({"Content-Type": "application/jose+json"})
|
||||
response = requests.post(
|
||||
url, data=data.encode("utf8"), timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ,verify=False
|
||||
)
|
||||
return response
|
||||
|
||||
def get_certificate(self):
|
||||
self.logger.debug("get_certificate")
|
||||
domain_dns_value = "placeholder"
|
||||
dns_names_to_delete = []
|
||||
try:
|
||||
self.acme_register()
|
||||
authorizations, finalize_url = self.apply_for_cert_issuance()
|
||||
responders = []
|
||||
for url in authorizations:
|
||||
identifier_auth = self.get_identifier_authorization(url)
|
||||
authorization_url = identifier_auth["url"]
|
||||
dns_name = identifier_auth["domain"]
|
||||
dns_token = identifier_auth["dns_token"]
|
||||
dns_challenge_url = identifier_auth["dns_challenge_url"]
|
||||
|
||||
acme_keyauthorization, domain_dns_value = self.get_keyauthorization(dns_token)
|
||||
self.dns_class.create_dns_record(dns_name, domain_dns_value)
|
||||
dns_names_to_delete.append(
|
||||
{"dns_name": dns_name, "domain_dns_value": domain_dns_value}
|
||||
)
|
||||
responders.append(
|
||||
{
|
||||
"authorization_url": authorization_url,
|
||||
"acme_keyauthorization": acme_keyauthorization,
|
||||
"dns_challenge_url": dns_challenge_url,
|
||||
}
|
||||
)
|
||||
|
||||
# for a case where you want certificates for *.example.com and example.com
|
||||
# you have to create both model records AND then respond to the challenge.
|
||||
# see issues/83
|
||||
for i in responders:
|
||||
# Make sure the authorization is in a status where we can submit a challenge
|
||||
# response. The authorization can be in the "valid" state before submitting
|
||||
# a challenge response if there was a previous authorization for these hosts
|
||||
# that was successfully validated, still cached by the server.
|
||||
auth_status_response = self.check_authorization_status(i["authorization_url"])
|
||||
if auth_status_response.json()["status"] == "pending":
|
||||
self.respond_to_challenge(i["acme_keyauthorization"], i["dns_challenge_url"])
|
||||
|
||||
for i in responders:
|
||||
# Before sending a CSR, we need to make sure the server has completed the
|
||||
# validation for all the authorizations
|
||||
self.check_authorization_status(i["authorization_url"], ["valid"])
|
||||
|
||||
certificate_url = self.send_csr(finalize_url)
|
||||
certificate = self.download_certificate(certificate_url)
|
||||
except Exception as e:
|
||||
self.logger.error("Error: Unable to issue certificate. error={0}".format(str(e)))
|
||||
raise e
|
||||
finally:
|
||||
for i in dns_names_to_delete:
|
||||
self.dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"])
|
||||
|
||||
return certificate
|
||||
|
||||
def cert(self):
|
||||
"""
|
||||
convenience method to get a certificate without much hassle
|
||||
"""
|
||||
return self.get_certificate()
|
||||
|
||||
def renew(self):
|
||||
"""
|
||||
renews a certificate.
|
||||
A renewal is actually just getting a new certificate.
|
||||
An issuance request counts as a renewal if it contains the exact same set of hostnames as a previously issued certificate.
|
||||
https://letsencrypt.org/docs/rate-limits/
|
||||
"""
|
||||
return self.cert()
|
||||
2
class/sewer/config.py
Normal file
2
class/sewer/config.py
Normal file
@@ -0,0 +1,2 @@
|
||||
ACME_DIRECTORY_URL_STAGING = "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
ACME_DIRECTORY_URL_PRODUCTION = "https://acme-v02.api.letsencrypt.org/directory"
|
||||
9
class/sewer/dns_providers/__init__.py
Normal file
9
class/sewer/dns_providers/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from .common import BaseDns # noqa: F401
|
||||
from .auroradns import AuroraDns # noqa: F401
|
||||
from .cloudflare import CloudFlareDns # noqa: F401
|
||||
from .acmedns import AcmeDnsDns # noqa: F401
|
||||
from .aliyundns import AliyunDns # noqa: F401
|
||||
from .hurricane import HurricaneDns # noqa: F401
|
||||
from .rackspace import RackspaceDns # noqa: F401
|
||||
from .dnspod import DNSPodDns
|
||||
from .duckdns import DuckDNSDns
|
||||
75
class/sewer/dns_providers/acmedns.py
Normal file
75
class/sewer/dns_providers/acmedns.py
Normal file
@@ -0,0 +1,75 @@
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except:
|
||||
import urlparse
|
||||
|
||||
try:
|
||||
acmedns_dependencies = True
|
||||
from dns.resolver import Resolver
|
||||
except ImportError:
|
||||
acmedns_dependencies = False
|
||||
import requests
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class AcmeDnsDns(common.BaseDns):
|
||||
"""
|
||||
"""
|
||||
|
||||
dns_provider_name = "acmedns"
|
||||
|
||||
def __init__(self, ACME_DNS_API_USER, ACME_DNS_API_KEY, ACME_DNS_API_BASE_URL):
|
||||
|
||||
if not acmedns_dependencies:
|
||||
raise ImportError(
|
||||
"""You need to install AcmeDnsDns dependencies. run; pip3 install sewer[acmedns]"""
|
||||
)
|
||||
|
||||
self.ACME_DNS_API_USER = ACME_DNS_API_USER
|
||||
self.ACME_DNS_API_KEY = ACME_DNS_API_KEY
|
||||
self.HTTP_TIMEOUT = 65 # seconds
|
||||
|
||||
if ACME_DNS_API_BASE_URL[-1] != "/":
|
||||
self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL + "/"
|
||||
else:
|
||||
self.ACME_DNS_API_BASE_URL = ACME_DNS_API_BASE_URL
|
||||
super(AcmeDnsDns, self).__init__()
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record")
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
|
||||
resolver = Resolver(configure=False)
|
||||
resolver.nameservers = ["8.8.8.8"]
|
||||
answer = resolver.query("_acme-challenge.{0}.".format(domain_name), "TXT")
|
||||
subdomain, _ = str(answer.canonical_name).split(".", 1)
|
||||
|
||||
url = urlparse.urljoin(self.ACME_DNS_API_BASE_URL, "update")
|
||||
headers = {"X-Api-User": self.ACME_DNS_API_USER, "X-Api-Key": self.ACME_DNS_API_KEY}
|
||||
body = {"subdomain": subdomain, "txt": domain_dns_value}
|
||||
update_acmedns_dns_record_response = requests.post(
|
||||
url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
self.logger.debug(
|
||||
"update_acmedns_dns_record_response. status_code={0}. response={1}".format(
|
||||
update_acmedns_dns_record_response.status_code,
|
||||
self.log_response(update_acmedns_dns_record_response),
|
||||
)
|
||||
)
|
||||
if update_acmedns_dns_record_response.status_code != 200:
|
||||
# raise error so that we do not continue to make calls to ACME
|
||||
# server
|
||||
raise ValueError(
|
||||
"Error creating acme-model model record: status_code={status_code} response={response}".format(
|
||||
status_code=update_acmedns_dns_record_response.status_code,
|
||||
response=self.log_response(update_acmedns_dns_record_response),
|
||||
)
|
||||
)
|
||||
self.logger.info("create_dns_record_end")
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record")
|
||||
# acme-model doesn't support this
|
||||
self.logger.info("delete_dns_record_success")
|
||||
210
class/sewer/dns_providers/aliyundns.py
Normal file
210
class/sewer/dns_providers/aliyundns.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import json
|
||||
|
||||
try:
|
||||
aliyun_dependencies = True
|
||||
from aliyunsdkcore import client
|
||||
from aliyunsdkalidns.request.v20150109 import DescribeDomainRecordsRequest
|
||||
from aliyunsdkalidns.request.v20150109 import AddDomainRecordRequest
|
||||
from aliyunsdkalidns.request.v20150109 import DeleteDomainRecordRequest
|
||||
except ImportError:
|
||||
aliyun_dependencies = False
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class _ResponseForAliyun(object):
|
||||
"""
|
||||
wrapper aliyun resp to the format sewer wanted.
|
||||
"""
|
||||
|
||||
def __init__(self, status_code=200, content=None, headers=None):
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {}
|
||||
self.content = content or {}
|
||||
self.content = json.dumps(content)
|
||||
super(_ResponseForAliyun, self).__init__()
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
||||
class AliyunDns(common.BaseDns):
|
||||
def __init__(self, key, secret, endpoint="cn-beijing", debug=False):
|
||||
"""
|
||||
aliyun model client
|
||||
:param str key: access key
|
||||
:param str secret: access sceret
|
||||
:param str endpoint: endpoint
|
||||
:param bool debug: if debug?
|
||||
"""
|
||||
super(AliyunDns, self).__init__()
|
||||
if not aliyun_dependencies:
|
||||
raise ImportError(
|
||||
"""You need to install aliyunDns dependencies. run; pip3 install sewer[aliyun]"""
|
||||
)
|
||||
self._key = key
|
||||
self._secret = secret
|
||||
self._endpoint = endpoint
|
||||
self._debug = debug
|
||||
self.clt = client.AcsClient(self._key, self._secret, self._endpoint, debug=self._debug)
|
||||
|
||||
def _send_reqeust(self, request):
|
||||
"""
|
||||
send request to aliyun
|
||||
"""
|
||||
request.set_accept_format("json")
|
||||
try:
|
||||
status, headers, result = self.clt.implementation_of_do_action(request)
|
||||
result = json.loads(result)
|
||||
if "Message" in result or "Code" in result:
|
||||
result["Success"] = False
|
||||
self.logger.warning("aliyundns resp error: %s", result)
|
||||
except Exception as exc:
|
||||
self.logger.warning("aliyundns failed to send request: %s, %s", str(exc), request)
|
||||
status, headers, result = 502, {}, '{"Success": false}'
|
||||
result = json.loads(result)
|
||||
|
||||
if self._debug:
|
||||
self.logger.info("aliyundns request name: %s", request.__class__.__name__)
|
||||
self.logger.info("aliyundns request query: %s", request.get_query_params())
|
||||
return _ResponseForAliyun(status, result, headers)
|
||||
|
||||
def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200):
|
||||
"""
|
||||
query recored items.
|
||||
:param str host: like example.com
|
||||
:param str zone: like menduo.example.com
|
||||
:param str tipe: TXT, CNAME, IP or other
|
||||
:param int page:
|
||||
:param int psize:
|
||||
:return dict: res = {
|
||||
'DomainRecords':
|
||||
{'Record': [
|
||||
{
|
||||
'DomainName': 'menduo.net',
|
||||
'Line': 'default',
|
||||
'Locked': False,
|
||||
'RR': 'zb',
|
||||
'RecordId': '3989515483698964',
|
||||
'Status': 'ENABLE',
|
||||
'TTL': 600,
|
||||
'Type': 'A',
|
||||
'Value': '127.0.0.1',
|
||||
'Weight': 1
|
||||
},
|
||||
{
|
||||
'DomainName': 'menduo.net',
|
||||
'Line': 'default',
|
||||
'Locked': False,
|
||||
'RR': 'a.sub',
|
||||
'RecordId': '3989515480778964',
|
||||
'Status': 'ENABLE',
|
||||
'TTL': 600,
|
||||
'Type': 'CNAME',
|
||||
'Value': 'h.p.menduo.net',
|
||||
'Weight': 1
|
||||
}
|
||||
]
|
||||
},
|
||||
'PageNumber': 1,
|
||||
'PageSize': 20,
|
||||
'RequestId': 'FC4D02CD-EDCC-4EE8-942F-1497CCC3B10E',
|
||||
'TotalCount': 95
|
||||
}
|
||||
"""
|
||||
request = DescribeDomainRecordsRequest.DescribeDomainRecordsRequest()
|
||||
request.get_action_name()
|
||||
request.set_DomainName(host)
|
||||
request.set_PageNumber(page)
|
||||
request.set_PageSize(psize)
|
||||
if zone:
|
||||
request.set_RRKeyWord(zone)
|
||||
if tipe:
|
||||
request.set_TypeKeyWord(tipe)
|
||||
resp = self._send_reqeust(request)
|
||||
body = resp.json()
|
||||
return body
|
||||
|
||||
def query_recored_id(self, root, zone, tipe="TXT"):
|
||||
"""
|
||||
find recored
|
||||
:param str root: root host, like example.com
|
||||
:param str zone: sub zone, like menduo.example.com
|
||||
:param str tipe: record tipe, TXT, CNAME, IP. we use TXT
|
||||
:return str:
|
||||
"""
|
||||
record_id = None
|
||||
recoreds = self.query_recored_items(root, zone, tipe=tipe)
|
||||
recored_list = recoreds.get("DomainRecords", {}).get("Record", [])
|
||||
recored_item_list = [i for i in recored_list if i["RR"] == zone]
|
||||
if len(recored_item_list):
|
||||
record_id = recored_item_list[0]["RecordId"]
|
||||
return record_id
|
||||
|
||||
@staticmethod
|
||||
def extract_zone(domain_name):
|
||||
"""
|
||||
extract domain to root, sub, acme_txt
|
||||
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
|
||||
:return tuple: root, zone, acme_txt
|
||||
"""
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
if domain_name.count(".") > 1:
|
||||
zone, middle, last = str(domain_name).rsplit(".", 2)
|
||||
root = ".".join([middle, last])
|
||||
acme_txt = "_acme-challenge.%s" % zone
|
||||
else:
|
||||
zone = ""
|
||||
root = domain_name
|
||||
acme_txt = "_acme-challenge"
|
||||
return root, zone, acme_txt
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
"""
|
||||
create a model record
|
||||
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
|
||||
:param str domain_dns_value: the value sewer client passed in.
|
||||
:return _ResponseForAliyun:
|
||||
"""
|
||||
self.logger.info("create_dns_record start: %s", (domain_name, domain_dns_value))
|
||||
root, _, acme_txt = self.extract_zone(domain_name)
|
||||
|
||||
request = AddDomainRecordRequest.AddDomainRecordRequest()
|
||||
request.set_DomainName(root)
|
||||
request.set_TTL(600)
|
||||
request.set_RR(acme_txt)
|
||||
request.set_Type("TXT")
|
||||
request.set_Value(domain_dns_value)
|
||||
resp = self._send_reqeust(request)
|
||||
|
||||
self.logger.info("create_dns_record end: %s", (domain_name, domain_dns_value, resp.json()))
|
||||
|
||||
return resp
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
"""
|
||||
delete a txt record we created just now.
|
||||
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
|
||||
:param str domain_dns_value: the value sewer client passed in. we do not use this.
|
||||
:return _ResponseForAliyun:
|
||||
:return:
|
||||
"""
|
||||
self.logger.info("delete_dns_record start: %s", (domain_name, domain_dns_value))
|
||||
|
||||
root, _, acme_txt = self.extract_zone(domain_name)
|
||||
|
||||
record_id = self.query_recored_id(root, acme_txt)
|
||||
if not record_id:
|
||||
msg = "failed to find record_id of domain: %s, value: %s", domain_name, domain_dns_value
|
||||
self.logger.warning(msg)
|
||||
return
|
||||
|
||||
self.logger.info("start to delete model record, id: %s", record_id)
|
||||
|
||||
request = DeleteDomainRecordRequest.DeleteDomainRecordRequest()
|
||||
request.set_RecordId(record_id)
|
||||
resp = self._send_reqeust(request)
|
||||
|
||||
self.logger.info("delete_dns_record end: %s", (domain_name, domain_dns_value, resp.json()))
|
||||
return resp
|
||||
100
class/sewer/dns_providers/auroradns.py
Normal file
100
class/sewer/dns_providers/auroradns.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# DNS Provider for AuroRa DNS from the dutch hosting provider pcextreme
|
||||
# https://www.pcextreme.nl/aurora/dns
|
||||
# Aurora uses libcloud from apache
|
||||
# https://libcloud.apache.org/
|
||||
try:
|
||||
aurora_dependencies = True
|
||||
from libcloud.dns.providers import get_driver
|
||||
from libcloud.dns.types import Provider, RecordType
|
||||
import tldextract
|
||||
except ImportError:
|
||||
aurora_dependencies = False
|
||||
from . import common
|
||||
|
||||
|
||||
class AuroraDns(common.BaseDns):
|
||||
"""
|
||||
Todo: re-organize this class so that we make it easier to mock things out to
|
||||
facilitate better tests.
|
||||
"""
|
||||
|
||||
dns_provider_name = "aurora"
|
||||
|
||||
def __init__(self, AURORA_API_KEY, AURORA_SECRET_KEY):
|
||||
|
||||
if not aurora_dependencies:
|
||||
raise ImportError(
|
||||
"""You need to install AuroraDns dependencies. run; pip3 install sewer[aurora]"""
|
||||
)
|
||||
|
||||
self.AURORA_API_KEY = AURORA_API_KEY
|
||||
self.AURORA_SECRET_KEY = AURORA_SECRET_KEY
|
||||
super(AuroraDns, self).__init__()
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record")
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
|
||||
extractedDomain = tldextract.extract(domain_name)
|
||||
domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix
|
||||
|
||||
if extractedDomain.subdomain is "":
|
||||
subDomain = "_acme-challenge"
|
||||
else:
|
||||
subDomain = "_acme-challenge." + extractedDomain.subdomain
|
||||
|
||||
cls = get_driver(Provider.AURORADNS)
|
||||
driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY)
|
||||
zone = driver.get_zone(domainSuffix)
|
||||
zone.create_record(name=subDomain, type=RecordType.TXT, data=domain_dns_value)
|
||||
|
||||
self.logger.info("create_dns_record_success")
|
||||
return
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record")
|
||||
|
||||
extractedDomain = tldextract.extract(domain_name)
|
||||
domainSuffix = extractedDomain.domain + "." + extractedDomain.suffix
|
||||
|
||||
if extractedDomain.subdomain is "":
|
||||
subDomain = "_acme-challenge"
|
||||
else:
|
||||
subDomain = "_acme-challenge." + extractedDomain.subdomain
|
||||
|
||||
cls = get_driver(Provider.AURORADNS)
|
||||
driver = cls(key=self.AURORA_API_KEY, secret=self.AURORA_SECRET_KEY)
|
||||
zone = driver.get_zone(domainSuffix)
|
||||
|
||||
records = driver.list_records(zone)
|
||||
for x in records:
|
||||
if x.name == subDomain and x.type == "TXT":
|
||||
record_id = x.id
|
||||
self.logger.info(
|
||||
"Found record "
|
||||
+ subDomain
|
||||
+ "."
|
||||
+ domainSuffix
|
||||
+ " with id : "
|
||||
+ record_id
|
||||
+ "."
|
||||
)
|
||||
record = driver.get_record(zone_id=zone.id, record_id=record_id)
|
||||
driver.delete_record(record)
|
||||
self.logger.info(
|
||||
"Deleted record "
|
||||
+ subDomain
|
||||
+ "."
|
||||
+ domainSuffix
|
||||
+ " with id : "
|
||||
+ record_id
|
||||
+ "."
|
||||
)
|
||||
else:
|
||||
self.logger.info(
|
||||
"Record " + subDomain + "." + domainSuffix + " not found. No record to delete."
|
||||
)
|
||||
|
||||
self.logger.info("delete_dns_record_success")
|
||||
return
|
||||
155
class/sewer/dns_providers/cloudflare.py
Normal file
155
class/sewer/dns_providers/cloudflare.py
Normal file
@@ -0,0 +1,155 @@
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except:
|
||||
import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class CloudFlareDns(common.BaseDns):
|
||||
"""
|
||||
"""
|
||||
|
||||
dns_provider_name = "cloudflare"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
CLOUDFLARE_EMAIL,
|
||||
CLOUDFLARE_API_KEY,
|
||||
CLOUDFLARE_API_BASE_URL="https://api.cloudflare.com/client/v4/",
|
||||
):
|
||||
self.CLOUDFLARE_DNS_ZONE_ID = None
|
||||
self.CLOUDFLARE_EMAIL = CLOUDFLARE_EMAIL
|
||||
self.CLOUDFLARE_API_KEY = CLOUDFLARE_API_KEY
|
||||
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL
|
||||
self.HTTP_TIMEOUT = 65 # seconds
|
||||
|
||||
if CLOUDFLARE_API_BASE_URL[-1] != "/":
|
||||
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL + "/"
|
||||
else:
|
||||
self.CLOUDFLARE_API_BASE_URL = CLOUDFLARE_API_BASE_URL
|
||||
super(CloudFlareDns, self).__init__()
|
||||
|
||||
def find_dns_zone(self, domain_name):
|
||||
self.logger.debug("find_dns_zone")
|
||||
url = urlparse.urljoin(self.CLOUDFLARE_API_BASE_URL, "zones?status=active")
|
||||
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
|
||||
find_dns_zone_response = requests.get(url, headers=headers, timeout=self.HTTP_TIMEOUT)
|
||||
self.logger.debug(
|
||||
"find_dns_zone_response. status_code={0}".format(find_dns_zone_response.status_code)
|
||||
)
|
||||
if find_dns_zone_response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Error creating cloudflare model record: status_code={status_code} response={response}".format(
|
||||
status_code=find_dns_zone_response.status_code,
|
||||
response=self.log_response(find_dns_zone_response),
|
||||
)
|
||||
)
|
||||
|
||||
result = find_dns_zone_response.json()["result"]
|
||||
for i in result:
|
||||
if i["name"] in domain_name:
|
||||
setattr(self, "CLOUDFLARE_DNS_ZONE_ID", i["id"])
|
||||
if isinstance(self.CLOUDFLARE_DNS_ZONE_ID, type(None)):
|
||||
raise ValueError(
|
||||
"Error unable to get DNS zone for domain_name={domain_name}: status_code={status_code} response={response}".format(
|
||||
domain_name=domain_name,
|
||||
status_code=find_dns_zone_response.status_code,
|
||||
response=self.log_response(find_dns_zone_response),
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.debug("find_dns_zone_success")
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record")
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
self.find_dns_zone(domain_name)
|
||||
|
||||
url = urllib.parse.urljoin(
|
||||
self.CLOUDFLARE_API_BASE_URL,
|
||||
"zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID),
|
||||
)
|
||||
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
|
||||
body = {
|
||||
"type": "TXT",
|
||||
"name": "_acme-challenge" + "." + domain_name + ".",
|
||||
"content": "{0}".format(domain_dns_value),
|
||||
}
|
||||
create_cloudflare_dns_record_response = requests.post(
|
||||
url, headers=headers, json=body, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
self.logger.debug(
|
||||
"create_cloudflare_dns_record_response. status_code={0}. response={1}".format(
|
||||
create_cloudflare_dns_record_response.status_code,
|
||||
self.log_response(create_cloudflare_dns_record_response),
|
||||
)
|
||||
)
|
||||
if create_cloudflare_dns_record_response.status_code != 200:
|
||||
# raise error so that we do not continue to make calls to ACME
|
||||
# server
|
||||
raise ValueError(
|
||||
"Error creating cloudflare model record: status_code={status_code} response={response}".format(
|
||||
status_code=create_cloudflare_dns_record_response.status_code,
|
||||
response=self.log_response(create_cloudflare_dns_record_response),
|
||||
)
|
||||
)
|
||||
self.logger.info("create_dns_record_end")
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record")
|
||||
|
||||
class MockResponse(object):
|
||||
def __init__(self, status_code=200, content="mock-response"):
|
||||
self.status_code = status_code
|
||||
self.content = content
|
||||
super(MockResponse, self).__init__()
|
||||
|
||||
def json(self):
|
||||
return {}
|
||||
|
||||
delete_dns_record_response = MockResponse()
|
||||
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
|
||||
|
||||
dns_name = "_acme-challenge" + "." + domain_name
|
||||
list_dns_payload = {"type": "TXT", "name": dns_name}
|
||||
list_dns_url = urllib.parse.urljoin(
|
||||
self.CLOUDFLARE_API_BASE_URL,
|
||||
"zones/{0}/dns_records".format(self.CLOUDFLARE_DNS_ZONE_ID),
|
||||
)
|
||||
|
||||
list_dns_response = requests.get(
|
||||
list_dns_url, params=list_dns_payload, headers=headers, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
|
||||
for i in range(0, len(list_dns_response.json()["result"])):
|
||||
dns_record_id = list_dns_response.json()["result"][i]["id"]
|
||||
url = urllib.parse.urljoin(
|
||||
self.CLOUDFLARE_API_BASE_URL,
|
||||
"zones/{0}/dns_records/{1}".format(self.CLOUDFLARE_DNS_ZONE_ID, dns_record_id),
|
||||
)
|
||||
headers = {"X-Auth-Email": self.CLOUDFLARE_EMAIL, "X-Auth-Key": self.CLOUDFLARE_API_KEY}
|
||||
delete_dns_record_response = requests.delete(
|
||||
url, headers=headers, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
self.logger.debug(
|
||||
"delete_dns_record_response. status_code={0}. response={1}".format(
|
||||
delete_dns_record_response.status_code,
|
||||
self.log_response(delete_dns_record_response),
|
||||
)
|
||||
)
|
||||
if delete_dns_record_response.status_code != 200:
|
||||
# extended logging for debugging
|
||||
# we do not need to raise exception
|
||||
self.logger.error(
|
||||
"delete_dns_record_response. status_code={0}. response={1}".format(
|
||||
delete_dns_record_response.status_code,
|
||||
self.log_response(delete_dns_record_response),
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.info("delete_dns_record_success")
|
||||
|
||||
77
class/sewer/dns_providers/common.py
Normal file
77
class/sewer/dns_providers/common.py
Normal file
@@ -0,0 +1,77 @@
|
||||
import logging
|
||||
|
||||
|
||||
class BaseDns(object):
|
||||
"""
|
||||
"""
|
||||
|
||||
def __init__(self, LOG_LEVEL="INFO"):
|
||||
self.LOG_LEVEL = LOG_LEVEL
|
||||
self.dns_provider_name = self.__class__.__name__
|
||||
|
||||
self.logger = logging.getLogger("sewer")
|
||||
handler = logging.StreamHandler()
|
||||
formatter = logging.Formatter("%(message)s")
|
||||
handler.setFormatter(formatter)
|
||||
if not self.logger.handlers:
|
||||
self.logger.addHandler(handler)
|
||||
self.logger.setLevel(self.LOG_LEVEL)
|
||||
|
||||
def log_response(self, response):
|
||||
"""
|
||||
renders a python-requests response as json or as a string
|
||||
"""
|
||||
try:
|
||||
log_body = response.json()
|
||||
except ValueError:
|
||||
log_body = response.content
|
||||
return log_body
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
"""
|
||||
Method that creates/adds a model TXT record for a domain/subdomain name on
|
||||
a chosen DNS provider.
|
||||
|
||||
:param domain_name: :string: The domain/subdomain name whose model record ought to be
|
||||
created/added on a chosen DNS provider.
|
||||
:param domain_dns_value: :string: The value/content of the TXT record that will be
|
||||
created/added for the given domain/subdomain
|
||||
|
||||
This method should return None
|
||||
|
||||
Basic Usage:
|
||||
If the value of the `domain_name` variable is example.com and the value of
|
||||
`domain_dns_value` is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld
|
||||
Then, your implementation of this method ought to create a DNS TXT record
|
||||
whose name is '_acme-challenge' + '.' + domain_name + '.' (ie: _acme-challenge.example.com. )
|
||||
and whose value/content is HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld
|
||||
|
||||
Using a model client like dig(https://linux.die.net/man/1/dig) to do a model lookup should result
|
||||
in something like:
|
||||
dig TXT _acme-challenge.example.com
|
||||
...
|
||||
;; ANSWER SECTION:
|
||||
_acme-challenge.example.com. 120 IN TXT "HAJA_4MkowIFByHhFaP8u035skaM91lTKplKld"
|
||||
_acme-challenge.singularity.brandur.org. 120 IN TXT "9C0DqKC_4MkowIFByHhFaP8u0Zv4z7Wz2IHM91lTKec"
|
||||
Optionally, you may also use an online model client like: https://toolbox.googleapps.com/apps/dig/#TXT/
|
||||
|
||||
Please consult your model provider on how/format of their DNS TXT records.
|
||||
You may also want to consult the cloudflare DNS implementation that is found in this repository.
|
||||
"""
|
||||
self.logger.info("create_dns_record")
|
||||
raise NotImplementedError("create_dns_record method must be implemented.")
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
"""
|
||||
Method that deletes/removes a model TXT record for a domain/subdomain name on
|
||||
a chosen DNS provider.
|
||||
|
||||
:param domain_name: :string: The domain/subdomain name whose model record ought to be
|
||||
deleted/removed on a chosen DNS provider.
|
||||
:param domain_dns_value: :string: The value/content of the TXT record that will be
|
||||
deleted/removed for the given domain/subdomain
|
||||
|
||||
This method should return None
|
||||
"""
|
||||
self.logger.info("delete_dns_record")
|
||||
raise NotImplementedError("delete_dns_record method must be implemented.")
|
||||
121
class/sewer/dns_providers/dnspod.py
Normal file
121
class/sewer/dns_providers/dnspod.py
Normal file
@@ -0,0 +1,121 @@
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except:
|
||||
import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class DNSPodDns(common.BaseDns):
|
||||
"""
|
||||
"""
|
||||
|
||||
dns_provider_name = "dnspod"
|
||||
|
||||
def __init__(self, DNSPOD_ID, DNSPOD_API_KEY, DNSPOD_API_BASE_URL="https://dnsapi.cn/"):
|
||||
self.DNSPOD_ID = DNSPOD_ID
|
||||
self.DNSPOD_API_KEY = DNSPOD_API_KEY
|
||||
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL
|
||||
self.HTTP_TIMEOUT = 65 # seconds
|
||||
self.DNSPOD_LOGIN = "{0},{1}".format(self.DNSPOD_ID, self.DNSPOD_API_KEY)
|
||||
|
||||
if DNSPOD_API_BASE_URL[-1] != "/":
|
||||
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL + "/"
|
||||
else:
|
||||
self.DNSPOD_API_BASE_URL = DNSPOD_API_BASE_URL
|
||||
super(DNSPodDns, self).__init__()
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record")
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
subd = ""
|
||||
if domain_name.count(".") != 1: # not top level domain
|
||||
pos = domain_name.rfind(".", 0, domain_name.rfind("."))
|
||||
subd = domain_name[:pos]
|
||||
domain_name = domain_name[pos + 1 :]
|
||||
if subd != "":
|
||||
subd = "." + subd
|
||||
|
||||
url = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Create")
|
||||
body = {
|
||||
"record_type": "TXT",
|
||||
"domain": domain_name,
|
||||
"sub_domain": "_acme-challenge" + subd,
|
||||
"value": domain_dns_value,
|
||||
"record_line_id": "0",
|
||||
"format": "json",
|
||||
"login_token": self.DNSPOD_LOGIN,
|
||||
}
|
||||
create_dnspod_dns_record_response = requests.post(
|
||||
url, data=body, timeout=self.HTTP_TIMEOUT
|
||||
).json()
|
||||
self.logger.debug(
|
||||
"create_dnspod_dns_record_response. status_code={0}. response={1}".format(
|
||||
create_dnspod_dns_record_response["status"]["code"],
|
||||
create_dnspod_dns_record_response["status"]["message"],
|
||||
)
|
||||
)
|
||||
if create_dnspod_dns_record_response["status"]["code"] != "1":
|
||||
# raise error so that we do not continue to make calls to ACME
|
||||
# server
|
||||
raise ValueError(
|
||||
"Error creating dnspod model record: status_code={status_code} response={response}".format(
|
||||
status_code=create_dnspod_dns_record_response["status"]["code"],
|
||||
response=create_dnspod_dns_record_response["status"]["message"],
|
||||
)
|
||||
)
|
||||
self.logger.info("create_dns_record_end")
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record")
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
subd = ""
|
||||
if domain_name.count(".") != 1: # not top level domain
|
||||
pos = domain_name.rfind(".", 0, domain_name.rfind("."))
|
||||
subd = domain_name[:pos]
|
||||
domain_name = domain_name[pos + 1 :]
|
||||
if subd != "":
|
||||
subd = "." + subd
|
||||
|
||||
url = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.List")
|
||||
# pos = domain_name.rfind(".",0, domain_name.rfind("."))
|
||||
subdomain = "_acme-challenge." + subd
|
||||
rootdomain = domain_name
|
||||
body = {
|
||||
"login_token": self.DNSPOD_LOGIN,
|
||||
"format": "json",
|
||||
"domain": rootdomain,
|
||||
"subdomain": subdomain,
|
||||
"record_type": "TXT",
|
||||
}
|
||||
list_dns_response = requests.post(url, data=body, timeout=self.HTTP_TIMEOUT).json()
|
||||
if list_dns_response["status"]["code"] != "1":
|
||||
self.logger.error(
|
||||
"list_dns_record_response. status_code={0}. message={1}".format(
|
||||
list_dns_response["status"]["code"], list_dns_response["status"]["message"]
|
||||
)
|
||||
)
|
||||
for i in range(0, len(list_dns_response["records"])):
|
||||
rid = list_dns_response["records"][i]["id"]
|
||||
urlr = urllib.parse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove")
|
||||
bodyr = {
|
||||
"login_token": self.DNSPOD_LOGIN,
|
||||
"format": "json",
|
||||
"domain": rootdomain,
|
||||
"record_id": rid,
|
||||
}
|
||||
delete_dns_record_response = requests.post(
|
||||
urlr, data=bodyr, timeout=self.HTTP_TIMEOUT
|
||||
).json()
|
||||
if delete_dns_record_response["status"]["code"] != "1":
|
||||
self.logger.error(
|
||||
"delete_dns_record_response. status_code={0}. message={1}".format(
|
||||
delete_dns_record_response["status"]["code"],
|
||||
delete_dns_record_response["status"]["message"],
|
||||
)
|
||||
)
|
||||
|
||||
self.logger.info("delete_dns_record_success")
|
||||
63
class/sewer/dns_providers/duckdns.py
Normal file
63
class/sewer/dns_providers/duckdns.py
Normal file
@@ -0,0 +1,63 @@
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except:
|
||||
import urlparse
|
||||
import requests
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class DuckDNSDns(common.BaseDns):
|
||||
|
||||
dns_provider_name = "duckdns"
|
||||
|
||||
def __init__(self, duckdns_token, DUCKDNS_API_BASE_URL="https://www.duckdns.org"):
|
||||
|
||||
self.duckdns_token = duckdns_token
|
||||
self.HTTP_TIMEOUT = 65 # seconds
|
||||
|
||||
if DUCKDNS_API_BASE_URL[-1] != "/":
|
||||
self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL + "/"
|
||||
else:
|
||||
self.DUCKDNS_API_BASE_URL = DUCKDNS_API_BASE_URL
|
||||
super(DuckDNSDns, self).__init__()
|
||||
|
||||
def _common_dns_record(self, logger_info, domain_name, payload_end_arg):
|
||||
self.logger.info("{0}".format(logger_info))
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
# add provider domain to the domain name if not present
|
||||
provider_domain = ".duckdns.org"
|
||||
if domain_name.rfind(provider_domain) == -1:
|
||||
"".join((domain_name, provider_domain))
|
||||
|
||||
url = urlparse.urljoin(self.DUCKDNS_API_BASE_URL, "update")
|
||||
|
||||
payload = dict([("domains", domain_name), ("token", self.duckdns_token), payload_end_arg])
|
||||
update_duckdns_dns_record_response = requests.get(
|
||||
url, params=payload, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
|
||||
normalized_response = update_duckdns_dns_record_response.text
|
||||
self.logger.debug(
|
||||
"update_duckdns_dns_record_response. status_code={0}. response={1}".format(
|
||||
update_duckdns_dns_record_response.status_code, normalized_response
|
||||
)
|
||||
)
|
||||
|
||||
if update_duckdns_dns_record_response.status_code != 200 or normalized_response != "OK":
|
||||
# raise error so that we do not continue to make calls to DuckDNS
|
||||
# server
|
||||
raise ValueError(
|
||||
"Error creating DuckDNS model record: status_code={status_code} response={response}".format(
|
||||
status_code=update_duckdns_dns_record_response.status_code,
|
||||
response=normalized_response,
|
||||
)
|
||||
)
|
||||
self.logger.info("{0}_success".format(logger_info))
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self._common_dns_record("create_dns_record", domain_name, ("txt", domain_dns_value))
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self._common_dns_record("delete_dns_record", domain_name, ("clear", "true"))
|
||||
79
class/sewer/dns_providers/hurricane.py
Normal file
79
class/sewer/dns_providers/hurricane.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Hurricane Electric DNS Support
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
hedns_dependencies = True
|
||||
import HurricaneDNS as _hurricanedns
|
||||
except ImportError:
|
||||
hedns_dependencies = False
|
||||
|
||||
from . import common
|
||||
|
||||
|
||||
class _Response(object):
|
||||
"""
|
||||
wrapper aliyun resp to the format sewer wanted.
|
||||
"""
|
||||
|
||||
def __init__(self, status_code=200, content=None, headers=None):
|
||||
self.status_code = status_code
|
||||
self.headers = headers or {}
|
||||
self.content = content or {}
|
||||
self.content = json.dumps(content)
|
||||
super(_Response, self).__init__()
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
||||
class HurricaneDns(common.BaseDns):
|
||||
def __init__(self, username, password):
|
||||
super(HurricaneDns, self).__init__()
|
||||
if not hedns_dependencies:
|
||||
raise ImportError(
|
||||
"""You need to install HurricaneDns dependencies. run: pip3 install sewer[hurricane]"""
|
||||
)
|
||||
|
||||
self.clt = _hurricanedns.HurricaneDNS(username, password)
|
||||
|
||||
@staticmethod
|
||||
def extract_zone(domain_name):
|
||||
"""
|
||||
extract domain to root, sub, acme_txt
|
||||
:param str domain_name: the value sewer client passed in, like *.menduo.example.com
|
||||
:return tuple: root, zone, acme_txt
|
||||
"""
|
||||
# if we have been given a wildcard name, strip wildcard
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
if domain_name.count(".") > 1:
|
||||
zone, middle, last = str(domain_name).rsplit(".", 2)
|
||||
root = ".".join([middle, last])
|
||||
acme_txt = "_acme-challenge.%s" % zone
|
||||
else:
|
||||
zone = ""
|
||||
root = domain_name
|
||||
acme_txt = "_acme-challenge"
|
||||
return root, zone, acme_txt
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record start: %s", (domain_name, domain_dns_value))
|
||||
|
||||
root, _, acme_txt = self.extract_zone(domain_name)
|
||||
self.clt.add_record(root, acme_txt, "TXT", domain_dns_value, ttl=300)
|
||||
|
||||
self.logger.info("create_dns_record end: %s", (domain_name, domain_dns_value))
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record start: %s", (domain_name, domain_dns_value))
|
||||
|
||||
root, _, acme_txt = self.extract_zone(domain_name)
|
||||
host = "%s.%s" % (acme_txt, root)
|
||||
|
||||
recored_list = self.clt.get_records(root, host, "TXT")
|
||||
|
||||
for i in recored_list:
|
||||
self.clt.del_record(root, i["id"])
|
||||
|
||||
self.logger.info("delete_dns_record end: %s", (domain_name, domain_dns_value))
|
||||
242
class/sewer/dns_providers/rackspace.py
Normal file
242
class/sewer/dns_providers/rackspace.py
Normal file
@@ -0,0 +1,242 @@
|
||||
try:
|
||||
import urllib.parse as urlparse
|
||||
except:
|
||||
import urlparse
|
||||
import requests
|
||||
from . import common
|
||||
|
||||
try:
|
||||
rackspace_dependencies = True
|
||||
import tldextract
|
||||
except ImportError:
|
||||
rackspace_dependencies = False
|
||||
|
||||
import time
|
||||
|
||||
|
||||
class RackspaceDns(common.BaseDns):
|
||||
"""
|
||||
"""
|
||||
|
||||
dns_providername = "rackspace"
|
||||
|
||||
def get_rackspace_credentials(self):
|
||||
self.logger.debug("get_rackspace_credentials")
|
||||
RACKSPACE_IDENTITY_URL = "https://identity.api.rackspacecloud.com/v2.0/tokens"
|
||||
payload = {
|
||||
"auth": {
|
||||
"RAX-KSKEY:apiKeyCredentials": {
|
||||
"username": self.RACKSPACE_USERNAME,
|
||||
"apiKey": self.RACKSPACE_API_KEY,
|
||||
}
|
||||
}
|
||||
}
|
||||
find_rackspace_api_details_response = requests.post(RACKSPACE_IDENTITY_URL, json=payload)
|
||||
self.logger.debug(
|
||||
"find_rackspace_api_details_response. status_code={0}".format(
|
||||
find_rackspace_api_details_response.status_code
|
||||
)
|
||||
)
|
||||
if find_rackspace_api_details_response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Error getting token and URL details from rackspace identity server: status_code={status_code} response={response}".format(
|
||||
status_code=find_rackspace_api_details_response.status_code,
|
||||
response=self.log_response(find_rackspace_api_details_response),
|
||||
)
|
||||
)
|
||||
data = find_rackspace_api_details_response.json()
|
||||
api_token = data["access"]["token"]["id"]
|
||||
url_data = next(
|
||||
(item for item in data["access"]["serviceCatalog"] if item["type"] == "rax:model"), None
|
||||
)
|
||||
if url_data is None:
|
||||
raise ValueError(
|
||||
"Error finding url data for the rackspace model api in the response from the identity server"
|
||||
)
|
||||
else:
|
||||
api_base_url = url_data["endpoints"][0]["publicURL"] + "/"
|
||||
return (api_token, api_base_url)
|
||||
|
||||
def __init__(self, RACKSPACE_USERNAME, RACKSPACE_API_KEY):
|
||||
|
||||
if not rackspace_dependencies:
|
||||
raise ImportError(
|
||||
"""You need to install RackspaceDns dependencies. run; pip3 install sewer[rackspace]"""
|
||||
)
|
||||
self.RACKSPACE_DNS_ZONE_ID = None
|
||||
self.RACKSPACE_USERNAME = RACKSPACE_USERNAME
|
||||
self.RACKSPACE_API_KEY = RACKSPACE_API_KEY
|
||||
self.HTTP_TIMEOUT = 65 # seconds
|
||||
super(RackspaceDns, self).__init__()
|
||||
self.RACKSPACE_API_TOKEN, self.RACKSPACE_API_BASE_URL = self.get_rackspace_credentials()
|
||||
self.RACKSPACE_HEADERS = {
|
||||
"X-Auth-Token": self.RACKSPACE_API_TOKEN,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def get_dns_zone(self, domain_name):
|
||||
self.logger.debug("get_dns_zone")
|
||||
extracted_domain = tldextract.extract(domain_name)
|
||||
self.RACKSPACE_DNS_ZONE = ".".join([extracted_domain.domain, extracted_domain.suffix])
|
||||
|
||||
def find_dns_zone_id(self, domain_name):
|
||||
self.logger.debug("find_dns_zone_id")
|
||||
self.get_dns_zone(domain_name)
|
||||
url = self.RACKSPACE_API_BASE_URL + "domains"
|
||||
find_dns_zone_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS)
|
||||
self.logger.debug(
|
||||
"find_dns_zone_id_response. status_code={0}".format(
|
||||
find_dns_zone_id_response.status_code
|
||||
)
|
||||
)
|
||||
if find_dns_zone_id_response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Error getting rackspace model domain info: status_code={status_code} response={response}".format(
|
||||
status_code=find_dns_zone_id_response.status_code,
|
||||
response=self.log_response(find_dns_zone_id_response),
|
||||
)
|
||||
)
|
||||
result = find_dns_zone_id_response.json()
|
||||
domain_data = next(
|
||||
(item for item in result["domains"] if item["name"] == self.RACKSPACE_DNS_ZONE), None
|
||||
)
|
||||
if domain_data is None:
|
||||
raise ValueError(
|
||||
"Error finding information for {dns_zone} in model response data:\n{response_data})".format(
|
||||
dns_zone=self.RACKSPACE_DNS_ZONE,
|
||||
response_data=self.log_response(find_dns_zone_id_response),
|
||||
)
|
||||
)
|
||||
dns_zone_id = domain_data["id"]
|
||||
self.logger.debug("find_dns_zone_id_success")
|
||||
return dns_zone_id
|
||||
|
||||
def find_dns_record_id(self, domain_name, domain_dns_value):
|
||||
self.logger.debug("find_dns_record_id")
|
||||
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
|
||||
url = self.RACKSPACE_API_BASE_URL + "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID)
|
||||
find_dns_record_id_response = requests.get(url, headers=self.RACKSPACE_HEADERS)
|
||||
self.logger.debug(
|
||||
"find_dns_record_id_response. status_code={0}".format(
|
||||
find_dns_record_id_response.status_code
|
||||
)
|
||||
)
|
||||
self.logger.debug(url)
|
||||
if find_dns_record_id_response.status_code != 200:
|
||||
raise ValueError(
|
||||
"Error finding model records for {dns_zone}: status_code={status_code} response={response}".format(
|
||||
dns_zone=self.RACKSPACE_DNS_ZONE,
|
||||
status_code=find_dns_record_id_response.status_code,
|
||||
response=self.log_response(find_dns_record_id_response),
|
||||
)
|
||||
)
|
||||
records = find_dns_record_id_response.json()["records"]
|
||||
RACKSPACE_RECORD_DATA = next(
|
||||
(item for item in records if item["data"] == domain_dns_value), None
|
||||
)
|
||||
if RACKSPACE_RECORD_DATA is None:
|
||||
raise ValueError(
|
||||
"Couldn't find record with name {domain_name}\ncontaining data: {domain_dns_value}\nin the response data:{response_data}".format(
|
||||
domain_name=domain_name,
|
||||
domain_dns_value=domain_dns_value,
|
||||
response_data=self.log_response(find_dns_record_id_response),
|
||||
)
|
||||
)
|
||||
record_id = RACKSPACE_RECORD_DATA["id"]
|
||||
self.logger.debug("find_dns_record_id success")
|
||||
return record_id
|
||||
|
||||
def poll_callback_url(self, callback_url):
|
||||
start_time = time.time()
|
||||
while True:
|
||||
callback_url_response = requests.get(callback_url, headers=self.RACKSPACE_HEADERS)
|
||||
if time.time() > start_time + self.HTTP_TIMEOUT:
|
||||
raise ValueError(
|
||||
"Timed out polling callbackurl for model record status. Last status_code={status_code} last response={response}".format(
|
||||
status_code=callback_url_response.status_code,
|
||||
response=self.log_response(callback_url_response),
|
||||
)
|
||||
)
|
||||
if callback_url_response.status_code != 200:
|
||||
raise Exception(
|
||||
"Could not get model record status from callback url. Status code ={status_code}. response={response}".format(
|
||||
status_code=callback_url_response.status_code,
|
||||
response=self.log_response(callback_url_response),
|
||||
)
|
||||
)
|
||||
if callback_url_response.json()["status"] == "ERROR":
|
||||
raise Exception(
|
||||
"Error in creating/deleting model record: status_Code={status_code}. response={response}".format(
|
||||
status_code=callback_url_response.status_code,
|
||||
response=self.log_response(callback_url_response),
|
||||
)
|
||||
)
|
||||
if callback_url_response.json()["status"] == "COMPLETED":
|
||||
break
|
||||
|
||||
def create_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("create_dns_record")
|
||||
# strip wildcard if present
|
||||
domain_name = domain_name.lstrip("*.")
|
||||
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
|
||||
record_name = "_acme-challenge." + domain_name
|
||||
url = urlparse.urljoin(
|
||||
self.RACKSPACE_API_BASE_URL, "domains/{0}/records".format(self.RACKSPACE_DNS_ZONE_ID)
|
||||
)
|
||||
body = {
|
||||
"records": [{"name": record_name, "type": "TXT", "data": domain_dns_value, "ttl": 3600}]
|
||||
}
|
||||
create_rackspace_dns_record_response = requests.post(
|
||||
url, headers=self.RACKSPACE_HEADERS, json=body, timeout=self.HTTP_TIMEOUT
|
||||
)
|
||||
self.logger.debug(
|
||||
"create_rackspace_dns_record_response. status_code={status_code}".format(
|
||||
status_code=create_rackspace_dns_record_response.status_code
|
||||
)
|
||||
)
|
||||
if create_rackspace_dns_record_response.status_code != 202:
|
||||
raise ValueError(
|
||||
"Error creating rackspace model record: status_code={status_code} response={response}".format(
|
||||
status_code=create_rackspace_dns_record_response.status_code,
|
||||
response=create_rackspace_dns_record_response.text,
|
||||
)
|
||||
)
|
||||
# response=self.log_response(create_rackspace_dns_record_response)))
|
||||
# After posting the model record we want created, the response gives us a url to check that will
|
||||
# update when the job is done
|
||||
callback_url = create_rackspace_dns_record_response.json()["callbackUrl"]
|
||||
self.poll_callback_url(callback_url)
|
||||
self.logger.info(
|
||||
"create_dns_record_success. Name: {record_name} Data: {data}".format(
|
||||
record_name=record_name, data=domain_dns_value
|
||||
)
|
||||
)
|
||||
|
||||
def delete_dns_record(self, domain_name, domain_dns_value):
|
||||
self.logger.info("delete_dns_record")
|
||||
record_name = "_acme-challenge." + domain_name
|
||||
self.RACKSPACE_DNS_ZONE_ID = self.find_dns_zone_id(domain_name)
|
||||
self.RACKSPACE_RECORD_ID = self.find_dns_record_id(domain_name, domain_dns_value)
|
||||
url = self.RACKSPACE_API_BASE_URL + "domains/{domain_id}/records/?id={record_id}".format(
|
||||
domain_id=self.RACKSPACE_DNS_ZONE_ID, record_id=self.RACKSPACE_RECORD_ID
|
||||
)
|
||||
delete_dns_record_response = requests.delete(url, headers=self.RACKSPACE_HEADERS)
|
||||
# After sending a delete request, if all goes well, we get a 202 from the server and a URL that we can poll
|
||||
# to see when the job is done
|
||||
self.logger.debug(
|
||||
"delete_dns_record_response={0}".format(delete_dns_record_response.status_code)
|
||||
)
|
||||
if delete_dns_record_response.status_code != 202:
|
||||
raise ValueError(
|
||||
"Error deleting rackspace model record: status_code={status_code} response={response}".format(
|
||||
status_code=delete_dns_record_response.status_code,
|
||||
response=self.log_response(delete_dns_record_response),
|
||||
)
|
||||
)
|
||||
callback_url = delete_dns_record_response.json()["callbackUrl"]
|
||||
self.poll_callback_url(callback_url)
|
||||
self.logger.info(
|
||||
"delete_dns_record_success. Name: {record_name} Data: {data}".format(
|
||||
record_name=record_name, data=domain_dns_value
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user