Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

11
class/sewer/__init__.py Normal file
View 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

View 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
View 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
View 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
View 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"

View 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

View 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")

View 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

View 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

View 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")

View 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.")

View 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")

View 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"))

View 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))

View 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
)
)