mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-09-06 09:37:39 +02:00
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
417 lines
15 KiB
Python
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()
|