初始化1

This commit is contained in:
zg
2026-07-16 13:03:11 +08:00
parent 0bdfcbc1c8
commit e6b0f287cc
100 changed files with 12184 additions and 0 deletions

View File

@@ -0,0 +1,263 @@
"""
证书到期邮件通知服务
- 由 cron 每日 10:00 触发
- 检查数据库中的 Let's Encrypt 证书
- 到期前 N 天起,连续每天发送提醒邮件
- 邮件通过 Mokee Gateway 对外开放 API 发送
使用方式:
python cert_notifier.py # 直接运行cron 调用)
python cert_notifier.py --dry-run # 预览模式,不实际发送
python cert_notifier.py --check # 仅显示证书状态
"""
import os
import sys
import json
import logging
import urllib.request
import urllib.error
from datetime import datetime
# 数据库配置
DB_HOST = os.environ.get('MYSQL_HOST', '10.1.1.122')
DB_PORT = int(os.environ.get('MYSQL_PORT', '3306'))
DB_USER = os.environ.get('MYSQL_USER', 'nginxserver')
DB_PASS = os.environ.get('MYSQL_PASS', 'mokee.')
DB_NAME = os.environ.get('MYSQL_DB', 'nginxserver')
# 网关配置(可通过环境变量覆盖)
GATEWAY_URL = os.environ.get('MOKEE_GATEWAY_URL', 'https://gw.server.mokee.com')
SYSTEM_CODE = os.environ.get('MOKEE_SYSTEM_CODE', 'nginxServer')
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
def get_db_connection():
"""获取数据库连接(延迟导入,避免非数据库场景报错)"""
import pymysql
return pymysql.connect(
host=DB_HOST, port=DB_PORT, user=DB_USER,
password=DB_PASS, database=DB_NAME,
charset='utf8mb4', connect_timeout=5
)
def get_token():
"""从 Mokee Gateway 获取一次性 Token"""
url = f"{GATEWAY_URL}/zgapi/v1/open/token"
data = json.dumps({"systemCode": SYSTEM_CODE}).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST')
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req, timeout=10) as resp:
result = json.loads(resp.read().decode('utf-8'))
if result.get('code') == 200:
token = result['data']['token']
logger.info(f"获取 Token 成功: {SYSTEM_CODE}")
return token
else:
logger.error(f"获取 Token 失败: {result}")
return None
except Exception as e:
logger.error(f"获取 Token 异常: {e}")
return None
def send_email(token, to, subject, content):
"""
通过 Mokee Gateway 发送邮件
成功 = Token 被消费,需要重新获取
"""
url = f"{GATEWAY_URL}/zgapi/v1/open/email/send"
data = json.dumps({
"to": to,
"subject": subject,
"content": content,
"contentType": "html"
}).encode('utf-8')
req = urllib.request.Request(url, data=data, method='POST')
req.add_header('Content-Type', 'application/json')
req.add_header('Authorization', f'Bearer {token}')
try:
with urllib.request.urlopen(req, timeout=15) as resp:
result = json.loads(resp.read().decode('utf-8'))
return result.get('code') == 200, str(result)
except Exception as e:
return False, str(e)
def build_email_html(domain, days_left, expire_date):
"""生成 HTML 邮件内容"""
if days_left <= 5:
level = 'danger'
color = '#F56C6C'
elif days_left <= 10:
level = 'warning'
color = '#E6A23C'
else:
level = 'info'
color = '#409EFF'
return f"""<!DOCTYPE html>
<html><body style="font-family: 'Microsoft YaHei', Arial, sans-serif;">
<div style="max-width:600px;margin:0 auto;padding:20px;border:1px solid #EBEEF5;border-radius:8px;">
<div style="background:{color};padding:16px;border-radius:8px 8px 0 0;text-align:center;">
<h2 style="color:#fff;margin:0;">⚠️ SSL 证书即将到期</h2>
</div>
<div style="padding:20px;background:#fff;">
<table style="width:100%;border-collapse:collapse;">
<tr><td style="padding:8px 0;color:#909399;width:80px;">域名</td>
<td style="padding:8px 0;font-weight:bold;font-size:16px;">{domain}</td></tr>
<tr><td style="padding:8px 0;color:#909399;">到期时间</td>
<td style="padding:8px 0;">{expire_date}</td></tr>
<tr><td style="padding:8px 0;color:#909399;">剩余天数</td>
<td style="padding:8px 0;color:{color};font-weight:bold;font-size:20px;">{days_left} 天</td></tr>
<tr><td style="padding:8px 0;color:#909399;">证书类型</td>
<td style="padding:8px 0;">Let's Encrypt (阿里云 DNS 验证)</td></tr>
</table>
<hr style="border:0;border-top:1px solid #EBEEF5;margin:16px 0;">
<p style="color:#909399;font-size:13px;">
系统已配置 acme.sh 自动续期,正常情况下会在到期前自动更新证书。<br>
如续期失败,请检查:<br>
1. 阿里云 AccessKey 是否有效<br>
2. 服务器能否访问 Let's Encrypt API<br>
3. DNS TXT 记录是否正常清理
</p>
<p style="color:#C0C4CC;font-size:12px;">
此邮件由 Nginx 域名管理平台自动发送,连续提醒至证书续期成功。
</p>
</div>
</div>
</body></html>"""
def check_and_notify(dry_run=False):
"""检查证书到期情况并发送通知"""
conn = get_db_connection()
cursor = conn.cursor()
# 查询需要通知的 Let's Encrypt 证书
cursor.execute('''
SELECT id, domain, notify_email, notify_enabled, notify_days_before, expire_date
FROM certificates
WHERE cert_type = 'letsencrypt'
AND status = 'active'
AND notify_enabled = 1
AND notify_email IS NOT NULL
AND notify_email != ''
''')
certs = cursor.fetchall()
if not certs:
logger.info("无需通知的 Let's Encrypt 证书")
conn.close()
return
today = datetime.now()
token = None # 延迟获取
notified_count = 0
for cert_id, domain, notify_email, enabled, days_before, expire_date_db in certs:
# 从数据库获取过期日期
if expire_date_db:
expire_date = expire_date_db if isinstance(expire_date_db, datetime) else datetime.strptime(str(expire_date_db), '%Y-%m-%d %H:%M:%S')
days_left = (expire_date - today).days
else:
logger.warning(f"{domain}: 无过期日期信息,跳过")
continue
logger.info(f"{domain}: 剩余 {days_left} 天到期 (通知阈值: {days_before}天前开始)")
# 判断是否需要通知: days_before 天前开始连续通知到到期前1天
if days_left > days_before or days_left <= 0:
logger.info(f"{domain}: 不在通知窗口 (days_left={days_left}, threshold={days_before})")
continue
# 检查今天是否已发送过通知
cursor.execute('''
SELECT id FROM cert_notify_log
WHERE cert_id = %s AND DATE(sent_at) = CURDATE()
''', (cert_id,))
if cursor.fetchone():
logger.info(f"{domain}: 今日已通知,跳过")
continue
# 生成邮件内容
expire_str = expire_date.strftime('%Y-%m-%d %H:%M')
subject = f"⚠️ SSL证书到期提醒 - {domain} (剩余{days_left}天)"
content = build_email_html(domain, days_left, expire_str)
if dry_run:
logger.info(f"[DRY-RUN] 将发送邮件: to={notify_email}, subject={subject}")
continue
# 获取 Token每次发送前确保有效
if token is None:
token = get_token()
if token is None:
logger.error(f"无法获取 Token跳过 {domain}")
continue
# 发送邮件
success, resp = send_email(token, notify_email, subject, content)
if success:
# Token 已消费,下次需要重新获取
token = None
logger.info(f"{domain}: 邮件发送成功 → {notify_email}")
else:
logger.warning(f"{domain}: 邮件发送失败 → {resp}")
# 500 错误不消费 Token可重试
# 记录日志
cursor.execute('''
INSERT INTO cert_notify_log (cert_id, domain, days_left, email_to, success, response_msg)
VALUES (%s, %s, %s, %s, %s, %s)
''', (cert_id, domain, days_left, notify_email, 1 if success else 0, resp[:1000]))
conn.commit()
if success:
notified_count += 1
conn.close()
logger.info(f"通知完成: 发送 {notified_count} 封邮件")
def check_status():
"""仅显示证书状态(--check 模式)"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT domain, cert_type, status, expire_date,
DATEDIFF(COALESCE(expire_date, NOW()), NOW()) AS days_left,
notify_enabled, notify_email, notify_days_before
FROM certificates
WHERE cert_type = 'letsencrypt'
ORDER BY days_left
''')
print(f"\n{'域名':25s} {'类型':12s} {'状态':8s} {'到期日期':20s} {'剩余天数':>8s} {'通知':>4s} {'收件人'}")
print('-' * 120)
for row in cursor.fetchall():
domain, cert_type, status, expire_date, days_left, enabled, email, days_before = row
dl = str(days_left) if days_left is not None else 'N/A'
print(f"{domain:25s} {cert_type:12s} {status:8s} {str(expire_date):20s} {dl:>8s} {'' if enabled else '':>4s} {email or '-'}")
conn.close()
if __name__ == '__main__':
if '--dry-run' in sys.argv:
print("=== 预览模式 (--dry-run) ===\n")
check_and_notify(dry_run=True)
elif '--check' in sys.argv:
check_status()
else:
check_and_notify()