Files
yakpanel-core/class/sewer_Usage.py
2026-04-07 02:04:22 +05:30

1008 lines
44 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#: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')