Files
aaPanel/class_v2/ssl_domainModelV2/service.py
T
Jack 6f212ce3d6 Update to 7.27.0
1. Added Domains menu to manage Domains and SSL Certificates
2. Added Add Site automatic record creation when adding websites (Only supported by IPv4)
3. Added Vietnamese language
4. Added Indonesian language
5. Added HTTPS Protection function for Website (disables automatic HTTP to HTTPS redirection when enabled)
6. Added Mail Marketing -- Automation trigger tasks
7. Added Mail Marketing -- Groups Import, Export, Merge
8. Added Mail Marketing -- Subscribers to support paste import
9. Added Mail Marketing -- Template Import Export Duplicate
10. Added Mail server -- Other Settings add timed Auto Responder
11. Added Mail Marketing -- Suspend List to detect abnormal mailboxes
12. Added Mail Marketing -- Marketing Task to export error mail logs
13. Added Mail Domain -- SSL certificate expiration alert
14. Optimized Webmail management to display based on domain additions
15. Optimize Website Interface Button Integration
16. Optimize the response speed of WP Toolkit interface
2025-03-26 09:04:29 +08:00

417 lines
15 KiB
Python

# coding: utf-8
import json
import os
import re
import time
from typing import List, Tuple
import public
from BTPanel import app
from acme_v2 import acme_v2
from mod.base.msg import SenderManager
from mod.base.push_mod.manager import PushManager, TaskConfig
from panelDnsapi import extract_zone
from public.aaModel import Q
from ssl_domainModelV2.config import DNS_MAP, DnsTask
from ssl_domainModelV2.model import (
DnsDomainProvider,
DnsDomainRecord,
DnsDomainTask,
DnsDomainSSL,
)
os.chdir('/www/server/panel')
RUNNING = os.path.join(public.get_panel_path(), "class_v2/ssl_domainModelV2/sync_running.pl")
# init status to 0
public.writeFile(RUNNING, "0", "w")
def init_sites_dns(domain_list: list):
"""
添加网站dns初始化
"""
make_suer_renew_task()
if not domain_list:
return
# flask app上下文
with app.app_context():
root_zero, _, _ = extract_zone(domain_list[0].get("domain"))
# 是否同域
for domain in domain_list:
root, _, _ = extract_zone(domain.get("domain"))
if root_zero != root_zero:
public.print_log("domain not the same, not support now")
return
flag = True
ssl_obj = DnsDomainSSL.objects.find_one(hash=domain_list[0].get("hash", ""))
provider = DnsDomainProvider.objects.filter(id=ssl_obj.provider_id).first()
for domain in domain_list:
# ================== record part =======================
root, zone, _ = extract_zone(domain.get("domain"))
# zone 为空时, 则为主域名, 否则添加任意子域名
domain_value = "@" if zone == "" else zone
proxy = 0 if "cf_proxy" not in domain.get("support", []) else 1
# 直接进行记录创建
body = {
"domain": root,
"record": domain_value,
"record_value": public.GetLocalIp(),
"record_type": "A",
"ttl": 1,
"proxy": proxy,
}
try:
if provider:
provider.model_create_dns_record(body)
except Exception:
import traceback
public.print_log(traceback.format_exc())
# ================== domain part=======================
if ssl_obj and not DomainValid.match_ssl_dns(domain.get("domain", ""), ssl_obj.dns):
flag = False
# ================== cert part =======================
new_domains = [x.get("domain", "") for x in domain_list]
if not flag:
try:
if ssl_obj.provider_id == 0: # 可能是上传的证书
# todo
return
else: # legal provider
provider = DnsDomainProvider.objects.filter(id=ssl_obj.provider_id).first()
if provider:
provider.model_apply_cert(new_domains)
new_ssl = DnsDomainSSL.objects.filter(dns=new_domains).first()
if new_ssl:
# deploy and update user_for
new_ssl.deploy_sites(new_domains)
except Exception as e:
import traceback
public.print_log(f"Sites Init Cert new apply error: {e}")
public.print_log(traceback.format_exc())
return
else:
# suitable
try:
# deploy and append user_for
ssl_obj.deploy_sites(new_domains)
except Exception as e:
import traceback
public.print_log(f"Sites Init Cert deploy sites error: {e}")
public.print_log(traceback.format_exc())
return
def init_dns_process(dns_info: dict):
"""
api添加后, 初始化厂商
"""
make_suer_renew_task()
SyncService(dns_info.get("id")).main(force=True) # sync base info
dns_obj = DnsDomainProvider.objects.filter(id=dns_info.get("id")).first() # reload
if not dns_obj:
return
if not dns_obj._before_save():
return
ready_init_apply: List[list] = [[f"*.{x}", x] for x in dns_obj.domains]
for domain_list in ready_init_apply:
ssl_obj = DnsDomainSSL.objects.filter(dns=domain_list).first()
if not ssl_obj:
dns_obj.model_apply_cert()
else:
# update ssl link to new provider
ssl_obj.provider_id = dns_obj.id
ssl_obj.save()
def make_suer_org_ssl():
"""
确保合法的账号下默认泛域名证书存在, 6小时间隔
"""
org_domain_ssl_pl = os.path.join(
public.get_panel_path(), "class_v2/ssl_domainModelV2/org_domain_ssl.pl"
)
if not os.path.exists(org_domain_ssl_pl):
public.writeFile(org_domain_ssl_pl, str(round(time.time())), "w")
last_time = public.readFile(org_domain_ssl_pl)
if not last_time:
public.writeFile(org_domain_ssl_pl, str(round(time.time())), "w")
# every 6 hours
if last_time and int(last_time) + 3600 * 6 < time.time():
for provider in DnsDomainProvider.objects.filter(status=1):
# all domains
for domain in provider.domains:
# org ssl exists?
if DnsDomainSSL.objects.filter(
Q(dns=[f"*.{domain}", domain]) | Q(dns=[domain, f"*.{domain}"])
).count() == 0:
try:
provider.model_apply_cert(domains=[domain], auto_wildcard=True)
except Exception as e:
public.print_log(f"make sure org ssl error : {e}")
continue
public.writeFile(org_domain_ssl_pl, str(round(time.time())), "w")
def make_suer_alarm_task() -> Tuple[bool, str]:
"""
确保告警推送服务存在, 适用于开关的地方
"""
# check current task
task = TaskConfig().get_by_keyword(source="SSL", keyword="all")
if task:
return True, "SSL Task Exists"
# check sender exists
senders_id = None
sender_list = SenderManager.get_sender_list(get=None)
if sender_list.get("status") == 0:
senders_id = [
x.get("id") for x in sender_list.get("message", []) if x.get("sender_type") != "sms"
]
if not senders_id:
return False, "please go to Alarm Settings add some senders"
# add new task
get = public.dict_obj()
get.template_id = "1"
get.task_data = json.dumps({
"task_data": {
"tid": "1",
"type": "ssl",
"title": "Certificate (SSL) expiration",
"status": True,
"count": 0,
"interval": 600,
"project": "all",
"cycle": 15,
},
"sender": senders_id,
"number_rule": {"day_num": 2, "total": 30},
"time_rule": {"send_interval": 600, "time_range": [0, 86399]}
})
res = PushManager().set_task_conf(get)
if res.get("status") == 0:
return True, "SSL Task Created"
else:
return False, res.get("msg", "create ssl task error")
def make_suer_renew_task() -> Tuple[bool, str]:
"""
确保续签服务存在, 适用于开关的地方
"""
acme_v2().set_crond_v2()
return True, "Renew task created"
class SyncService(object):
"""
同步域名,记录
"""
dns_sync_log = os.path.join(public.get_panel_path(), "logs/dns_sync.log")
if not os.path.exists(dns_sync_log):
public.writeFile(dns_sync_log, "")
def __init__(self, target_id: int = None):
self.obj = None
self.target_id = target_id
if not os.path.exists(RUNNING):
public.writeFile(RUNNING, "0", "w")
def write_log(self, body, mode="a"):
body += "\n"
with open(self.dns_sync_log, mode) as f:
f.write(body)
def get_lock(self) -> bool:
running = public.readFile(RUNNING)
if running == "1":
return True
else:
return False
def _change_lock(self, body: str = "0"):
public.writeFile(RUNNING, body, "w")
def process(self):
make_suer_renew_task()
self.sync_dns_domains() # 同步域名, 记录
self.write_log("SyncService Done!")
def generate_auth_config(self, obj: DnsDomainProvider, auth: dict) -> dict:
if obj.name == "CloudFlareDns":
limit = True if obj.permission == "limit" else False
config = {**auth, "limit": limit}
else:
config = auth
return config
def records_process(
self, provider_obj: DnsDomainProvider,
all_domains: list,
task_obj: DnsDomainTask = None
):
if not provider_obj or not provider_obj.dns_obj or not all_domains:
return
# 同步每个域名底下的记录值
for domain in all_domains:
if task_obj:
self.write_log(f"|-- Scanning {provider_obj.name} [{domain}] records...")
# 清理数据
DnsDomainRecord.objects.filter(provider_id=provider_obj.id, domain=domain).delete()
res = provider_obj.dns_obj.get_dns_record(domain)
if not res:
if task_obj:
self.write_log("|-- Not found records, skip...")
continue
try:
res = [
{
"provider_id": provider_obj.id,
"provider_name": provider_obj.name,
"api_user": provider_obj.api_user,
"domain": domain,
"record": r.get("record", ""),
"record_type": r.get("record_type", ""),
"record_value": r.get("record_value", ""),
"ttl": r.get("ttl", 1),
"proxy": r.get("proxy", -1),
} for r in res
]
res.sort(key=lambda x: x["record_type"])
DnsDomainRecord.objects.insert_many(res)
except Exception as e:
public.print_log(f"DnsDomainRecord insert error {e}")
continue
if task_obj:
task_obj.task_transfer(add=round(100 / len(all_domains)))
self.write_log(f"|-- [{domain}] Records Update Successfully")
def sync_dns_domains(self):
# 获取当前时间转字符
from datetime import datetime
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.write_log(f"\n=== SyncService Start at {now} ===\n"
f"please wait...if task do not start all the time please restart the panel")
if not self.target_id:
targets = DnsDomainProvider.objects.filter(status=1)
else:
targets = DnsDomainProvider.objects.filter(id=self.target_id)
for i in targets:
self.write_log("=" * 68)
self.write_log(f"| {i.name} {i.api_user}")
auth = {"api_user": i.api_user, "api_key": i.api_key}
dns_obj = DNS_MAP.get(i.name)
if not dns_obj:
self.write_log(f"|- not support {i.name}")
continue
dns_obj = dns_obj(
**self.generate_auth_config(i, auth)
)
task_obj = i.create_task(
{"task_name": DnsTask.sync_dns.value, "task_log": self.dns_sync_log}
)
try:
time.sleep(1)
all_domains = dns_obj.get_domains()
if not all_domains:
task_obj.task_done("Verify Fail or Not Found domains, skip...")
self.write_log(f"|-- Verify Fail or Not Found domains, skip...")
continue
self.write_log(f"|-- Domains: {', '.join(all_domains)}")
DnsDomainProvider.objects.filter(id=i.id).update(domains=all_domains)
task_obj.task_transfer(set_status=30)
self.write_log(f"|-- {i.name} Update Domains Successfully")
# 更新域名下的所有记录
self.records_process(i, all_domains, task_obj)
task_obj.task_done()
except Exception:
if task_obj:
task_obj.task_done(f"{i.name} {i.api_user} verify error, skip...")
self.write_log(f"{i.name} {i.api_user} verify error, skip...")
public.print_log(f"{i.name} {i.api_user} verify error, skip...")
continue
def main(self, force: bool = False):
if force:
self.process()
else:
# running lock
self._change_lock("1")
self.process()
self._change_lock("0")
class DomainValid:
"""
简单判断合法可适用的域名
"""
@staticmethod
def is_valid_domain(domain: str) -> bool:
"""
验证域名的格式标准规范
1. 总长度不超过253个字符
2. 至少包含两个标签(即至少一个点分隔)
3. 每个标签长度1~63字符
4. 标签由字母/数字/连字符组成
5. 标签不以连字符开头或结尾
6. 顶级域名(最后一个标签)非纯数字且长度≥2
"""
# 基础检查
if not domain or len(domain) > 253:
return False
# 分割标签并验证数量
labels = domain.split(".")
if len(labels) < 2:
return False
# 正则表达式验证每个标签, 不管中文
label_pattern = r"^[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$"
for label in labels:
if not re.match(label_pattern, label):
return False
# 验证顶级域名
tld = labels[-1]
if len(tld) < 2 or tld.isdigit():
return False
return True
@staticmethod
def domain_valid(ssl_obj: DnsDomainSSL, new_domain: str) -> bool:
"""
校验同域,合法
"""
for domain in list(set(ssl_obj.dns + [ssl_obj.subject])):
if DomainValid.is_valid_domain(new_domain):
valid_domain_root, _, _ = extract_zone(domain)
new_domain_root, _, _ = extract_zone(new_domain)
if valid_domain_root == new_domain_root:
return True
return False
@staticmethod
def match_ssl_dns(domain: str, valid_dns: str):
"""
校验证书适用
"""
if domain in valid_dns: # 是否精确匹配列表中的某个
return True
for dns in valid_dns:
if dns.startswith("*."):
base_domain = dns[2:]
if domain.endswith(base_domain):
sub_domain = domain[:-len(base_domain)]
sub_domain = sub_domain.rstrip('.')
# 是否仍然包含"."
if "." not in sub_domain and sub_domain:
return True
return False
if __name__ == "__main__":
SyncService().main()