#:coding:utf-8 # The MIT License (MIT) # # Copyright (c) 2017 Komu Wairagu # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "https://github.com/komuw/sewer", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import sys, os import time import copy import json import base64 import hashlib import binascii import urllib if sys.version_info[0] == 2: # python2 import urlparse import urllib2 import cryptography.hazmat import cryptography.hazmat.backends import cryptography.hazmat.primitives.serialization else: # python3 from urllib.parse import urlparse import cryptography import platform import hmac try: import requests except: os.system('btpip install requests') import requests try: import OpenSSL except: os.system('btpip install pyOpenSSL') import OpenSSL import random import datetime import logging from hashlib import sha1 os.chdir("/www/server/panel") if not 'class/' in sys.path: sys.path.insert(0,'class/') import public class ACMEclient(object): def __init__( self, dns_class, domain_alt_names=[], Manual=0, # 手动验证 0 非手动,1 获取手动要添加的txt记录值,2 验证添加后的txt解析记录 contact_email=None, account_key=None, certificate_key=None, bits=2048, digest="sha256", ACME_REQUEST_TIMEOUT=7, # 请求超时时间/s ACME_AUTH_STATUS_WAIT_PERIOD=8, # 认证等待时间/s ACME_AUTH_STATUS_MAX_CHECKS=3, # 验证最大重试次数 ACME_DIRECTORY_URL="https://acme-v02.api.letsencrypt.org/directory", LOG_LEVEL="INFO", ): self.Manual = Manual self.domain_name = domain_alt_names[0] 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"] # 唯一帐户标识符 # 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 print( "初始化成功, domain_names={}, acme_server={}".format( self.all_domain_names, self.ACME_DIRECTORY_URL, ) ) except Exception as e: self.logger.error("{0}. error={1}".format(public.GetMsg("ACME_ERR2"),str(e))) sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR1")})) @staticmethod def log_response(response): try: log_body = response.json() except ValueError: log_body = response.content[:30] return log_body @staticmethod def get_user_agent(): return "{system}: {machine} ".format( system=platform.system(), machine=platform.machine(), ) def get_acme_endpoints(self): print("Get _acme_ endpoint") headers = {"User-Agent": self.User_Agent} i = 0 while i < 3: try: get_acme_endpoints = requests.get( self.ACME_DIRECTORY_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) if get_acme_endpoints.status_code not in [200, 201]: raise ValueError( "{acme}: 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): print("Create certificate_key") return self.create_key().decode() def create_account_key(self): self.logger.debug(public.GetMsg("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): print("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): print("acme registration") 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) if acme_register_response.status_code not in [201, 200, 409]: public.WriteFile(os.path.join(ssl_home_path, "apply_for_cert_issuance_response"), acme_register_response.text, mode="w") raise ValueError( "{ssl_register}: 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) print("acme_register_success") return acme_register_response def apply_for_cert_issuance(self): print("Apply for a certificate") identifiers = [] for domain_name in self.all_domain_names: identifiers.append({"type": "dns", "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) if apply_for_cert_issuance_response.status_code != 201: public.WriteFile(os.path.join(ssl_home_path, "apply_for_cert_issuance_response"), apply_for_cert_issuance_response.text, mode="w") raise ValueError( "{ssl_accept_err}: 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"] print("Successful application for certificate") return authorizations, finalize_url def get_identifier_authorization(self, url): print("Get identifier authorization") headers = {"User-Agent": self.User_Agent} i = 0 while i < 3: try: get_identifier_authorization_response = requests.get( url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) if get_identifier_authorization_response.status_code not in [200, 201]: raise ValueError( "{ssl_accept_err1}: 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"] == "dns-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, } print( "Get identifier authorization successfully. identifier_auth={0}".format(identifier_auth) ) return identifier_auth def get_keyauthorization(self, dns_token): print("Get key authorization") 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, dns_names_to_delete=[]): """ 检查授权的状态,验证dns有没有添加txt解析记录 """ print("Check authorization status") time.sleep(self.ACME_AUTH_STATUS_WAIT_PERIOD) # 等待 desired_status = desired_status or ["pending", "valid"] number_of_checks = 0 while True: headers = {"User-Agent": self.User_Agent} i = 0 while i < 3: try: check_authorization_status_response = requests.get( authorization_url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) authorization_status = check_authorization_status_response.json()["status"] number_of_checks = number_of_checks + 1 if number_of_checks == self.ACME_AUTH_STATUS_MAX_CHECKS: msg = public.GetMsg("SSL_CHECK_TIPS",(number_of_checks,self.ACME_AUTH_STATUS_MAX_CHECKS,self.ACME_AUTH_STATUS_WAIT_PERIOD)) print (msg) for i in dns_names_to_delete: # 验证失败后也删除添加的dns self.dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"]) sys.exit(json.dumps({"status": False, "data": public.GetMsg("CHECK_TXT_ERR5"), "msg": msg, })) if authorization_status in desired_status: break else: print("Failed to verify model txt wait {} seconds to re-verify model, returned information:".format(self.ACME_AUTH_STATUS_WAIT_PERIOD)) print(check_authorization_status_response.json()) public.WriteFile(os.path.join(ssl_home_path, "check_authorization_status_response"), check_authorization_status_response.text, mode="w") # 等待 time.sleep(self.ACME_AUTH_STATUS_WAIT_PERIOD) print("End of checking authorization status") return check_authorization_status_response def respond_to_challenge(self, acme_keyauthorization, dns_challenge_url): print("Response challenge") payload = {"keyAuthorization": acme_keyauthorization} respond_to_challenge_response = self.make_signed_acme_request(dns_challenge_url, payload) print("Response challenge_ success") return respond_to_challenge_response def send_csr(self, finalize_url): 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) if send_csr_response.status_code not in [200, 201]: raise ValueError( "{send_csr_err}: 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"] print("send_csr_success") return certificate_url def download_certificate(self, certificate_url): print("Download the certificate") download_certificate_response = self.make_signed_acme_request( certificate_url, payload="DOWNLOAD_Z_CERTIFICATE" ) if download_certificate_response.status_code not in [200, 201]: raise ValueError( "{get_cert_err}: 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") print("Download certificate successful") 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 对ACME服务器的每个请求都必须包含一个新的未使用的nonce 。 """ print("get_nonce") headers = {"User-Agent": self.User_Agent} i = 0 while i < 3: try: response = requests.get( self.ACME_GET_NONCE_URL, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) nonce = response.headers["Replay-Nonce"] return nonce @staticmethod def stringfy_items(payload): 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): 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): print("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 return header def make_signed_acme_request(self, url, payload): print("Sign acme request") headers = {"User-Agent": self.User_Agent} payload = self.stringfy_items(payload) if payload in ["GET_Z_CHALLENGE", "DOWNLOAD_Z_CERTIFICATE"]: i = 0 while i < 3: try: response = requests.get(url, timeout=self.ACME_REQUEST_TIMEOUT, headers=headers) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) 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"}) i = 0 while i < 3: try: response = requests.post( url, data=data.encode("utf8"), timeout=self.ACME_REQUEST_TIMEOUT, headers=headers ) except Exception: i += 1 else: break else: sys.exit(json.dumps({"data": public.GetMsg("ACME_ERR3")})) 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 = [] domain_txt_dns_value = [] 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) _, _, acme_txt = extract_zone(dns_name) domain_txt_dns_value.append({"acme_txt": acme_txt,"dns_name":dns_name, "acme_txt_value": domain_dns_value}) 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, } ) # 1 if self.Manual == 1: print("Please add txt parsing") print(domain_txt_dns_value) public.WriteFile(os.path.join(ssl_home_path, "domain_txt_dns_value.json"), json.dumps(domain_txt_dns_value), mode="w") public.WriteFile(os.path.join(ssl_home_path, "Confirmation_verification"), "", mode="w") num = 0 while True: if os.path.isfile(os.path.join(ssl_home_path, "Confirmation_verification")): Confirmation_verification = public.ReadFile(os.path.join(ssl_home_path, "Confirmation_verification")) if Confirmation_verification.strip() == "ok": break time.sleep(5) num += 1 if num > 90: timeout_info = json.dumps({"data": public.GetMsg("CHECK_TXT_ERR6"), "status": False}) public.WriteFile(os.path.join(ssl_home_path, "timeout_info"), timeout_info, mode="w") sys.exit(timeout_info) # 5分钟后失效 for i in responders: 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: self.check_authorization_status(i["authorization_url"], ["valid"], dns_names_to_delete) certificate_url = self.send_csr(finalize_url) certificate = self.download_certificate(certificate_url) # except Exception as e: # sys.exit("错误:无法颁发证书. error={0}".format(str(e))) # finally: # for i in dns_names_to_delete: # self.dns_class.delete_dns_record(i["dns_name"], i["domain_dns_value"]) 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): return self.get_certificate() def renew(self): """ 续签证书。 续订实际上是获得新证书。 https://letsencrypt.org/docs/rate-limits/ """ return self.cert() def extract_zone(domain_name): 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 class DNSPodDns(object): 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 def create_dns_record(self, domain_name, domain_dns_value): print("create_dns_record {} {}".format(domain_name, domain_dns_value)) # 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 if sys.version_info[0] == 2: url = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Create") else: url = urllib.parse.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() if create_dnspod_dns_record_response["status"]["code"] != "1": if create_dnspod_dns_record_response["status"]["code"] == "13" or create_dnspod_dns_record_response["status"]["code"] == "7": sys.exit(json.dumps({"data": public.GetMsg("DNSPORD_ERR"), "msg": create_dnspod_dns_record_response})) elif create_dnspod_dns_record_response["status"]["code"] == "10004" or create_dnspod_dns_record_response["status"]["code"] == "10002": sys.exit(json.dumps({"data": public.GetMsg("DNSPORD_ERR1"), "msg": create_dnspod_dns_record_response})) else: sys.exit(json.dumps({"data": create_dnspod_dns_record_response["status"]['message'], "msg": create_dnspod_dns_record_response})) print("create_dns_record_end") def delete_dns_record(self, domain_name, domain_dns_value): print("delete_dns_record", domain_name) 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 if sys.version_info[0] == 2: url = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.List") else: 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": print("list_dns_record_response. status_code={0}. message={1}".format(list_dns_response["status"]["code"], list_dns_response["status"]["message"])) return for i in range(0, len(list_dns_response["records"])): rid = list_dns_response["records"][i]["id"] if sys.version_info[0] == 2: urlr = urlparse.urljoin(self.DNSPOD_API_BASE_URL, "Record.Remove") else: 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": print("delete_dns_record_response. status_code={0}. message={1}".format(delete_dns_record_response["status"]["code"], delete_dns_record_response["status"]["message"], )) print("delete_dns_record_success") class AliyunDns(object): def __init__(self, key, secret, ): self.key = str(key).strip() self.secret = str(secret).strip() self.url = "http://alidns.aliyuncs.com" def sign(self, accessKeySecret, parameters): # '''签名方法 def percent_encode(encodeStr): encodeStr = str(encodeStr) if sys.version_info[0] == 3: res = urllib.parse.quote(encodeStr, '') else: res = urllib2.quote(encodeStr, '') res = res.replace('+', '%20') res = res.replace('*', '%2A') res = res.replace('%7E', '~') return res sortedParameters = sorted(parameters.items(), key=lambda parameters: parameters[0]) canonicalizedQueryString = '' for (k, v) in sortedParameters: canonicalizedQueryString += '&' + percent_encode(k) + '=' + percent_encode(v) stringToSign = 'GET&%2F&' + percent_encode(canonicalizedQueryString[1:]) if sys.version_info[0] == 2: h = hmac.new(accessKeySecret + "&", stringToSign, sha1) else: h = hmac.new(bytes(accessKeySecret + "&", encoding="utf8"), stringToSign.encode('utf8'), sha1) signature = base64.encodestring(h.digest()).strip() return signature def create_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("create_dns_record start: ", acme_txt, domain_dns_value) randomint = random.randint(11111111111111, 99999999999999) now = datetime.datetime.utcnow() otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") paramsdata = { "Action": "AddDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, "DomainName": root, "RR": acme_txt, "Type": "TXT", "Value": domain_dns_value, } Signature = self.sign(self.secret, paramsdata) paramsdata['Signature'] = Signature req = requests.get(url=self.url, params=paramsdata) if req.status_code != 200: if req.json()['Code'] == 'IncorrectDomainUser' or req.json()['Code'] == 'InvalidDomainName.NoExist': sys.exit(json.dumps({"data": public.GetMsg("ALICLOUD_ERR"), "msg": req.json()})) elif req.json()['Code'] == 'InvalidAccessKeyId.NotFound' or req.json()['Code'] == 'SignatureDoesNotMatch': sys.exit(json.dumps({"data": public.GetMsg("API_SK_ERR"), "msg": req.json()})) else: sys.exit(json.dumps({"data": req.json()['Message'], "msg": req.json()})) print("create_dns_record end") def query_recored_items(self, host, zone=None, tipe=None, page=1, psize=200): randomint = random.randint(11111111111111, 99999999999999) now = datetime.datetime.utcnow() otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") paramsdata = { "Action": "DescribeDomainRecords", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, "DomainName": host, } if zone: paramsdata['RRKeyWord'] = zone if tipe: paramsdata['TypeKeyWord'] = tipe Signature = self.sign(self.secret, paramsdata) paramsdata['Signature'] = Signature req = requests.get(url=self.url, params=paramsdata) return req.json() def query_recored_id(self, root, zone, tipe="TXT"): 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 def delete_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("delete_dns_record start: ", acme_txt, domain_dns_value) record_id = self.query_recored_id(root, acme_txt) if not record_id: msg = public.GetMsg("CANT_FIND_RECORDID"), domain_name print(msg) return print("start to delete dns record, id: ", record_id) randomint = random.randint(11111111111111, 99999999999999) now = datetime.datetime.utcnow() otherStyleTime = now.strftime("%Y-%m-%dT%H:%M:%SZ") paramsdata = { "Action": "DeleteDomainRecord", "Format": "json", "Version": "2015-01-09", "SignatureMethod": "HMAC-SHA1", "Timestamp": otherStyleTime, "SignatureVersion": "1.0", "SignatureNonce": str(randomint), "AccessKeyId": self.key, "RecordId": record_id, } Signature = self.sign(self.secret, paramsdata) paramsdata['Signature'] = Signature req = requests.get(url=self.url, params=paramsdata) if req.status_code != 200: sys.exit(json.dumps({"data": public.GetMsg("DEL_RES_FAIL"), "msg": req.json()})) print("delete_dns_record end: ", acme_txt) class CloudxnsDns(object): def __init__(self, key, secret, ): self.key = key self.secret = secret self.APIREQUESTDATE = time.ctime() def get_headers(self, url, parameter=''): APIREQUESTDATE = self.APIREQUESTDATE APIHMAC = public.Md5(self.key + url + parameter + APIREQUESTDATE + self.secret) headers = { "API-KEY": self.key, "API-REQUEST-DATE": APIREQUESTDATE, "API-HMAC": APIHMAC, "API-FORMAT": "json" } return headers def get_domain_list(self): url = "https://www.cloudxns.net/api2/domain" headers = self.get_headers(url) req = requests.get(url=url, headers=headers) req = req.json() if req['code'] != 1: sys.exit(json.dumps({"data": public.GetMsg("SK_ERR"), "status": False, "msg": req})) return req def get_domain_id(self, domain_name): req = self.get_domain_list() for i in req["data"]: if domain_name.strip() == i['domain'][:-1]: return i['id'] # print (i['domain'][:-1], "======", domain_name.strip()) sys.exit(json.dumps({"data": public.GetMsg("CLOUDXNS_ERR"), "status": False, "msg": req})) def create_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("create_dns_record,", acme_txt, domain_dns_value) url = "https://www.cloudxns.net/api2/record" data = { "domain_id": int(self.get_domain_id(root)), "host": acme_txt, "value": domain_dns_value, "type": "TXT", "line_id": 1, } parameter = json.dumps(data) headers = self.get_headers(url, parameter) req = requests.post(url=url, headers=headers, data=parameter) req = req.json() if req['code'] != 1: sys.exit(json.dumps({"data": public.GetMsg("SK_ERR"), "status": False, "msg": req})) print("create_dns_record_end") def delete_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("delete_dns_record start: ", acme_txt, domain_dns_value) url = "https://www.cloudxns.net/api2/record/{}/{}".format(self.get_record_id(root), self.get_domain_id(root)) headers = self.get_headers(url, ) req = requests.delete(url=url, headers=headers, ) req = req.json() print("delete_dns_record_success") def get_record_id(self, domain_name): url = "http://www.cloudxns.net/api2/record/{}?host_id=0&offset=0&row_num=2000".format(self.get_domain_id(domain_name)) headers = self.get_headers(url, ) req = requests.get(url=url, headers=headers, ) req = req.json() for i in req['data']: if i['type'] == "TXT": return i['record_id'] class Dns_com(object): def create_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("create_dns_record,", acme_txt, domain_dns_value) result = public.ExecShell('''{} /www/server/panel/plugin/dns/dns_main.py add_txt {} {}'''.format(public.get_python_bin(),acme_txt + '.' + root, domain_dns_value)) if result[0].strip() == "False": sys.exit(json.dumps({"data": public.GetMsg("BT_DNSRES_ERR")})) print("create_dns_record_end") def delete_dns_record(self, domain_name, domain_dns_value): root, _, acme_txt = extract_zone(domain_name) print("delete_dns_record start: ", acme_txt, domain_dns_value) public.ExecShell('''{} /www/server/panel/plugin/dns/dns_main.py remove_txt {} {}'''.format(public.get_python_bin() ,acme_txt + '.' + root, domain_dns_value)) print("delete_dns_record_success") class Dns_Manual(object): def create_dns_record(self, domain_name, domain_dns_value): pass def delete_dns_record(self, domain_name, domain_dns_value): pass if __name__ == "__main__":#dns调用验证脚本 if len(sys.argv) == 1: print ('''dnspod使用方法: python xxxx.py '{"dnsapi":"dns_dp","key":"ID","secret":"Token","domain_alt_names":"a.com,b.com","contact_email":"290070744@qq.com","dnssleep":10,"path":"/xxx/xxx"}' ''') print ('''阿里dns使用方法: python xxxx.py '{"dnsapi":"dns_ali","key":"AccessKey","secret":"SecretKey","domain_alt_names":"a.com,b.com","contact_email":"290070744@qq.com","dnssleep":10,"path":"/xxx/xxx"}' ''') sys.exit(json.dumps({"data": public.GetMsg("ADD_ARGS_TO_SHELL_ERR")})) print(sys.argv[1]) data = json.loads(sys.argv[1]) # data = {"domain_alt_names": "a1.jiangwenhui.xyz", "dnssleep": "10", "dnsapi": "dns_dp", "secret": "xxxxxxxxxx", "contact_email": "", "key": "xxxxx","path":"/xxx/xxx"} Manual = 0 dnsapi = data['dnsapi'] key = data['key'] secret = data['secret'] if dnsapi == "dns_dp": # 腾讯DnsPod dns_class = DNSPodDns(DNSPOD_ID=key, DNSPOD_API_KEY=secret) elif dnsapi == "dns_ali": # 阿里云DNS dns_class = AliyunDns(key=key, secret=secret) elif dnsapi == "dns_cx": # CloudXns dns_class = CloudxnsDns(key=key, secret=secret) elif dnsapi == "dns_bt": # dns.com dns_class = Dns_com() elif dnsapi == "dns": # 手动的 dns_class = Dns_Manual() Manual = 1 domain_alt_names = data['domain_alt_names'].split(",") ACME_AUTH_STATUS_WAIT_PERIOD = int(data['dnssleep']) # 等待时间/秒 contact_email = data['contact_email'] sitename = data['path'].split("/")[-1] ssl_home_path = os.path.join("/www/server/panel/vhost/cert/", sitename) public.ExecShell("mkdir -p {}".format(ssl_home_path)) if os.path.isfile(os.path.join(ssl_home_path, "account_key.key")): # 如果存在account_key就是续签 with open(os.path.join(ssl_home_path, "account_key.key"), 'r') as account_key_file: account_key = account_key_file.read() else: account_key = None client = ACMEclient( account_key=account_key, Manual=Manual, # 手动验证 0 非手动 ,1 获取手动要添加的txt记录值 contact_email=contact_email, dns_class=dns_class, domain_alt_names=domain_alt_names, ACME_AUTH_STATUS_WAIT_PERIOD=ACME_AUTH_STATUS_WAIT_PERIOD, # 验证域名解析授权状态的等待时间/秒 ACME_AUTH_STATUS_MAX_CHECKS=2, # 检查授权状态的最大次数 # ACME_DIRECTORY_URL="https://acme-staging-v02.api.letsencrypt.org/directory", # 测试api地址 https://acme-staging-v02.api.letsencrypt.org/directory ACME_DIRECTORY_URL="https://acme-v02.api.letsencrypt.org/directory", # 正式ali地址 https://acme-v02.api.letsencrypt.org/directory ACME_REQUEST_TIMEOUT=30, # ACME请求超时/s ) certificate = client.cert() certificate_key = client.certificate_key account_key = client.account_key print("证书certificate:{}".format(certificate)) print("证书certificate_key:{}".format(certificate_key)) print("your letsencrypt.org account key is:{}".format(account_key)) home_key = os.path.join(ssl_home_path, "privkey.pem") home_csr = os.path.join(ssl_home_path, "fullchain.pem") with open(home_csr, 'w') as certificate_file: certificate_file.write(certificate) with open(home_key, 'w') as certificate_key_file: certificate_key_file.write(certificate_key) if not os.path.isfile(os.path.join(ssl_home_path, "account_key.key")): with open(os.path.join(ssl_home_path, "account_key.key"), 'w') as account_key_file: account_key_file.write(account_key) key_install_path = data['path'] ssl_certificate = os.path.join(key_install_path, "fullchain.pem") ssl_certificate_key = os.path.join(key_install_path, "privkey.pem") key_install_path = key_install_path.replace("*.", '') ssl_certificate = ssl_certificate.replace("*.", '') ssl_certificate_key = ssl_certificate_key.replace("*.", '') public.ExecShell("mkdir -p {}".format(key_install_path)) public.ExecShell('''/bin/cp {} {}'''.format(home_csr, ssl_certificate)) public.ExecShell('''/bin/cp {} {}'''.format(home_key, ssl_certificate_key)) print("ssl_success") # 重载Web服务配置 if os.path.exists('/www/server/nginx/sbin/nginx'): result = public.ExecShell('/etc/init.d/nginx reload') if result[1].find('nginx.pid') != -1: public.ExecShell('pkill -9 nginx && sleep 1'); public.ExecShell('/etc/init.d/nginx start'); else: result = public.ExecShell('/etc/init.d/httpd reload')