# coding: utf-8 # ------------------------------ # business ssl # ------------------------------ import json import os import re import threading import time import public from YakPanel import app from acme_v2 import acme_v2 from public.validate import Param from ssl_domainModelV2.api import DomainObject from ssl_domainModelV2.model import DnsDomainProvider, DnsDomainSSL, DnsDomainRecord from ssl_domainModelV2.service import CertHandler, SyncService from panelDnsapi import extract_zone # noinspection PyUnusedLocal class BusinessSSL(object): __BINDURL = f"{public.OfficialApiBase()}/api/user" # 获取token 获取官网token __APIURL = f"{public.OfficialApiBase()}/api" __CODEURL = f"{public.OfficialApiBase()}/Auth/GetBindCode" # 获取绑定验证码 __UPATH = "data/userInfo.json" __PUBKEY = "data/public.key" __userInfo = {} __PDATA = None _check_url = None def __init__(self): pdata = { "access_key": "test", "secret_key": "123456", } data = { "access_key": "test", "secret_key": "123456", } # 存放调用接口的参数 if os.path.exists(self.__UPATH): my_tmp = public.readFile(self.__UPATH) if my_tmp: try: self.__userInfo = json.loads(my_tmp) except: pass try: if self.__userInfo: pdata["access_key"] = self.__userInfo["access_key"] data["secret_key"] = self.__userInfo["secret_key"] except: pass pdata["data"] = data self.__PDATA = pdata # 校验域名是否有适配的dns def check_domain_suitable(self, get): """ 校验域名是否符合 """ if not hasattr(get, "domains"): return public.fail_v2("Domain name cannot be empty") domains = str(get.domains).strip() if not domains: return public.fail_v2("Domain name cannot be empty") domains = domains.split(",") if len(domains) == 0: return public.fail_v2("Domain name cannot be empty") auto = [] for d in domains: if not d: continue root, _, _ = extract_zone(d) obj = DnsDomainProvider.objects.filter( domains__contains=root, status=1 ).fields("id", "name", "alias").first() if obj: auto.append({"domain": d, **obj.as_dict()}) res = {"auto": auto} return public.success_v2(res) # 发送请求 def request(self, dname): try: temp = json.dumps(self.__PDATA.get("data")) except: temp = self.__PDATA.get("data") self.__PDATA["data"] = temp if not self.__userInfo.get("token"): raise public.HintException("Please Login First") url_headers = { "authorization": "bt {}".format(self.__userInfo["token"]) } try: response_data = public.httpPost(self.__APIURL + "/" + dname, data=self.__PDATA, headers=url_headers) except Exception as ex: raise public.error_conn_cloud(str(ex)) try: return json.loads(response_data) except: return public.return_msg_gettext( False, "Failed to connect to the official website, please try again later!" ) # 获取证书管理员信息 def get_cert_admin(self, get): result = self.request("cert/user/administrator") return public.success_v2(result.get("res")) # 获取产品列表 def get_product_list(self, get): result = self.request("cert/product/list") return public.success_v2(result.get("res")) # 获取商业证书 def get_order_find(self, get): self.__PDATA["uc_id"] = get.uc_id result = self.request('cert/user/info') return public.success_v2(result.get("res")) # 生成商业证书支付订单 def apply_cert_order_pay(self, args): pdata = json.loads(args.pdata) self.__PDATA["data"] = pdata result = self.request("cert/order/create") return public.success_v2(result.get("res")) # 单独购买人工安装服务 def apply_cert_install_pay(self, args): """ @name 单独购买人工安装服务 @param args{ 'uc_id' 订单ID } """ self.__PDATA['uc_id'] = args.uc_id result = self.request('cert/order/deployment_assistance') return result # 商业证书订单列表 def get_order_list(self, get=None): result = self.request("cert/user/list") if result.get("success") is False: return public.fail_v2(public.lang("Failed to get order list, please try again later!")) return public.success_v2(result.get("res")) # 获取网站运行目录 def _get_site_run_path(self, pid): """ @name 获取网站运行目录 @author hwliang<2020-08-05> @param pid(int) 网站标识 @return string """ siteInfo = public.M('sites').where('id=?', (pid,)).find() siteName = siteInfo['name'] sitePath = siteInfo['path'] webserver_type = public.get_webserver() setupPath = '/www/server' path = None if webserver_type == 'nginx': filename = setupPath + '/panel/vhost/nginx/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) rep = r'\s*root\s+(.+);' tmp1 = re.search(rep, conf) if tmp1: path = tmp1.groups()[0] elif webserver_type == 'apache': filename = setupPath + '/panel/vhost/apache/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) rep = r'\s*DocumentRoot\s*"(.+)"\s*\n' tmp1 = re.search(rep, conf) if tmp1: path = tmp1.groups()[0] else: filename = setupPath + '/panel/vhost/openlitespeed/' + siteName + '.conf' if os.path.exists(filename): conf = public.readFile(filename) rep = r"vhRoot\s*(.*)" path = re.search(rep, conf) if not path: path = None else: path = path.groups()[0] if not path: path = sitePath return path # 获取指定域名的PATH def _get_domain_run_path(self, domain): pid = public.M('domain').where('name=?', (domain,)).getField('pid') if not pid: return False return self._get_site_run_path(pid) def check_ssl_caa(self, domains, clist: list = None): """ @name 检查CAA记录是否正确 @param domains 域名列表 @param clist 正确的记录值关键词 @return bool """ if clist is None: clist = ["sectigo.com", "digicert.com", "comodoca.com"] try: data = {} for domain in domains: root, zone = public.get_root_domain(domain) for d in [ domain, root, f"_acme-challenge.{root}", f"_acme-challenge.{domain}" ]: ret = public.query_dns(d, "CAA") if not ret: continue slist = [] for val in ret: if val['value'] in clist: return False slist.append(val) if len(slist) > 0: data[d] = slist if data: return { "status": False, "msg": "error: There is a CAA record in the DNS resolution " "of the domain name. Please delete it and apply again ", "data": json.dumps(data), "caa_list": data, } except: pass return False # 完善资料CA, 提交资料 (前提支付接口完成) def apply_order_ca(self, args): """ auto = [ { "domain": "testwpsite.yakpanel.org", "id": 13, "name": "CloudFlareDns", "alias": "a" }, { "domain": "testphp22.yakpanel.org", "id": 14, "name": "CloudFlareDns", "alias": "b" } ] """ pdata = json.loads(args.pdata) result = self.check_ssl_caa(pdata['domains']) if result: return public.fail_v2(result) auto: list = pdata.pop("auto") uc_id = pdata.pop("uc_id") self.__PDATA["data"] = pdata result = self.request("cert/user/update_profile") # submit if result.get("success") is False: return public.fail_v2(result.get("res", "Failed to submit data, please try again later!")) if not auto or not uc_id: return public.success_v2(result.get("res")) task = threading.Thread(target=self._auto_dns, args=(auto, uc_id)) task.start() return public.success_v2(result.get("res")) # 修改验证方式 def again_verify(self, args): try: args.validate([ Param("uc_id").String().Require(), Param("dcv_method").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)) self.__PDATA['uc_id'] = args.uc_id self.__PDATA['dcv_method'] = args.dcv_method result = self.request('cert/user/update_dcv') if result.get("success"): return public.success_v2(result.get("res")) return public.fail_v2(result.get("res")) # dns auto parsed def _auto_dns(self, auto: list, uc_id: int) -> None: with app.app_context(): # makesure pending count = 0 while count <= 999: new_get = public.dict_obj() new_get.uc_id = uc_id res = self.get_verify_result(new_get) if res.get("status") == 0: v_msg = res.get("message") if v_msg.get("code") == 1 and v_msg.get("certStatus") == "PENDING": verify_data = v_msg.get("data") break count += 1 time.sleep(10) for a in auto: if not a.get("id"): continue if not a.get("domain"): continue if not a.get("name"): continue provider = DnsDomainProvider.objects.filter(id=a.get("id")).first() if provider: boyd = { "provider_id": provider.id, "provider_name": provider.name, "api_user": provider.api_user, "domain": verify_data.get("dcvList")[0].get("domainName"), "record": verify_data.get("DCVdnsHost"), "record_value": verify_data.get("DCVdnsValue"), "record_type": verify_data.get("DCVdnsType"), "ttl": 1, "proxy": 0 if a.get("name") == "CloudFlareDns" else -1, "priority": -1, } provider.model_create_dns_record(boyd) # 验证通过后移除CNAME def _remove_caname(self, uc_id: int, ssl_info: dict) -> None: with app.app_context(): provider = DnsDomainProvider.objects.filter(id=ssl_info.get("provider_id")).first() if provider and uc_id: get = public.dict_obj() get.uc_id = uc_id order = self.get_verify_result(get) if order.get("status") != 0: public.print_log("remove caname error: order status not 0") return if order.get("message").get("status") != "COMPLETE": public.print_log("remove caname error: order status not complete") return data = order.get("message").get("data") try: DCVdnsHost = data.get("DCVdnsHost") DCVdnsType = data.get("DCVdnsType") domainName = data.get("dcvList")[0].get("domainName") root, _ = public.get_root_domain(domainName) except Exception as e: public.print_log(f"error: {e}") if not DCVdnsHost or not DCVdnsType or not root: public.print_log( f"remove caname error: " f"DCVdnsHost={DCVdnsHost}, DCVdnsType={DCVdnsType}, domainName root={root}" ) return SyncService().records_process(provider_obj=provider, all_domains=[root]) record = DnsDomainRecord.objects.filter( provider_id=provider.id, domain=root, record=DCVdnsHost, record_type=DCVdnsType, ).first() if record: provider.model_delete_dns_record(record.id) @staticmethod def _replace_all_cert(ssl_info: dict): try: acme_v2().sub_all_cert( key_file=os.path.join(ssl_info["path"], "privkey.pem"), pem_file=os.path.join(ssl_info["path"], "fullchain.pem"), ) except Exception as e: public.print_log("replace business cert error: {}".format(e)) # 验证URL是否匹配 def check_url_txt(self, args): try: args.validate([ Param("url").String().Require(), Param("content").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)) url = args.url content = args.content import http_requests res = http_requests.get(url, s_type='curl', timeout=5) result = res.text status = 0 if not result: status = 0 if result.find('11001') != -1 or result.find('curl: (6)') != -1: status = -1 if result.find('curl: (7)') != -1 or res.status_code in [403, 401]: status = -5 if result.find('Not Found') != -1 or result.find('not found') != -1 or res.status_code in [404]: status = -2 if result.find('timed out') != -1: status = -3 if result.find('301') != -1 or result.find('302') != -1 or result.find( 'Redirecting...') != -1 or res.status_code in [301, 302]: status = -4 if result == content: status = 1 if status == 1: return public.success_v2({"status": status}) else: return public.fail_v2({"status": status}) # 获取商业证书验证结果, 包含验证信息 def get_verify_result(self, args): try: self.__PDATA['uc_id'] = args.uc_id res = self.request('cert/user/validate') if res.get("success", False) is False: return public.fail_v2( res.get("res", "Failed to get verification result, please try again later!") ) verify_info = res['res'] if verify_info['status'] in ['COMPLETE', False]: return public.success_v2(verify_info) is_file_verify = 'CNAME_CSR_HASH' != verify_info['data']['dcvList'][0]['dcvMethod'] verify_info['paths'] = [] verify_info['hosts'] = [] if verify_info['data']['application']['status'] == 'ongoing': return public.fail_v2( public.lang("In verification, please contact YakPanel if the audit still fails after 24 hours") ) for dinfo in verify_info['data']['dcvList']: is_https = dinfo['dcvMethod'] == 'HTTPS_CSR_HASH' if is_https: is_https = 's' else: is_https = '' domain = dinfo['domainName'] if domain[:2] == '*.': domain = domain[2:] dinfo['domainName'] = domain if is_file_verify: valid_path = "/.well-known/pki-validation" siteRunPath = self._get_domain_run_path(domain) url = 'http' + is_https + '://' + domain + valid_path + '/' + verify_info['data']['DCVfileName'] get = public.dict_obj() get.url = url get.content = verify_info['data']['DCVfileContent'] status = self.check_url_txt(get)["message"].get("status") verify_info['paths'].append({'url': url, 'status': status}) if not siteRunPath: continue verify_path = siteRunPath + valid_path if not os.path.exists(verify_path): os.makedirs(verify_path) verify_file = verify_path + '/' + verify_info['data']['DCVfileName'] if os.path.exists(verify_file): continue public.writeFile(verify_file, verify_info['data']['DCVfileContent']) else: # if domain[:4] == 'www.': domain = domain[4:] domain, subb = public.get_root_domain(domain) dinfo['domainName'] = domain if verify_info['data'].get('DCVdnsHost'): verify_info['hosts'].append(verify_info['data']['DCVdnsHost'] + '.' + domain) return public.success_v2(verify_info) except Exception as e: return public.fail_v2("Failed to get verification result, please try again later!") # 下载证书 def download_cert(self, get): self.__PDATA["uc_id"] = get.uc_id result = self.request("cert/user/download") return result # 商业证书入库 def _save_ssl(self, order: dict) -> None: """ 阻塞型更新入库 """ if DnsDomainSSL.objects.filter(cert_id=order.get("certId")).first(): return try: new_get = public.dict_obj() new_get.uc_id = order.get("uc_id") cert = self.get_order_find(new_get) cert = cert.get("message") cert_private = cert.get("private_key") cert_pem = cert.get("certificate") + "\n" + cert.get("ca_certificate") handler = CertHandler() hash_ = handler.get_hash(cert_pem) # cert maybe uploaded to the free pages, resave. DnsDomainSSL.objects.filter(hash=hash_).delete() # 入库, 继承user_for ssl_info = handler.save_by_data( cert_pem=cert_pem, private_key=cert_private, order={ "cert_id": order.get("certId"), "order_info": order, } ) if ssl_info: try: # 首次入库替换证书 self._replace_all_cert(ssl_info) except Exception as e1: public.print_log(f"replace business cert error: {e1}") try: # 首次入库尝试移除caname task = threading.Thread( target=self._remove_caname, args=(order.get("uc_id"), ssl_info,) ) task.start() except Exception as err: public.print_log(f"remove business ssl caname error: {err}") except Exception as e: public.print_log(f"update business ssl error: {e}") return @staticmethod def find_next_cert(cert_id: str): if not cert_id: return None try: while 1: time.sleep(3) order_list = BusinessSSL().get_order_list().get("message", []) for o in order_list: if o.get("p_certId") == cert_id: return o except Exception as e: public.print_log(f"find_cert error: {e}") raise Exception(f"Failed to find cert for {cert_id}, please try again later!") # 商业续签 def renew_cert_order(self, get, cert_id: str): """ @name 商业证书续签 首次续签请求续签然后verify轮询 cert_id 续签得cert_id, 可能会被不断重签, 需要不断轮询获取最新的cert_id去验证 """ self.__PDATA['uc_id'] = get.uc_id result = self.request('cert/user/renew') count = 99 try: new_order = None cur_cert_id = cert_id while count >= 0: count -= 1 if not new_order: new_order = self.find_next_cert(cur_cert_id) # 验证 new_order 订单 args = public.dict_obj args.uc_id = new_order.get("uc_id") time.sleep(3) verify = self.get_verify_result(args) if verify.get("status") == 0: if verify.get("message").get("status") == "REISSUED": # new_order has been REISSUED, update cur_cert_id, reset new_order cur_cert_id = new_order['certId'] new_order = None continue if verify.get("message").get("status") == "COMPLETE": return public.success_v2("success") except Exception as e: return public.fail_v2("Failed to renew certificate, please try again later: {}".format(e)) finally: # it will renew db cert self.list_business_ssl(public.to_dict_obj({"p": 1, "limit": "10"})) return public.fail_v2("failed to verify certificate") # 商业证书列表 def list_business_ssl(self, get=None): try: if get: get.validate([ Param("p").Integer(), Param("limit").String(), ], [ public.validate.trim_filter(), ]) except Exception as ex: public.print_log("error info: {}".format(ex)) return public.return_message(-1, 0, str(ex)) org_orders = self.get_order_list() if org_orders.get("status") != 0: return public.fail_v2("Failed To Get Order List, Please Try Again Later") if not isinstance(org_orders.get("message"), list): return public.fail_v2("Failed to get order list, please try again later!") msg = "Get Business SSL Order Successfully" if len(org_orders.get("message")) == 0: DnsDomainSSL.objects.filter(is_order=1).delete() return public.success_v2({"data": [], "total": 0, "msg": msg}) res = [] org_orders["message"].sort(key=lambda i: i.get("uc_id")) for o in org_orders["message"]: if o.get("order_status") == "COMPLETE" and o.get("certId"): if bool([x for x in org_orders["message"] if x.get("p_certId") == o.get("certId")]): continue self._save_ssl(o) else: DnsDomainSSL.objects.filter( order_info__pid=o.get("pid"), order_info__uc_id=o.get("uc_id"), order_info__oid=o.get("oid"), ).delete() if o.get("order_status") == "REISSUED": continue res.append({"order_info": o}) page = int(getattr(get, "p", 1)) limit = int(getattr(get, "limit", 100)) ssl_obj = DnsDomainSSL.objects.filter(is_order=1) total = ssl_obj.count() ssl_obj.offset((page - 1) * limit).limit(limit) res.extend([ { "hash": ssl.hash, "provider": ssl.info.get("issuer_O", "unknown"), "issuer": ssl.info.get("issuer", "unknown"), "verify_domains": list(set(ssl.dns + [ssl.subject])), "end_time": DomainObject._end_time(ssl.not_after), "end_date": ssl.not_after, "last_apply_time": ssl.info.get("notBefore", ""), "auto_renew": ssl.auto_renew, "cert": { "csr": public.readFile(ssl.path + "/fullchain.pem"), # 证书 "key": public.readFile(ssl.path + "/privkey.pem"), # 密钥 }, "order_info": ssl.order_info, "user_for": ssl.user_for, "alarm": ssl.alarm, } for ssl in ssl_obj ]) return public.success_v2({"data": res, "total": total, "msg": msg})