Files
dromara--domain-admin/domain_admin/service/issue_certificate_service.py
T
2026-07-13 12:31:34 +08:00

688 lines
20 KiB
Python

# -*- coding: utf-8 -*-
"""
@File : issue_certificate_service.py
@Date : 2023-07-23
"""
import json
import time
import traceback
from datetime import datetime, timedelta
import OpenSSL
import requests
from domain_admin.enums.dns_type_enum import DnsTypeEnum
from domain_admin.enums.host_auth_type_enum import HostAuthTypeEnum
from domain_admin.log import logger
from domain_admin.model.dns_model import DnsModel
from domain_admin.model.host_model import HostModel
from domain_admin.model.issue_certificate_model import IssueCertificateModel, ValidStatus, ChallengeDeployTypeEnum, \
SSLDeployTypeEnum
from domain_admin.utils import datetime_util, fabric_util, domain_util
from domain_admin.utils.acme_util import acme_v2_api
from domain_admin.utils.acme_util.challenge_type import ChallengeType
from domain_admin.utils.acme_util.directory_type_enum import DirectoryTypeEnum
from domain_admin.utils.acme_util.key_type_enum import KeyTypeEnum
from domain_admin.utils.cert_util import cert_common
from domain_admin.utils.flask_ext.app_exception import AppException
from domain_admin.utils.open_api import aliyun_domain_api, tencentcloud_domain_api, aliyun_oss_api, aliyun_cdn_api, \
aliyun_dcdn_api
from domain_admin.utils.open_api.aliyun_domain_api import RecordTypeEnum
from domain_admin import config
def issue_certificate(
domains, user_id,
directory_type=DirectoryTypeEnum.LETS_ENCRYPT,
key_type=KeyTypeEnum.RSA
):
"""
申请新证书
:param key_type:
:param directory_type:
:param domains:
:param user_id:
:return:
"""
# Issue certificate
# Create domain private key and CSR
pkey_pem, csr_pem = acme_v2_api.new_csr_comp(domains=domains)
issue_certificate_row = IssueCertificateModel.create(
user_id=user_id,
# challenge_type=challenge_type,
domain_raw=json.dumps(domains),
ssl_certificate_key=pkey_pem,
directory_type=directory_type,
key_type=key_type,
# status='pending',
)
return issue_certificate_row
def get_certificate_challenges(issue_certificate_id):
"""
获取验证方式
:param issue_certificate_id:
:return:
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
# Create domain private key and CSR
domains = issue_certificate_row.domains
pkey_pem = issue_certificate_row.ssl_certificate_key
pkey_pem, csr_pem = acme_v2_api.new_csr_comp(
domains=domains,
pkey_pem=pkey_pem,
)
acme_client = acme_v2_api.get_acme_client(
directory_type=issue_certificate_row.directory_type,
key_type=issue_certificate_row.key_type
)
orderr = acme_client.new_order(csr_pem)
# Select HTTP-01 within offered challenges by the CA server
lst = []
for domain, domain_challenges in acme_v2_api.select_challenge(orderr).items():
for challenge in domain_challenges:
response, validation = challenge.response_and_validation(acme_client.net.key)
data = {
'domain': domain,
'sub_domain': domain_util.get_subdomain(domain),
'root_domain': domain_util.get_root_domain(domain),
'validation': validation,
'challenge': challenge
}
lst.append(data)
return lst
def verify_certificate(issue_certificate_id, challenge_type, verify_deploy_path, host_id, dns_id):
"""
验证域名
:param issue_certificate_id:
:param challenge_type:
:return:
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
items = get_certificate_challenges(issue_certificate_id)
acme_client = acme_v2_api.get_acme_client(
directory_type=issue_certificate_row.directory_type,
key_type=issue_certificate_row.key_type
)
# 如果有challenge_type对应的部署信息,则进行部署
if challenge_type == ChallengeType.HTTP01 and verify_deploy_path and host_id:
itemsCopy = []
for item in items:
temp = item['challenge'].to_json()
temp['validation'] = item['validation']
itemsCopy.append(temp)
deploy_verify_file(
host_id=host_id,
verify_deploy_path=verify_deploy_path,
challenges=itemsCopy
)
if challenge_type == ChallengeType.DNS01 and dns_id:
add_dns_domain_record(
dns_id=dns_id,
challenges=items
)
# 开始验证
verify_count = 0
for item in items:
challenge = item['challenge']
challenge_json = challenge.to_json()
# 指定认证类型
if challenge_type != challenge_json['type']:
continue
response, validation = challenge.response_and_validation(acme_client.net.key)
logger.info(validation)
# Let the CA server know that we are ready for the challenge.
acme_client.answer_challenge(challenge, response)
count = 0
max_count = 5
while True:
count += 1
status = get_challenge_status(challenge_json['url'])
if status == 'valid':
IssueCertificateModel.update(
status=status,
update_time=datetime_util.get_datetime()
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
break
if count >= max_count:
raise AppException("域名验证失败:{}".format(item['domain']))
time.sleep(count)
verify_count += 1
if verify_count == 0:
raise AppException("域名验证失败")
# if success update challenge_type
IssueCertificateModel.update(
challenge_type=challenge_type,
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()
def renew_certificate(row_id):
"""
Renew certificate
:param row_id:
:return:
"""
issue_certificate_row = IssueCertificateModel.get_by_id(row_id)
pkey_pem = issue_certificate_row.ssl_certificate_key
domains = issue_certificate_row.domains
acme_client = acme_v2_api.get_acme_client(
directory_type=issue_certificate_row.directory_type,
key_type=issue_certificate_row.key_type
)
# Create domain private key and CSR
pkey_pem, csr_pem = acme_v2_api.new_csr_comp(
domains=domains,
pkey_pem=pkey_pem,
)
orderr = acme_client.new_order(csr_pem)
# Select HTTP-01 within offered challenges by the CA server
# challb = acme_v2_api.select_challenge(orderr, challenge_type)
# logger.debug(json_util.json_dump(challb.to_json()))
# Performing challenge
fullchain_pem = acme_v2_api.perform_http01(acme_client, orderr)
logger.debug(fullchain_pem)
fullchain_com = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, fullchain_pem
)
cert = cert_common.parse_cert(fullchain_com)
IssueCertificateModel.update(
ssl_certificate=fullchain_pem,
start_time=cert.notBefore,
expire_time=cert.notAfter,
update_time=datetime_util.get_datetime()
).where(
IssueCertificateModel.id == issue_certificate_row.id
).execute()
def get_challenge_status(url):
"""
:param url:
:return:
{
"type": "http-01",
"status": "valid",
"url": "https://acme-staging-v02.api.letsencrypt.org/acme/chall-v3/8189968164/2x43yw",
"token": "uEy2gzPlQN1IIE541sbRK23AQWFh87MvmvzMDOUvIKk",
"validationRecord": [],
"validated": "2023-07-23T08:59:59Z"
}
"""
logger.debug(url)
res = requests.get(url)
data = res.json()
logger.debug(data)
return data['status']
def renew_all_certificate():
"""
更新所有证书
:return:
"""
now = datetime.now()
notify_expire_time = now + timedelta(days=config.DEFAULT_RENEW_DAYS)
rows = IssueCertificateModel.select().where(
(IssueCertificateModel.is_auto_renew == True)
& (
(IssueCertificateModel.expire_time <= notify_expire_time)
| (IssueCertificateModel.expire_time == None)
)
).order_by(IssueCertificateModel.expire_time.asc())
for row in rows:
try:
renew_certificate_row(row)
IssueCertificateModel.update(
renew_status=0,
renew_message='成功',
update_time=datetime.now()
).where(
IssueCertificateModel.id == row.id
).execute()
except Exception as e:
logger.error(traceback.format_exc())
IssueCertificateModel.update(
renew_status=1,
renew_message=str(e),
update_time=datetime.now()
).where(
IssueCertificateModel.id == row.id
).execute()
def renew_certificate_row(row):
"""
证书自动续期
:param row:
:return:
"""
# 重新申请
pkey_pem, csr_pem = acme_v2_api.new_csr_comp(
domains=row.domains,
)
IssueCertificateModel.update(
ssl_certificate_key=pkey_pem,
ssl_certificate='',
start_time=None,
expire_time=None,
status=ValidStatus.PENDING,
).where(
IssueCertificateModel.id == row.id
)
# 验证文件部署 & 验证域名
verify_certificate(
issue_certificate_id=row.id,
challenge_type=row.challenge_type,
verify_deploy_path=row.deploy_verify_path,
host_id=row.challenge_deploy_id,
dns_id=row.challenge_deploy_id
)
# 下载证书
renew_certificate(row.id)
# 自动部署,重启服务
if row.deploy_type_id == SSLDeployTypeEnum.SSH:
issue_certificate_row = IssueCertificateModel.get_by_id(row.id)
deploy_certificate_file(
host_id=row.deploy_host_id,
key_content=issue_certificate_row.ssl_certificate_key,
pem_content=issue_certificate_row.ssl_certificate,
key_deploy_path=row.deploy_key_file,
pem_deploy_path=row.deploy_fullchain_file,
reload_cmd=row.deploy_reloadcmd
)
elif row.deploy_type_id == SSLDeployTypeEnum.WEB_HOOK:
deploy_ssl_by_web_hook(
issue_certificate_id=row.id,
url=row.deploy_url,
headers=row.deploy_header,
)
elif row.deploy_type_id == SSLDeployTypeEnum.OSS:
deploy_cert_to_oss(
issue_certificate_id=row.id,
dns_id=row.deploy_host_id
)
elif row.deploy_type_id == SSLDeployTypeEnum.CDN:
deploy_cert_to_cdn(
issue_certificate_id=row.id,
dns_id=row.deploy_host_id
)
elif row.deploy_type_id == SSLDeployTypeEnum.DCDN:
deploy_cert_to_dcdn(
issue_certificate_id=row.id,
dns_id=row.deploy_host_id
)
def deploy_verify_file(host_id, verify_deploy_path, challenges):
"""
部署验证文件
:return:
"""
logger.info(challenges)
host_row = HostModel.get_by_id(host_id)
host = host_row.host
port = host_row.port
user = host_row.user
password = host_row.password
private_key = host_row.private_key
auth_type = host_row.auth_type
for row in challenges:
if not row['token']:
raise AppException('token is empty')
verify_deploy_filename = verify_deploy_path + row['token']
if not verify_deploy_filename:
raise AppException('verify_deploy_filename is empty')
logger.debug("verify_deploy_filename: %s", verify_deploy_filename)
if not row['validation']:
raise AppException('validation is empty')
if auth_type == HostAuthTypeEnum.PRIVATE_KEY:
fabric_util.deploy_file_by_key(
host=host,
port=port,
user=user,
private_key=private_key,
content=row['validation'],
remote=verify_deploy_filename
)
else:
fabric_util.deploy_file(
host=host,
port=port,
user=user,
password=password,
content=row['validation'],
remote=verify_deploy_filename
)
def deploy_certificate_file(
host_id,
key_content,
key_deploy_path,
pem_content,
pem_deploy_path,
reload_cmd
):
"""
自动部署SSL证书到服务器
:param host_id: 主机id
:param key_deploy_path: 私钥部署路径
:param key_content: 私钥内容
:param pem_deploy_path: 公钥部署路径
:param pem_content: 公钥内容
:param reload_cmd: 重启命令
:return:
"""
host_row = HostModel.get_by_id(host_id)
host = host_row.host
port = host_row.port
user = host_row.user
password = host_row.password
auth_type = host_row.auth_type
private_key = host_row.private_key
# deploy key
if key_deploy_path:
if auth_type == HostAuthTypeEnum.PRIVATE_KEY:
fabric_util.deploy_file_by_key(
host=host,
port=port,
user=user,
private_key=private_key,
content=key_content,
remote=key_deploy_path
)
else:
fabric_util.deploy_file(
host=host,
port=port,
user=user,
password=password,
content=key_content,
remote=key_deploy_path
)
# deploy ssl_certificate
if pem_deploy_path:
if auth_type == HostAuthTypeEnum.PRIVATE_KEY:
fabric_util.deploy_file_by_key(
host=host,
port=port,
user=user,
private_key=private_key,
content=pem_content,
remote=pem_deploy_path
)
else:
fabric_util.deploy_file(
host=host,
port=port,
user=user,
password=password,
content=pem_content,
remote=pem_deploy_path
)
# reload
if reload_cmd:
if auth_type == HostAuthTypeEnum.PRIVATE_KEY:
fabric_util.run_command_by_key(
host=host,
port=port,
user=user,
private_key=private_key,
command=reload_cmd
)
else:
fabric_util.run_command(
host=host,
port=port,
user=user,
password=password,
command=reload_cmd
)
def add_dns_domain_record(dns_id, challenges):
"""
添加dns记录
:param dns_id:
:param challenges:
:return:
"""
dns_row = DnsModel.get_by_id(dns_id)
for challenge_row in challenges:
challenge_json = challenge_row['challenge'].to_json()
if challenge_json['type'] == ChallengeType.DNS01:
if challenge_row['sub_domain']:
record_key = '_acme-challenge.' + challenge_row['sub_domain']
else:
record_key = '_acme-challenge'
if dns_row.dns_type_id == DnsTypeEnum.ALIYUN:
aliyun_domain_api.add_domain_record(
access_key_id=dns_row.access_key,
access_key_secret=dns_row.secret_key,
domain_name=challenge_row['root_domain'],
record_type=RecordTypeEnum.TXT,
record_key=record_key,
record_value=challenge_row['validation']
)
elif dns_row.dns_type_id == DnsTypeEnum.TENCENT_CLOUD:
tencentcloud_domain_api.add_domain_record(
access_key_id=dns_row.access_key,
access_key_secret=dns_row.secret_key,
domain_name=challenge_row['root_domain'],
record_type=RecordTypeEnum.TXT,
record_key=record_key,
record_value=challenge_row['validation']
)
def deploy_ssl_by_web_hook(issue_certificate_id, url, headers):
"""
通过webhook部署ssl证书
:param issue_certificate_id:
:param url:
:param headers:
:return:
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row:
raise AppException('数据不存在')
data = {
'domains': issue_certificate_row.domains,
'ssl_certificate': issue_certificate_row.ssl_certificate,
'ssl_certificate_key': issue_certificate_row.ssl_certificate_key,
'start_time': datetime_util.format_datetime(issue_certificate_row.start_time),
'expire_time': datetime_util.format_datetime(issue_certificate_row.expire_time),
}
res = requests.request(
method='POST',
url=url,
headers=headers,
json=data
)
if not res.ok:
raise res.raise_for_status()
return res.text
def deploy_cert_to_oss(issue_certificate_id, dns_id):
"""
部署ssl证书到oss
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row:
raise AppException('证书数据不存在')
dns_row = DnsModel.get_by_id(dns_id)
if not dns_row:
raise AppException('DNS数据不存在')
domain = issue_certificate_row.domains[0]
oss_info = aliyun_oss_api.cname_to_oss_info(domain)
if not oss_info:
raise AppException('dns 未设置')
logger.info("oss_info: %s", oss_info)
aliyun_oss_api.put_bucket_cname(
access_key_id=dns_row.access_key,
access_key_secret=dns_row.secret_key,
bucket_name=oss_info['bucket_name'],
domain=domain,
certificate=issue_certificate_row.ssl_certificate,
private_key=issue_certificate_row.ssl_certificate_key,
endpoint=oss_info['endpoint'],
)
def deploy_cert_to_cdn(issue_certificate_id, dns_id):
"""
部署ssl证书到cdn
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row:
raise AppException('证书数据不存在')
dns_row = DnsModel.get_by_id(dns_id)
if not dns_row:
raise AppException('DNS数据不存在')
domain = issue_certificate_row.domains[0]
# oss_info = aliyun_oss_api.cname_to_oss_info(domain)
# if not oss_info:
# raise AppException('dns 未设置')
#
# logger.info("oss_info: %s", oss_info)
aliyun_cdn_api.set_cdn_domain_ssl_certificate_v2(
access_key_id=dns_row.access_key,
access_key_secret=dns_row.secret_key,
domain_name=domain,
certificate=issue_certificate_row.ssl_certificate,
private_key=issue_certificate_row.ssl_certificate_key,
)
def deploy_cert_to_dcdn(issue_certificate_id, dns_id):
"""
部署ssl证书到dcdn
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if not issue_certificate_row:
raise AppException('证书数据不存在')
dns_row = DnsModel.get_by_id(dns_id)
if not dns_row:
raise AppException('DNS数据不存在')
domain = issue_certificate_row.domains[0]
aliyun_dcdn_api.set_dcdn_domain_ssl_certificate(
access_key_id=dns_row.access_key,
access_key_secret=dns_row.secret_key,
domain_name=domain,
certificate=issue_certificate_row.ssl_certificate,
private_key=issue_certificate_row.ssl_certificate_key,
)
def check_auto_renew(issue_certificate_id):
"""
首次申请,自动判断是否可以自动续期
:param issue_certificate_id:
:return:
"""
issue_certificate_row = IssueCertificateModel.get_by_id(issue_certificate_id)
if issue_certificate_row.can_auto_renew:
IssueCertificateModel.update(
is_auto_renew=True
).where(
IssueCertificateModel.id == issue_certificate_id
).execute()