# coding: utf-8 # ------------------------------ # 域名管理 # ------------------------------ import ipaddress import json import os.path import shutil import sys import threading import time from datetime import datetime from typing import Tuple, Dict if not "class/" in sys.path: sys.path.insert(0, "class/") if not "class_v2/" in sys.path: sys.path.insert(0, "class_v2/") import public try: import dns.resolver except ImportError: try: public.ExecShell("btpip install dnspython") import dns.resolver except ImportError: public.print_log("install dnspython fail.") from acme_v2 import acme_v2 from config_v2 import config from panelDnsapi import extract_zone from panel_site_v2 import panelSite from public.aaModel import Q from public.exceptions import HintException from public.validate import Param from .config import ( DNS_MAP, WorkFor, UserFor, PANEL_DOMAIN, PANEL_LIMIT_DOMAIN, MANUAL_APPLY_PL, ) from .model import ( DnsDomainProvider, DnsDomainRecord, DnsDomainSSL, DnsDomainTask, ) from .service import ( DomainValid, CertHandler, ) def fix_log(ssl: DnsDomainSSL, log: str) -> None: if not ssl or not log: return if not ssl.log or ssl.log != log: ssl.log = log ssl.save() # noinspection PyUnusedLocal class DomainObject: date_format = "%Y-%m-%d" vhost = os.path.join(public.get_panel_path(), "vhost") mail_db_file = "/www/vmail/postfixadmin.db" manual_apply = MANUAL_APPLY_PL deploy_map = { 1: UserFor.sites, 2: UserFor.panel, 3: UserFor.mails, 4: UserFor.account, } def __init__(self): self.supports = list(DNS_MAP.keys()) if not os.path.exists(self.manual_apply): public.writeFile(self.manual_apply, json.dumps({})) @staticmethod def _clear_task_force(): # clear task, 3 hours try: one_hours = 1000 * 60 * 60 * 3 out_time = round(time.time() * 1000) - one_hours DnsDomainTask.objects.filter(create_time__lte=out_time).delete() except Exception: pass @staticmethod def _end_time(data_str: str) -> int: try: if not data_str: return 0 today = datetime.today().date() end_date = datetime.strptime(data_str, DomainObject.date_format).date() return max((end_date - today).days - 1 if today <= end_date else 0, 0) except ValueError: return 0 @staticmethod def _process_key(data: dict, key: list = None) -> dict: if not key: return data if "api_user" in key: # 隐藏api_user k = "api_user" if len(data.get(k, "")) > 0: data[k] = data[k][:len(data[k]) // 2] + "***" if "record" in key: # 隐藏cf自带域名 k = "record" endswith_str = f'.{data.get("domain", "")}' if data.get(k, "").endswith(endswith_str): data[k] = data[k].replace(endswith_str, "") return data @staticmethod def _add_ssl_info(data: dict) -> dict: new_domains = [] domains = data.get("domains", []) for domain in domains: ssl_obj = DnsDomainSSL.objects.filter( provider_id=data.get("id", 0), dns__contains=domain, ).order_by("-create_time").first() if not ssl_obj: ssl_info = { "id": 0, "end_time": -1, "end_date": "-", "alarm": 0, "auto_renew": 0, } else: ssl_info = { "id": ssl_obj.id, "end_time": DomainObject._end_time(ssl_obj.not_after), "end_date": ssl_obj.not_after, "alarm": ssl_obj.alarm, "auto_renew": ssl_obj.auto_renew, } new_domains.append( {"name": domain, "ssl_info": ssl_info} ) data["domains"] = new_domains return data @staticmethod def _add_task_info(data: dict, task_name: str = None) -> dict: # 初始化, 申请任意域名, 续签, 同步 tasks_obj = DnsDomainTask.objects.filter( provider_id=data.get("id", -1), task_status__lt=100 ) if task_name: tasks_obj.filter(task_name=task_name) data["task"] = tasks_obj.order_by("-create_time").as_list() return data def get_dns_support(self, get): res = list(set(self.supports)) if "YakPanelDns" in res: res.remove("YakPanelDns") return public.success_v2(res) # =========== 托管商 =========== def sync_dns_info(self, get): """ 对账号立即同步域名信息 """ from .service import SyncService target_id = get.id if hasattr(get, "id") else None instance = SyncService(target_id) task = threading.Thread( target=instance.process, kwargs=({"apply_new": True}) ) task.start() return public.success_v2(public.lang("success")) def list_dns_api(self, get): """ dns api 列表 """ public.set_module_logs("sys_domain", "Domain_SSL_Open", 1) try: get.validate([ Param("p").Integer(), Param("limit").Integer(), Param("pid").Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) # clear task self._clear_task_force() from .service import check_legal, init_aaDns check_legal() init_aaDns() page = int(getattr(get, "p", 1)) limit = int(getattr(get, "limit", 100)) obj = DnsDomainProvider.objects.all() if hasattr(get, "pid"): obj = DnsDomainProvider.objects.filter(id=get.pid) total = obj.count() result = obj.limit(limit).offset((page - 1) * limit).as_list() for r in result: r = self._process_key(r, key=["api_user"]) r = self._add_ssl_info(r) r = self._add_task_info(r) return public.success_v2({"data": result, "total": total}) def create_dns_api(self, get): try: get.validate([ Param("name").String().Require(), Param("api_user").String().Require(), Param("api_key").String().Require(), Param("permission").String(), Param("alias").String().Require(), Param("status").Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) if "*" in get.api_user: return public.fail_v2(public.lang("'*' Symbols that are not allowed")) if hasattr(get, "status"): get.status = int(get.status) if get.name not in self.supports: return public.fail_v2(public.lang(f"Provider not support! Support DNS provider :{self.supports}")) if get.name == "CloudFlareDns": if not hasattr(get, "permission") or get.permission not in ["limit", "global"]: return public.fail_v2(public.lang("CloudFlareDns Permission must be 'limit' or 'global'!")) else: get.permission = "-" if DnsDomainProvider.objects.filter(alias=get.alias).first(): return public.fail_v2(public.lang("Alias already exists!")) if DnsDomainProvider.objects.filter( api_user=get.api_user, api_key=get.api_key, name=get.name, ).first(): return public.fail_v2(public.lang(f"Account already exists!")) try: dns = DnsDomainProvider(**get.get_items()) if not dns.is_pro(): return public.fail_v2(public.lang("Please Upgrade PRO Version!")) dns.dns_obj.verify() dns.save() dns.init_ssl_myself_thread() public.set_module_logs("sys_domain", "Add_Dns_Api", 1) return public.success_v2(public.lang("Save Successfully!")) except Exception as ex: raise ex def delete_dns_api(self, get): try: get.validate([ Param("id").Integer().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) DnsDomainRecord.objects.filter(provider_id=int(get.id)).delete() provider = DnsDomainProvider.objects.filter(id=int(get.id)).first() msg = f"DNS: {provider.name} Alias: {provider.alias} , Delete Successfully!" provider.delete() public.WriteLog("DnsSSLManager", msg) return public.success_v2(public.lang("Delete Successfully!")) def edit_dns_api(self, get): if not hasattr(get, "id"): return public.fail_v2(public.lang("id is required")) if hasattr(get, "status"): get.status = int(get.status) if hasattr(get, "name") and get.name == "CloudFlareDns": if not hasattr(get, "permission") or get.permission not in ["limit", "global"]: return public.fail_v2(public.lang("CloudFlareDns Permission must be 'limit' or 'global'")) if hasattr(get, "user") and "*" in get.api_user: return public.fail_v2(public.lang("'*' symbols that are not allowed")) # alias 不允许重复 if hasattr(get, "alias") and DnsDomainProvider.objects.filter(alias=get.alias).first(): return public.fail_v2(public.lang("Alias already exists!")) try: dns = DnsDomainProvider.objects.filter(id=get.id).first() for k, v in get.get_items().items(): if hasattr(dns, k) and k != "id": setattr(dns, k, v) # 仅当开启时候校验 if dns.status == 1: dns.dns_obj.verify() if dns.name != "YakPanelDns": DnsDomainProvider.objects.filter(id=get.id).update(dns.as_dict()) else: # 兼容 aaDNS 服务状态变更 from ssl_dnsV2.dns_manager import DnsManager status_map = {1: "restart", 0: "stop"} DnsManager().change_service_status(status=status_map.get(get.status)) except Exception as ex: return public.fail_v2(str(ex)) return public.success_v2(public.lang("Save Successfully!")) # =========== dns 记录 =========== def list_dns_record(self, get): try: get.validate([ Param("p").Integer(), Param("limit").Integer(), Param("search_pid").Integer().Require(), Param("domain").String(), Param("search").String(), Param("search_and").String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) page = int(getattr(get, "p", 1)) limit = int(getattr(get, "limit", 100)) pid = int(get.search_pid) provider = DnsDomainProvider.objects.find_one(id=pid) if not provider: return public.fail_v2(public.lang("Provider not found!")) if hasattr(get, "domain") and get.domain: domain_name = get.domain else: domain_name = provider.domains[0] if provider.domains else "" from .service import RecordCache RecordCache.record_ensure(domain_name) # search_and if hasattr(get, "search_and"): try: search_and = json.loads(get.search_and) except: search_and = {} obj = DnsDomainRecord.objects.filter( provider_id=pid, domain=domain_name, **search_and ) # no search, no search_and elif not hasattr(get, "search"): obj = DnsDomainRecord.objects.filter(provider_id=pid, domain=domain_name) # only search else: if hasattr(get, "search_and") and hasattr(get, "search"): return public.fail_v2( public.lang("search_and and search can not be used at the same time") ) obj = DnsDomainRecord.objects.filter( Q(provider_id=pid, domain=domain_name) & ( Q(record__like=get.search) | Q(record_value__like=get.search) ) ) try: total = obj.count() result = obj.limit(limit).offset((page - 1) * limit).as_list() except Exception as e: raise HintException(e) data = [ self._process_key(x, key=["api_user", "record"]) for x in result ] data.sort(key=lambda x: (x["record_type"], x["record"])) return public.success_v2({"data": data, "total": total}) def create_dns_record(self, get): try: get.validate([ Param("pid").Integer().Require(), Param("domain").String().Require(), Param("record").String().Require(), Param("record_type").String().Require(), Param("record_value").String().Require(), Param("priority").Integer().Require(), Param("ttl").Integer().Require(), Param("proxy").Integer(), Param("ps").String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) provider = DnsDomainProvider.objects.filter(id=int(get.pid)).first() body = { "provider_id": provider.id, "provider_name": provider.name, "api_user": provider.api_user, "domain": get.domain, "record": get.record, "record_type": get.record_type, "record_value": get.record_value, "ttl": int(get.ttl), "proxy": int(get.proxy), "priority": int(get.priority), } if hasattr(get, "ps"): body["ps"] = get.ps response = provider.model_create_dns_record(body) if not response.get("status"): return public.fail_v2(response.get("msg")) return public.success_v2(public.lang("Save Successfully!")) def delete_dns_record(self, get): try: get.validate([ Param("id").Integer().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) record = DnsDomainRecord.objects.find_one(id=int(get.id)) if not record: return public.fail_v2(public.lang("DNS record Not Found!")) provider = DnsDomainProvider.objects.find_one(id=record.provider_id) if not provider: return public.fail_v2(public.lang("DNS Provider Not Found!")) response = provider.model_delete_dns_record(int(get.id)) if not response.get("status"): return public.fail_v2(response.get("msg")) return public.success_v2(public.lang("Delete Successfully!")) def edit_dns_record(self, get): try: get.validate([ Param("id").Integer().Require(), Param("pid").Integer().Require(), Param("domain").String().Require(), Param("record").String().Require(), Param("record_type").String().Require(), Param("record_value").String().Require(), Param("ttl").Integer().Require(), Param("proxy").Integer().Require(), # -1 is not MX record Param("priority").Integer("between", [-1, 65535]).Require(), Param("ps").String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) pid = int(get.pid) record_id = int(get.id) ps = get.ps if hasattr(get, "ps") else "" # check if change real_change = False provider = DnsDomainProvider.objects.find_one(id=pid) if not provider: return public.fail_v2(public.lang("DNS Provider Not Found!")) target = DnsDomainRecord.objects.find_one(id=record_id) if not target: return public.fail_v2(public.lang("DNS Record Not Found!")) new_body = { "provider_id": provider.id, "provider_name": provider.name, "api_user": provider.api_user, "domain": get.domain, "record": get.record, "record_type": get.record_type, "record_value": get.record_value, "ttl": int(get.ttl), "proxy": int(get.proxy), "priority": int(get.priority), "ps": ps, } if any([ target.record != get.record, target.record_type != get.record_type, target.record_value != get.record_value, target.ttl != int(get.ttl), target.proxy != int(get.proxy), target.priority != int(get.priority), ]): real_change = True if not real_change: DnsDomainRecord.objects.filter(id=record_id).update(new_body) return public.success_v2(public.lang("Update Successfully!")) # real change try: update = provider.model_edit_dns_record(record_id, new_body) if update.get("status"): return public.success_v2(public.lang("Update Successfully!")) else: return public.fail_v2(update.get("msg", "Update Failed...")) except Exception as ex: return public.fail_v2(str(ex)) # ========== 域名管理概况 SSL ======= def list_domain_details(self, get): try: get.validate([ Param("p").Integer(), Param("limit").Integer(), Param("id").Integer().Require(), Param("domain").String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) page = int(getattr(get, "p", 1)) limit = int(getattr(get, "limit", 100)) provider = DnsDomainProvider.objects.find_one(id=get.id) if not provider: return public.fail_v2(public.lang("DNS Provider Not Found!")) data = self._add_ssl_info(provider.as_dict()) data = data.get("domains", []) if hasattr(get, "domain"): # filter domain data = [x for x in data if get.domain in x.get("name", "")] bulitin = True if provider.name == "YakPanelDns" else False for d in data: d["records"] = DnsDomainRecord.objects.filter( domain=d.get("name", ""), provider_id=int(get.id) ).count() if bulitin: from ssl_dnsV2.model import DnsResolve resolve = DnsResolve.objects.filter(domain=d.get("name", "")).fields( "ns_resolve", "a_resolve", "tips" ).first() if not resolve: d["dns_resolve"] = {"ns_resolve": 0, "a_resolve": 0, "tips": "Not Found Msg"} else: d["dns_resolve"] = resolve.as_dict() total = len(data) start = (page - 1) * limit end = start + limit return public.success_v2({"data": data[start:end], "total": total}) # =========== SSL ============ def _get_alarm_status(self) -> bool: # 校验alarm task_path = f"{public.get_panel_path()}/data/mod_push_data/task.json" if not os.path.exists(task_path): return False task = public.readFile(task_path) if not task: return False try: task_info = json.loads(task) except: task_info = [] for t in task_info: if t.get("task_data", {}).get("type") == "ssl": return True return False def _get_verify(self, ssl: DnsDomainSSL) -> str: if not ssl.auth_info: return "dns01_manual" auth_type = ssl.auth_info.get("auth_type", "") auth_to = ssl.auth_info.get("auth_to", "").rstrip("/").replace("//", "/") if auth_type == "http": return "http01" elif auth_type == "dns": try: if not "|" in auth_to: return "dns01_manual" info = auth_to.split("|") if len(info) == 3 and info[0]: return "dns01" else: return "dns01_manual" except: return "dns01_manual" elif DomainValid.is_ip(ssl.dns): if ssl.info.get("issuer_O") == "Let's Encrypt": return "http01" elif auth_to == "" or any("*" in d for d in ssl.dns): return "dns01_manual" return "" def list_ssl_info(self, get): """ 证书列表 """ try: get.validate([ Param("p").Integer(), Param("limit").String(), Param("is_order").Integer(), Param("search").String(), ], [ public.validate.trim_filter(), ]) get.is_order = 0 if not hasattr(get, "is_order") else int(get.is_order) if not hasattr(get, "search"): get.search = "" except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) alarm_status = self._get_alarm_status() page = int(getattr(get, "p", 1)) limit = int(getattr(get, "limit", 100)) ssl_obj = DnsDomainSSL.objects.filter( Q(is_order=int(get.is_order)) & (Q(dns__like=get.search) | Q(subject__like=get.search)) ).order_by("-create_time") total = ssl_obj.count() ssl_obj.limit(limit).offset((page - 1) * limit) data = [ self._add_task_info( data={ "hash": ssl.hash, "provider": ssl.info.get("issuer_O", "unknown"), "issuer": ssl.info.get("issuer", "unknown"), "verify_domains": ssl.dns, "end_time": self._end_time(ssl.not_after), "end_date": ssl.not_after, "auto_renew": ssl.auto_renew, "last_apply_time": ssl.info.get("notBefore", ""), "cert": { "csr": public.readFile(ssl.path + "/fullchain.pem"), # 证书 "key": public.readFile(ssl.path + "/privkey.pem"), # 密钥 }, "log": ssl.log if ssl.log else ssl.get_ssl_log(), "user_for": ssl.user_for, "alarm": ssl.alarm if alarm_status else 0, "verify": self._get_verify(ssl) }, ) for ssl in ssl_obj ] return public.success_v2({"data": data, "total": total}) def download_cert(self, get): """ 下载 """ try: get.validate([ Param("hash").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) file_path = "/www/server/panel/vhost" + f"/ssl_saved/{get.hash}" if not os.path.exists(file_path): return public.fail_v2(public.lang("SSL Certificate Not Found!")) CertHandler.make_last_info(file_path, force=True) download_path = os.path.join(self.vhost, "ssl_saved/download") os.makedirs(download_path, exist_ok=True) output_path = os.path.join(download_path, f"{get.hash}") if os.path.exists(f"{output_path}.zip"): public.ExecShell(f"rm -f {output_path}.zip") try: shutil.make_archive(output_path, "zip", file_path) except Exception as e: return public.fail_v2(f"error: {str(e)}") return public.success_v2(f"{output_path}.zip") def one_cilck_renew(self, get): """ 一键全量续签 """ from .service import make_suer_renew_task make_suer_renew_task() # 确保任务存在 echo = public.md5(public.md5("domain_ssl_renew_lets_ssl_bt")) task = public.S("crontab").where("echo=?", echo).find() if not task: raise HintException("Cron Domian Renew task not found, please try again later!") execstr = f"{public.GetConfigValue("setup_path")}/cron/{echo}" public.ExecShell(f"chmod +x {execstr}") public.ExecShell(f"nohup {execstr} start >> {execstr}.log 2>&1 &") return public.success_v2({"id": task["id"]}) def renew_cert_process(self, get): """ 续签, 带进度, 带部署 """ try: get.validate([ Param("hash").String().Require(), ], [ public.validate.trim_filter() ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) invalid_msg = public.lang( "This Certificate auth info is invalid, cannot renew! Please Apply for a New Certificate." "It will be renewed for you automatically in the future" ) ssl_obj = DnsDomainSSL.objects.filter(hash=get.hash).first() if not ssl_obj: raise HintException(public.lang("SSL Certificate Not Found!")) is_ip_ssl = DomainValid.is_ip(ssl_obj.dns) if not ssl_obj.auth_info and not is_ip_ssl: ssl_obj.renew_status = 0 ssl_obj.save() raise HintException(invalid_msg) day = 3 if is_ip_ssl else 30 ts_month = day * 24 * 60 * 60 * 1000 months = int(time.time() * 1000) + ts_month debug = public.readFile("/www/server/panel/data/debug.pl") or "False" if debug.lower() == "false" and ssl_obj.not_after_ts > months: raise HintException(public.lang(f"SSL Certificate is less than {day} days, no need to renew!")) auth_type = ssl_obj.auth_info.get("auth_type") auth_to = ssl_obj.auth_info.get("auth_to", "").rstrip("/").replace("//", "/") args = public.dict_obj() args.domains = json.dumps(ssl_obj.dns) if auth_type == "http" or (auth_type == "dns" and auth_to == "dns"): if auth_to: # 有 site # 明确的http方式, 或 手动认证申请的dns, 尝试http args.auth_type = "http" args.deploy = 1 args.site_id = public.M("sites").where("path=?", (auth_to,)).getField("id") or "-1" return self.apply_new_ssl(args) else: # todo 新场景, 无 site 续签 ip ssl, 暂时仅支持面板ssl if not is_ip_ssl or not ssl_obj.panel_uf == ["panel"]: raise HintException("only panel ip ssl support http01 renew now!") if len(ssl_obj.dns) > 1: raise HintException("only single ip ssl support http01 renew now!") from .service import init_panel_http, generate_panel_task task_obj = generate_panel_task({"domain": ssl_obj.dns[0]}) http_task = threading.Thread( target=init_panel_http, args=(ssl_obj.dns[0], task_obj, True) ) http_task.start() return public.success_v2({ "result": public.lang("Apply Successfully! please wait for a moment"), "task_id": task_obj.id, "path": task_obj.task_log, }) elif auth_type == "dns": args.auth_type = "dns" return self.apply_new_ssl(args) else: raise HintException(invalid_msg) def manual_apply_vaild(self, get): """ 手动申请验证 """ try: get.validate([ Param("site_id").Integer().Require(), Param("domains").String().Require(), ], [ public.validate.trim_filter(), ]) get.site_id = str(get.site_id) domains = [x.strip() for x in list(set(get.domains.split(",")))] domains.sort() except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) manual_apply: Dict[str, str] = json.loads( public.readFile(self.manual_apply) ) domians_index = CertHandler.original_md5(domains) if domians_index not in manual_apply.keys(): return public.fail_v2(public.lang("Order Not Found!")) new_get = public.dict_obj() new_get.index = manual_apply[domians_index] vaild = acme_v2().validate_domain(new_get) if vaild.get("save_path"): ssl_hash = CertHandler.get_hash(vaild.get("cert", "") + vaild.get("root", "")) ssl_obj = DnsDomainSSL.objects.filter(hash=ssl_hash).first() site_name = public.M("sites").where("id=?", get.site_id).getField("name") if site_name and ssl_obj: ssl_obj.deploy_sites([site_name]) if domians_index in manual_apply.keys(): del manual_apply[domians_index] public.writeFile(self.manual_apply, json.dumps(manual_apply)) return public.success_v2(public.lang("Apply Successfully!")) if vaild.get("status") is True: if domians_index in manual_apply.keys(): del manual_apply[domians_index] public.writeFile(self.manual_apply, json.dumps(manual_apply)) return public.success_v2(public.lang("Apply Successfully!")) try: return public.fail_v2((str(vaild.get("msg", "Vaild failed! please try again")))) except: return public.fail_v2("Vaild failed! please try again") def manual_apply_check(self, get): """ 手动申请验证兜底 """ try: get.validate([ Param("site_id").Integer().Require(), Param("domains").Array().Filter(json.loads) ], [ public.validate.trim_filter(), ]) domains = [x.strip() for x in list(set(get.domains))] domains.sort() target = CertHandler.original_md5(domains) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) manual_apply: Dict[str, str] = json.loads( public.readFile(self.manual_apply) ) target_detail = {} for k in list(manual_apply.keys()): new_get = public.dict_obj() new_get.index = manual_apply[k] detail = acme_v2().get_order_detail(new_get) # loop for clean expires if target == k: target_detail = detail.get("message") return public.success_v2(target_detail) def apply_new_ssl(self, get): """ 申请证书 get.domains = '["www.a.com","a.com"]' get.auth_type = dns/http/dns_manual get.auto_wildcard = 0/1 # 自动组合泛域名 get.deploy = -1/0/ 是否需要重新部署ssl get.site_id = 站点id, 匹配具体的站点 :return: 1. dns 自动验证 2. http 文件验证 { "result": success_msg, "task_id": task.id, "path": http_task.task_log, } 3. dns_manual 手动验证 返回订单详情 dict , 或者免认证期间 直接下发ssl成功消息 str """ try: get.validate([ Param("domains").String().Require(), Param("auth_type").String().Require(), Param("auto_wildcard").Integer(), Param("deploy").Integer(), Param("site_id").Integer(), ], [ public.validate.trim_filter(), ]) get.domains = json.loads(get.domains) if get.auth_type not in ["dns", "http", "dns_manual"]: return public.fail_v2(public.lang("auth_type must be 'dns', 'http' or 'dns_manual'")) if len(get.domains) == 0: return public.fail_v2(public.lang("domains is empty")) get.deploy = getattr(get, "deploy", -1) get.site_id = int(getattr(get, "site_id", -1)) auto_wildcard = False if int(getattr(get, "auto_wildcard", 0)) == 0 else True except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) success_msg = "Apply Successfully! please wait for a moment" deploy_flag = self.deploy_map.get(int(get.deploy)) apply_domains = [x.strip() for x in list(set(get.domains))] apply_domains.sort() if len(apply_domains) == 0: return public.fail_v2(public.lang("Domains Error: domains is empty")) from .model import apply_cert from .service import generate_sites_task if get.auth_type == "dns": # auto dns verify provider = None for d in apply_domains: temp_root, _, _ = extract_zone(d) provider = DnsDomainProvider.objects.filter( domains__contains=temp_root ).first() if not provider: return public.fail_v2(public.lang( f"Dns01 Verify Errot, DNS Provider Not Found! for {temp_root}" )) dns_task = generate_sites_task({}, get.site_id) threading.Thread(target=provider.model_apply_cert, kwargs=({ "domains": apply_domains, "auto_wildcard": auto_wildcard, "deploy": deploy_flag, "task_obj": dns_task, })).start() return public.success_v2({ "result": public.lang(success_msg), "task_id": dns_task.id, "path": dns_task.task_log, }) elif get.auth_type == "http": # file verify from .service import find_site_with_domain site = find_site_with_domain(get.domains[0]) if not site and not DomainValid.is_ip(get.domains): return public.fail_v2(public.lang("Http01 Verify Error, but Site Not Found.")) if site.get("status") != "1": return public.fail_v2(public.lang("Http01 Verify Error, but Site is Not Running")) if any("*." in i for i in get.domains): return public.fail_v2(public.lang( "Http01 Verify Error: The wildcard domain name can only be verified by DNS." )) http_task = generate_sites_task({}, get.site_id) threading.Thread(target=apply_cert, kwargs=({ "domains": apply_domains, "auth_to": site.get("path"), "auth_type": "http", "task_obj": http_task, "deploy": deploy_flag, })).start() return public.success_v2({ "result": public.lang(success_msg), "task_id": http_task.id, "path": http_task.task_log, }) elif get.auth_type == "dns_manual": # manual dns verify manual_apply: Dict[str, str] = json.loads( public.readFile(self.manual_apply) ) domian_index = CertHandler.original_md5(apply_domains) if domian_index in manual_apply.keys(): new_get = public.dict_obj() new_get.index = manual_apply[domian_index] return acme_v2().get_order_detail(new_get) apply_res = acme_v2().apply_cert_domain( domains=apply_domains, auth_to="dns", auth_type="dns", auto_wildcard=auto_wildcard, ) if apply_res.get("save_path"): # 免认证期间将跳过验证, 直接下发ssl, 进行部署覆盖 ssl_hash = CertHandler.get_hash(apply_res.get("cert", "") + apply_res.get("root", "")) ssl_obj = DnsDomainSSL.objects.filter(hash=ssl_hash).first() site_name = public.M("sites").where("id=?", get.site_id).getField("name") deploy = {} if site_name and ssl_obj: deploy = ssl_obj.deploy_sites([site_name]) if domian_index in manual_apply.keys(): del manual_apply[domian_index] public.writeFile(self.manual_apply, json.dumps(manual_apply)) success_msg = {"result": apply_res.get("msg", "Apply Successfully!")} if deploy.get("status"): success_msg.update({"deploy": deploy.get("status")}) return public.success_v2(public.lang(success_msg)) if apply_res.get("index"): # 新申请, 返回订单详情 manual_apply.update({domian_index: apply_res.get("index")}) public.writeFile(self.manual_apply, json.dumps(manual_apply)) new_get = public.dict_obj() new_get.index = apply_res.get("index") return acme_v2().get_order_detail(new_get) if apply_res.get("status") is False: return public.fail_v2(apply_res.get("msg", "Apply Failed!")) return public.success_v2(public.lang("Apply Successfully!")) return public.fail_v2(public.lang("Unknown Auth Type")) def upload_cert(self, get): """ 上传证书 """ try: get.validate([ Param("key").String().Require(), Param("cert").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) try: from ssl_domainModelV2.service import CertHandler data = CertHandler().save_by_data(get.cert, get.key) if not data: return public.fail_v2(public.lang("update cert failed")) return public.success_v2(data) except Exception as ex: return public.fail_v2(str(ex)) def remove_cert(self, get): """ 删除证书 """ try: get.validate([ Param("hash").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) ssl_obj = DnsDomainSSL.objects.find_one(hash=get.hash) if not ssl_obj: return public.fail_v2(public.lang("SSL Certificate Not Found!")) # vhost/ssl为site ssl证书存放目录 cert_name = ssl_obj.subject.replace("*.", "") vpath = os.path.join(self.vhost, "ssl", cert_name) if os.path.exists(vpath): public.ExecShell("rm -rf " + vpath) try: ssl_obj.delete() except Exception as ex: public.print_log("error info: {}".format(ex)) return public.fail_v2(str(ex)) return public.success_v2(public.lang("Remove Successfully!")) def switch_auto_renew(self, get): """ 自动续签开关 """ try: get.validate([ Param("hash").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) ssl_obj = DnsDomainSSL.objects.find_one(hash=get.hash) if not ssl_obj: return public.fail_v2(public.lang("SSL Not Found!")) # make_suer_renew_task() # make suer corn task open_map = {0: 1, 1: 0} ssl_obj.auto_renew = open_map.get(ssl_obj.auto_renew, 1) ssl_obj.save() return public.success_v2(public.lang("Setting Successfully!")) def switch_ssl_alarm(self, get): """ 证书告警开关 """ try: get.validate([ Param("hash").String().Require(), Param("alarm").Integer().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) ssl_obj = DnsDomainSSL.objects.find_one(hash=get.hash) if not ssl_obj: return public.fail_v2(public.lang("SSL Not Found!")) alarm = int(get.alarm) if alarm == 1: # open from .service import make_suer_alarm_task make_suer_alarm_task() # make suer alarm task ssl_obj.alarm = alarm ssl_obj.save() else: # close ssl_obj.alarm = alarm ssl_obj.save() return public.success_v2(public.lang("Setting Successfully!")) # =========== Deploy ================ @staticmethod def __add_match_flag(targes: list, dns_obj: DnsDomainSSL, key_word: str = None, match_domain: str = None) -> list: if key_word and match_domain: # 不能同时存在 return targes for t in targes: temp_name = t.get(key_word, "") if key_word else match_domain if DomainValid.match_ssl_dns(temp_name, dns_obj): t["match"] = 1 return targes def cert_domain_list(self, get): """ 证书管理列表 """ try: get.validate([ Param("hash").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) ssl_obj = DnsDomainSSL.objects.find_one(hash=get.hash) if not ssl_obj: return public.fail_v2(public.lang("SSL Certificate Not Found!")) # ====================== sites ====================== # 不考虑status sites = public.S("sites").field( "id", "name", "path", "status", "type_id", "project_type", ).select() for s in sites: domains = public.S("domain").where("pid=?", (s["id"],)).field("name").select() if not domains: continue if all([ DomainValid.match_ssl_dns(d.get("name", ""), ssl_obj) for d in domains if d.get("name") ]): s["match"] = 1 # ====================== mails ====================== mails = [] if os.path.exists(self.mail_db_file): # 不考虑active mails = public.S("domain", self.mail_db_file).field( "domain", "a_record", "active" ).select() mails = self.__add_match_flag(mails, ssl_obj, "domain") # ====================== panel ====================== panel = [] panel_ssl = config().GetPanelSSL(get=None) if panel_ssl.get("status") == 0: panel = [panel_ssl.get("message")] current_domain = "" if os.path.exists(PANEL_LIMIT_DOMAIN): limit_domain = public.readFile(PANEL_LIMIT_DOMAIN) if limit_domain: # 如果有限制域名 current_domain = limit_domain else: if os.path.exists(PANEL_DOMAIN): # 如果有配置过的域名 current_domain = public.readFile(PANEL_DOMAIN) if current_domain and panel: panel = self.__add_match_flag(panel, ssl_obj, None, current_domain) data = { "sites": sites, "mails": mails, "panel": panel, "accounts": [], } return public.success_v2(data) @staticmethod def __before_deploy(get: public.dict_obj) -> Tuple[DnsDomainSSL, public.dict_obj]: """ 证书部署前通用检查 :return: ssl obj, get obj """ try: get.validate([ Param("hash").String().Require(), Param("domains").String(), Param("append").Integer(), Param("recover").Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: raise ex if hasattr(get, "domains"): try: get.domains = json.loads(get.domains) except json.decoder.JSONDecodeError as js_err: raise HintException("json decode error: {}".format(str(js_err))) for k in ["recover", "append"]: if hasattr(get, k): try: setattr(get, k, int(getattr(get, k))) except TypeError as tr: raise tr ssl_obj = DnsDomainSSL.objects.find_one(hash=get.hash) if not ssl_obj: raise Exception("SSL Certificate Not Found!") return ssl_obj, get def cert_deploy_sites(self, get): """ 证书部署到 选定的 sites get.hash get.domain (site name) get.append=1 追加模式, 其余为替换模式 """ try: # 返回ssl对象, get对象 ssl_obj, get = self.__before_deploy(get) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.fail_v2(str(ex)) # 网站入口单独部署 if hasattr(get, "append") and get.append == 1: result = ssl_obj.deploy_sites(site_names=get.domains) else: # 域名管理批量处理 # 不同的元素 diff = list(set(ssl_obj.sites_uf).symmetric_difference(set(get.domains))) # 1, 需要移除的sites for remove in [x for x in diff if x in set(ssl_obj.sites_uf)]: new_get = public.dict_obj() new_get.siteName = remove new_get.updateOf = 1 new_get.reload = 0 try: # close site's ssl conf remove_res = panelSite().CloseSSLConf(new_get) except Exception as e: public.print_log(f"remove site ssl error info: {str(e)}") continue # 2, 移除diff之后, 部署 result = ssl_obj.deploy_sites(site_names=get.domains, replace=True) return public.return_message(0 if result.get("status") else -1, 0, public.lang(result.get("msg"))) def cert_deploy_mails(self, get): """ 证书部署到 选定的 mails 只涉及替换全部 """ try: ssl_obj, get = self.__before_deploy(get) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.fail_v2(str(ex)) # 不同的元素 diff = list(set(ssl_obj.mails_uf).symmetric_difference(set(get.domains))) # 1, 需要移除的mails try: import sys if os.path.exists("/www/server/panel/plugin/mail_sys"): sys.path.insert(1, "/www/server/panel/plugin/mail_sys") from plugin.mail_sys.mail_sys_main import mail_sys_main for remove in [x for x in diff if x in set(ssl_obj.mails_uf)]: args = public.dict_obj() args.csr = "" args.key = "" args.domain = remove args.act = "delete" try: mail_sys_main().set_mail_certificate_multiple(args) except: continue except Exception as err: public.print_log("remove mail ssl error info: {}".format(str(err))) # 2, 移除diff之后, 部署 result = ssl_obj.deploy_mails(get.domains) if result.get("status"): return public.success_v2(public.lang("Deploy Mail's SSL Successfully!")) return public.fail_v2(public.lang(result.get("msg", "Deploy Faild..."))) def cert_deploy_accounts(self, get): """ 证书部署到 选定的 accounts """ try: ssl_obj, get = self.__before_deploy(get) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.fail_v2(str(ex)) return public.success_v2(public.lang("Deploy Account's SSL Successfully!")) def cert_deploy_panel(self, get): """ 指定部署到 panel 主面板, or 恢复自签证书 """ try: get.validate([ Param("hash").String().Require(), Param("recover").Integer().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.fail_v2(str(ex)) ssl_obj, get = self.__before_deploy(get) res = ssl_obj.deploy_panel(recover=int(get.recover)) if res.get("status"): return public.success_v2(public.lang("Deploy Panel's SSL Successfully!")) return public.fail_v2(public.lang(res.get("msg", "Deploy Panel's SSL Failed!"))) # =========== Site =========== def get_sites(self, get): """ 获取所有网站 """ return public.success_v2([ x for x in public.S("sites").field("id", "name", "project_type").select() ]) def check_domain_automatic(self, get): """ dns自动化的检测服务 涵盖site, mail, panel, account等 :return: { "hash": "", 适用证书 "domain": "", 检查的域名 "support": [ "auto", 自动解析 "ssl_cert",自动部署 "cf_proxy", cf代理 ], } """ try: get.validate([ Param("domain").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) result = { "hash": "", "domain": get.domain, } support = list() root, _, _ = extract_zone(get.domain) if DnsDomainProvider.objects.filter(domains__contains=root).first(): support.append("auto") # 自动解析功能 support.append("ssl_cert") # 自动部署证书 exist = None for ssl in DomainValid.get_best_ssl(get.domain): if DomainValid.match_ssl_dns(get.domain, ssl, False): exist = ssl break if exist is not None: result["hash"] = exist.hash if bool("CloudFlareDns" in exist.auth_info.get("auth_to", "")): # ip为内网地址不可以开启代理 try: local_ip = public.GetLocalIp() provate = ipaddress.IPv4Address(local_ip).is_private except Exception: provate = True if not provate: # 是否支持 CF 代理 support.append("cf_proxy") result["support"] = support return public.success_v2(result) def ssl_tasks_status(self, get): """ 获取任务状态 """ try: get.validate([ Param("task_type").Integer(), Param("task_id").Integer(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) if not hasattr(get, "task_id"): task_type = WorkFor.sites if not hasattr(get, "task_type") else int(get.task_type) data = DnsDomainTask.objects.filter( task_type=task_type, task_status__lt=100, ).as_list() else: objs = DnsDomainTask.objects.filter(id=get.task_id).first() data = objs.as_dict() if objs else "Task Not Found!" return public.success_v2(data) # ========== Panel =========== def get_panel_domain(self, get=None): if not os.path.exists(PANEL_DOMAIN): # 填充已经存在的限制domain if os.path.exists(PANEL_LIMIT_DOMAIN): limit_domain = public.readFile(PANEL_LIMIT_DOMAIN) if limit_domain: public.writeFile(PANEL_DOMAIN, limit_domain, "w") else: public.writeFile(PANEL_DOMAIN, "", "w") return public.success_v2({"domain": limit_domain}) else: public.writeFile(PANEL_DOMAIN, "", "w") return public.success_v2({"domain": ""}) return public.success_v2({"domain": public.readFile(PANEL_DOMAIN)}) def set_panel_domain_ssl(self, get): """ get.domain = { "hash": "xxx", "domain": "example.com", "support": ["auto" (自动解析), "ssl_cert" (自动部署), "cf_proxy" (开启cf代理)], "auth_type": "http" (http验证) or "dns" (dns验证), } """ try: get.validate([ Param("domain").String().Require(), ], [ public.validate.trim_filter(), ]) get.domain = json.loads(get.domain) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) auth_type = get.domain.get("auth_type") domain = get.domain.get("domain", "").strip() if "*." in domain: return public.fail_v2(public.lang("Wildcard domain is not supported!")) if DomainValid.is_ip(domain) and DomainValid.is_private_ip(domain): # 如果是ip, 且不是公网 return public.fail_v2(public.lang("Private IP address is not supported!")) if not DomainValid.is_valid_domain(domain) and not (DomainValid.is_ip(domain)): # 既不是合法域名, 也不是ip return public.fail_v2(public.lang("Domain or IP addr Not Valid!")) get.domain["domain"] = domain if not domain: return public.fail_v2(public.lang("domain is empty")) if auth_type not in ["http", "dns"]: return public.fail_v2(public.lang("auth_type must be 'http' or 'dns'")) # real chage from .service import generate_panel_task task_obj = generate_panel_task(get.domain) if auth_type == "dns": support = get.domain.get("support") if "cf_proxy" in support: support.remove("cf_proxy") get.domain["support"] = support from .service import init_panel_dns dns_task = threading.Thread( target=init_panel_dns, args=(get.domain, task_obj) ) dns_task.start() else: from .service import init_panel_http http_task = threading.Thread( target=init_panel_http, args=(domain, task_obj) ) http_task.start() return public.success_v2( { "result": "Set Panel's SSL Successfully! Please wait a few times.", "task_id": task_obj.id } ) # ========== Mail check =========== def mail_record_check(self, get): """ 邮箱域名解析检查 get.domain = "example.com" """ try: get.validate([ Param("domain").String().Require(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) domain = get.domain.strip() if not DomainValid.is_valid_domain(domain): return public.fail_v2(public.lang("Domain Not Valid!")) domain_info = { 'hash': '', 'domain': domain } from .service import get_mail_record records = get_mail_record(domain_info) if isinstance(records, str): return public.fail_v2(public.lang(records)) return public.success_v2(records) def move_old_account(): old_dns_api = os.path.join(public.get_panel_path(), "config/dns_api.json") read = public.readFile(old_dns_api) once = os.path.join(public.get_panel_path(), "config/dns_api_once.pl") if not os.path.exists(once): public.writeFile(once, "0") if public.readFile(once) != "0": return try: dnsapi_config = json.loads(read) if read else [] except Exception: return for dns in dnsapi_config: try: if dns.get("name") == "CloudFlareDns": dns_name = "CloudFlareDns" key = "" account = "" for cf in dns.get("data", []): if cf.get("key") == "SAVED_CF_MAIL" and cf.get("value") != "": account = cf.get("value", "") elif cf.get("key") == "SAVED_CF_KEY" and cf.get("value") != "": key = cf.get("value") if dns_name and key: try: exists = DnsDomainProvider.objects.filter( name=dns_name, api_user=account, api_key=key ).first() if not exists: if os.path.exists('/www/server/panel/data/cf_limit_api.pl'): limit = True else: limit = False get = public.dict_obj() get.name = dns_name get.api_user = account get.api_key = key get.permission = "limit" if limit is True else "global" get.alias = "myCloudFlareDns" DomainObject().create_dns_api(get) except: pass elif dns.get("name") == "NameCheapDns": dns_name = "NameCheapDns" key = "" account = "" for nc in dns.get("data", []): if nc.get("key") == "SAVED_NC_ACCOUNT" and nc.get("value") != "": account = nc.get("value", "") elif nc.get("key") == "SAVED_CX_APIKEY" and nc.get("value") != "": key = nc.get("value") if dns_name and key and account: try: exists = DnsDomainProvider.objects.filter( name=dns_name, api_user=account, api_key=key ).first() if not exists: try: get = public.dict_obj() get.name = dns_name get.api_user = account get.api_key = key get.alias = "myNameCheapDns" DomainObject().create_dns_api(get) except Exception: import traceback public.print_log(traceback.format_exc()) except: pass except Exception: continue public.writeFile(once, "1") move_old_account()