From 5bd9e95e15b8fc99fa6916e3d8fdb8375c6ad636 Mon Sep 17 00:00:00 2001 From: jingrow Date: Thu, 1 Jan 2026 18:28:10 +0000 Subject: [PATCH] =?UTF-8?q?ssl=5Fmanager=E5=A2=9E=E5=8A=A0=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=A2=9D=E5=A4=96=E5=9F=9F=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__pycache__/ssl_manager.cpython-313.pyc | Bin 18623 -> 25780 bytes .../__pycache__/test_ssl_auto.cpython-313.pyc | Bin 28488 -> 0 bytes ssl_manager/ssl_manager.py | 241 +++++++++++++++--- ssl_manager/test_ssl_auto.py | 74 +++--- 4 files changed, 236 insertions(+), 79 deletions(-) delete mode 100644 ssl_manager/__pycache__/test_ssl_auto.cpython-313.pyc diff --git a/ssl_manager/__pycache__/ssl_manager.cpython-313.pyc b/ssl_manager/__pycache__/ssl_manager.cpython-313.pyc index 1a09d1e7c984f73b34f06bb5358834b8ecd69f89..c8800d343873a7f3279d25c12a6ae8541872279a 100644 GIT binary patch delta 9748 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 o%w=*jUPe&jK$-8lt8U3II@rUuuRJ?=?lcr1B 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 List[str]: + """从证书内容中提取所有域名(包括 Subject Alternative Names)""" + domains = [] + try: + # 确保 cert_content 是字符串类型(因为 text=True) + if isinstance(cert_content, bytes): + cert_str = cert_content.decode('utf-8') + else: + cert_str = cert_content + + # 使用 openssl 命令提取证书信息 + # 注意:text=True 时,input 应该是字符串 + result = subprocess.run( + ['openssl', 'x509', '-noout', '-text', '-in', '/dev/stdin'], + input=cert_str, + capture_output=True, + text=True, + check=True, + timeout=10 + ) + + cert_text = result.stdout + + # 提取 Subject CN + cn_match = re.search(r'Subject:.*?CN\s*=\s*([^\s,/\n]+)', cert_text) + if cn_match: + domains.append(cn_match.group(1)) + + # 提取 Subject Alternative Names + # 匹配 SAN 部分,包括多行情况 + san_pattern = r'X509v3 Subject Alternative Name:\s*\n\s*((?:DNS:[^\n]+(?:\n\s+[^\n]+)*))' + san_section = re.search(san_pattern, cert_text, re.MULTILINE) + if san_section: + san_text = san_section.group(1) + # 匹配所有 DNS: 后面的域名(支持跨行) + dns_matches = re.findall(r'DNS:([^\s,/\n]+)', san_text) + domains.extend(dns_matches) + + # 如果上面的方法没匹配到,尝试更宽松的匹配 + if not domains or (san_section is None and 'Subject Alternative Name' in cert_text): + # 直接在 SAN 部分查找所有 DNS 条目 + san_start = cert_text.find('X509v3 Subject Alternative Name:') + if san_start != -1: + # 找到 SAN 部分,提取接下来的几行 + san_end = cert_text.find('\n\n', san_start) + if san_end == -1: + san_end = min(san_start + 500, len(cert_text)) # 最多取500字符 + san_block = cert_text[san_start:san_end] + # 匹配所有 DNS: 条目 + dns_matches = re.findall(r'DNS:([^\s,/\n]+)', san_block) + if dns_matches: + domains.extend(dns_matches) + + # 去重并保持顺序 + seen = set() + unique_domains = [] + for domain in domains: + if domain and domain not in seen: + seen.add(domain) + unique_domains.append(domain) + + if unique_domains: + logger.info(f"从证书中提取到域名: {unique_domains}") + else: + logger.warning("未能从证书中提取域名") + + return unique_domains + + except subprocess.CalledProcessError as e: + logger.error(f"提取证书域名失败: {e.stderr}") + return [] + except Exception as e: + logger.error(f"提取证书域名异常: {e}") + import traceback + logger.error(f"异常堆栈: {traceback.format_exc()}") + return [] + def read_cert_files(self, domain: str) -> Optional[Dict[str, str]]: """读取证书文件""" domain_cert_dir = Path(self.cert_dir) / domain @@ -117,36 +196,73 @@ class APISIXSSLManager: def upload_cert_to_apisix(self, domain: str, cert_content: str, key_content: str) -> bool: """将证书上传到 APISIX""" - # 生成 SSL ID(使用域名作为 ID) + # 从证书中提取所有域名(包括 SAN) + cert_domains = self.extract_domains_from_cert(cert_content) + + # 如果没有提取到域名,使用传入的 domain 作为后备 + if not cert_domains: + logger.warning(f"无法从证书提取域名,使用传入的域名: {domain}") + cert_domains = [domain] + + # 确保传入的 domain 也在列表中(如果不在的话) + if domain not in cert_domains: + cert_domains.append(domain) + + # 生成 SSL ID(使用主域名作为 ID,用于查找和更新) ssl_id = domain.replace('.', '_').replace('*', 'wildcard') - # 构建 SSL 配置(创建时不包含 id) + # 构建 SSL 配置(创建时不包含 id,更新时需要 id) + # SNI 列表包含证书中的所有域名 ssl_config = { - "snis": [domain], + "snis": cert_domains, "cert": cert_content, "key": key_content } - # 检查是否已存在 - check_url = f"{self.apisix_admin_url}/apisix/admin/ssls/{ssl_id}" + logger.info(f"配置 SNI 域名列表: {cert_domains}") + headers = self._get_apisix_headers() try: - # 先检查是否存在 + # 先检查是否已存在相同 SNI 的配置 + # 方法1:通过 ID 查找(如果之前创建时使用了这个 ID) + check_url = f"{self.apisix_admin_url}/apisix/admin/ssls/{ssl_id}" response = requests.get(check_url, headers=headers, timeout=10) + existing_ssl_id = None if response.status_code == 200: + existing_ssl_id = ssl_id + logger.info(f"找到现有 SSL 配置 (ID: {ssl_id})") + else: + # 方法2:查询所有 SSL 配置,检查是否有相同 SNI 的配置 + all_ssls_url = f"{self.apisix_admin_url}/apisix/admin/ssls" + all_response = requests.get(all_ssls_url, headers=headers, timeout=10) + if all_response.status_code == 200: + all_ssls = all_response.json() + ssl_list = all_ssls.get('list', []) if isinstance(all_ssls, dict) else all_ssls + + # 检查每个 SSL 配置的 SNI 是否匹配 + for ssl_item in ssl_list: + ssl_value = ssl_item.get('value', {}) if isinstance(ssl_item, dict) else ssl_item + existing_snis = ssl_value.get('snis', []) + # 检查 SNI 列表是否相同(忽略顺序) + if set(existing_snis) == set(cert_domains): + existing_ssl_id = ssl_item.get('id') or ssl_item.get('key', {}).get('id') + logger.info(f"找到现有 SSL 配置,SNI 匹配 (ID: {existing_ssl_id})") + break + + if existing_ssl_id: # 更新现有证书(更新时需要 id) - logger.info(f"更新 APISIX SSL 配置: {domain}") - ssl_config["id"] = ssl_id + logger.info(f"更新 APISIX SSL 配置: {domain} (ID: {existing_ssl_id})") + ssl_config["id"] = existing_ssl_id response = requests.put( - f"{self.apisix_admin_url}/apisix/admin/ssls/{ssl_id}", + f"{self.apisix_admin_url}/apisix/admin/ssls/{existing_ssl_id}", headers=headers, json=ssl_config, timeout=10 ) else: - # 创建新证书(创建时不需要 id,APISIX 会自动生成) + # 创建新证书(POST 时不包含 id,让 APISIX 自动生成) logger.info(f"创建 APISIX SSL 配置: {domain}") response = requests.post( f"{self.apisix_admin_url}/apisix/admin/ssls", @@ -166,8 +282,14 @@ class APISIXSSLManager: logger.error(f"上传证书到 APISIX 失败: {e}") return False - def request_certificate(self, domain: str, additional_domains: List[str] = None) -> bool: - """申请 Let's Encrypt 证书""" + def request_certificate(self, domain: str, additional_domains: List[str] = None, max_retries: int = 3) -> bool: + """申请 Let's Encrypt 证书 + + Args: + domain: 主域名 + additional_domains: 额外域名列表 + max_retries: 最大重试次数(默认3次) + """ domains = [domain] if additional_domains: domains.extend(additional_domains) @@ -193,34 +315,73 @@ class APISIXSSLManager: logger.info(f"申请证书: {domain}, 命令: {' '.join(cmd)}") - try: - result = subprocess.run( - cmd, - capture_output=True, - text=True, - check=True, - timeout=300 - ) - - if result.returncode == 0: - logger.info(f"证书申请成功: {domain}") - # 读取证书并上传到 APISIX - cert_data = self.read_cert_files(domain) - if cert_data: - return self.upload_cert_to_apisix(domain, cert_data['cert'], cert_data['key']) - else: - logger.error(f"无法读取证书文件: {domain}") - return False - else: - logger.error(f"证书申请失败: {result.stderr}") - return False + # 重试机制 + for attempt in range(1, max_retries + 1): + try: + if attempt > 1: + logger.info(f"第 {attempt} 次尝试申请证书 (共 {max_retries} 次)...") + time.sleep(5) # 重试前等待5秒 - except subprocess.TimeoutExpired: - logger.error(f"证书申请超时: {domain}") - return False - except Exception as e: - logger.error(f"证书申请异常: {e}") - return False + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, # 不自动抛出异常,手动处理 + timeout=300 + ) + + if result.returncode == 0: + logger.info(f"证书申请成功: {domain}") + # 读取证书并上传到 APISIX + cert_data = self.read_cert_files(domain) + if cert_data: + return self.upload_cert_to_apisix(domain, cert_data['cert'], cert_data['key']) + else: + logger.error(f"无法读取证书文件: {domain}") + return False + else: + # 检查是否是网络超时错误 + error_output = result.stderr or "" + is_timeout_error = "ReadTimeout" in error_output or "timed out" in error_output.lower() + + if is_timeout_error and attempt < max_retries: + logger.warning(f"证书申请网络超时 (尝试 {attempt}/{max_retries}),将重试...") + continue + else: + logger.error(f"证书申请失败 (退出码: {result.returncode})") + if result.stdout: + logger.error(f"标准输出: {result.stdout}") + if result.stderr: + logger.error(f"错误输出: {result.stderr}") + + # 如果是网络超时且已尝试所有次数,给出提示 + if is_timeout_error: + logger.error("网络连接超时,可能的原因:") + logger.error("1. 服务器无法访问 Let's Encrypt 服务器 (acme-staging-v02.api.letsencrypt.org 或 acme-v02.api.letsencrypt.org)") + logger.error("2. 防火墙阻止了 HTTPS 连接") + logger.error("3. 网络不稳定,建议稍后重试") + logger.error("4. 可以检查网络连接: curl -I https://acme-staging-v02.api.letsencrypt.org/directory") + + return False + + except subprocess.TimeoutExpired: + if attempt < max_retries: + logger.warning(f"证书申请超时 (尝试 {attempt}/{max_retries}),将重试...") + continue + else: + logger.error(f"证书申请超时: {domain} (已尝试 {max_retries} 次)") + return False + except Exception as e: + if attempt < max_retries: + logger.warning(f"证书申请异常 (尝试 {attempt}/{max_retries}): {e},将重试...") + continue + else: + logger.error(f"证书申请异常: {e}") + import traceback + logger.error(f"异常堆栈: {traceback.format_exc()}") + return False + + return False def renew_certificate(self, domain: str) -> bool: """续期证书""" diff --git a/ssl_manager/test_ssl_auto.py b/ssl_manager/test_ssl_auto.py index 17a2b7c..072754a 100755 --- a/ssl_manager/test_ssl_auto.py +++ b/ssl_manager/test_ssl_auto.py @@ -331,15 +331,21 @@ class SSLTestRunner: print_error(f"测试验证路径异常: {e}") return False - def create_test_route(self, domain: str) -> bool: + def create_test_route(self, domain: str, additional_domains: list = None) -> bool: """创建测试路由""" print_info(f"创建测试路由: {domain}") + # 构建域名列表(主域名 + 额外域名) + hosts = [domain] + if additional_domains: + hosts.extend(additional_domains) + print_info(f"路由将包含域名: {', '.join(hosts)}") + route_config = { "uri": "/*", "name": domain, "methods": ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"], - "host": domain, + "hosts": hosts, # 使用 hosts 数组支持多个域名 "upstream": { "nodes": [ { @@ -373,44 +379,22 @@ class SSLTestRunner: print_error(f"创建测试路由异常: {e}") return False - def request_certificate(self, domain: str) -> bool: + def request_certificate(self, domain: str, additional_domains: list = None) -> bool: """申请证书""" - print_info(f"申请证书: {domain} (staging={self.staging})") - - cmd = [ - self.ssl_manager.certbot_path, - 'certonly', - '--webroot', - '--webroot-path', self.webroot_path, - '--non-interactive', - '--agree-tos', - '--email', self.email, - '--cert-name', domain, - '-d', domain - ] - - if self.staging: - cmd.append('--staging') + if additional_domains: + print_info(f"申请证书: {domain} + {additional_domains} (staging={self.staging})") + else: + print_info(f"申请证书: {domain} (staging={self.staging})") + # 使用 ssl_manager 的 request_certificate 方法,它已经支持额外域名 try: - print_info(f"执行命令: {' '.join(cmd)}") - result = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=300 - ) - - if result.returncode == 0: + result = self.ssl_manager.request_certificate(domain, additional_domains) + if result: print_success(f"证书申请成功: {domain}") - print_info(result.stdout) return True else: - print_error(f"证书申请失败: {result.stderr}") + print_error(f"证书申请失败: {domain}") return False - except subprocess.TimeoutExpired: - print_error("证书申请超时") - return False except Exception as e: print_error(f"证书申请异常: {e}") return False @@ -504,7 +488,7 @@ class SSLTestRunner: except: pass - def run_full_test(self, domain: str = None, cleanup: bool = False): + def run_full_test(self, domain: str = None, additional_domains: list = None, cleanup: bool = False): """运行完整测试""" if not domain: domain = self.test_domain @@ -512,6 +496,8 @@ class SSLTestRunner: print(f"\n{Colors.BOLD}{'='*60}") print(f"APISIX SSL 证书自动申请测试") print(f"测试域名: {domain}") + if additional_domains: + print(f"额外域名: {', '.join(additional_domains)}") print(f"Staging 模式: {self.staging}") print(f"{'='*60}{Colors.RESET}\n") @@ -521,10 +507,10 @@ class SSLTestRunner: (3, "检查 Webroot 目录", lambda: self.check_webroot_directory()), (4, "检查/创建 Webroot 路由", lambda: self.check_webroot_route(domain)), (5, "测试验证路径", lambda: self.test_verification_path(domain)), - (6, "创建测试路由", lambda: self.create_test_route(domain)), - (7, "申请 SSL 证书", lambda: self.request_certificate(domain)), - (8, "同步证书到 APISIX", lambda: self.sync_certificate_to_apisix(domain)), - (9, "验证证书信息", lambda: self.verify_certificate(domain)), + (6, "创建测试路由", lambda: self.create_test_route(domain, additional_domains)), + (7, "申请 SSL 证书", lambda: self.request_certificate(domain, additional_domains)), + # 注意:证书申请成功后会自动上传到 APISIX,不需要单独同步步骤 + (8, "验证证书信息", lambda: self.verify_certificate(domain)), ] success_count = 0 @@ -564,6 +550,7 @@ def main(): parser = argparse.ArgumentParser(description='APISIX SSL 证书自动申请测试脚本') parser.add_argument('--domain', '-d', help='测试域名(不指定则自动生成)') + parser.add_argument('--additional-domains', '-a', nargs='+', help='额外域名(如 www 子域名)') parser.add_argument('--config', '-c', help='配置文件路径(可选,用于覆盖默认配置)') parser.add_argument('--cleanup', action='store_true', help='测试完成后清理测试数据') parser.add_argument('--no-cleanup', action='store_true', help='测试完成后不清理测试数据') @@ -579,9 +566,18 @@ def main(): print_warning(f"未指定域名,使用自动生成的测试域名: {domain}") print_info("注意:此域名需要 DNS 解析到当前服务器才能申请证书") + # 处理额外域名 + additional_domains = [] + if args.additional_domains: + additional_domains.extend(args.additional_domains) + cleanup = args.cleanup or (not args.no_cleanup and not args.domain) - success = runner.run_full_test(domain, cleanup=cleanup) + success = runner.run_full_test( + domain, + additional_domains=additional_domains if additional_domains else None, + cleanup=cleanup + ) sys.exit(0 if success else 1)