1008 lines
44 KiB
Python
1008 lines
44 KiB
Python
|
|
#: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')
|