修复
This commit is contained in:
+12
-4
@@ -310,15 +310,23 @@ class AcmeService:
|
||||
|
||||
try:
|
||||
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
|
||||
# 兼容新旧版本 cryptography 库
|
||||
not_before = getattr(cert, "not_valid_before_utc", None) or cert.not_valid_before
|
||||
not_after = getattr(cert, "not_valid_after_utc", None) or cert.not_valid_after
|
||||
try:
|
||||
san = [name.value for name in cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value]
|
||||
except Exception:
|
||||
san = []
|
||||
return {
|
||||
"subject": cert.subject.rfc4514_string(),
|
||||
"issuer": cert.issuer.rfc4514_string(),
|
||||
"not_before": cert.not_valid_before_utc.isoformat(),
|
||||
"not_after": cert.not_valid_after_utc.isoformat(),
|
||||
"not_before": not_before.isoformat(),
|
||||
"not_after": not_after.isoformat(),
|
||||
"serial_number": str(cert.serial_number),
|
||||
"san": [name.value for name in cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value],
|
||||
"san": san,
|
||||
}
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.error(f"解析证书失败 {cert_path}: {e}")
|
||||
return None
|
||||
|
||||
def auto_renew_all(self, domains: list[Domain]) -> list[dict]:
|
||||
|
||||
+67
-1
@@ -1,14 +1,16 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import asyncio
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from starlette.requests import Request
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
from backend.database import init_db
|
||||
from backend.database import init_db, async_session
|
||||
from backend.routers import api, admin
|
||||
from backend.config import get_settings
|
||||
from backend.bootstrap import ensure_self_signed_cert
|
||||
@@ -25,12 +27,75 @@ from backend.routers.admin import (
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger("certcenter")
|
||||
|
||||
scheduler = AsyncIOScheduler()
|
||||
|
||||
|
||||
async def auto_renew_job():
|
||||
"""定时任务:自动续签即将过期的证书"""
|
||||
from backend.routers.admin import _get_domain_acme_config
|
||||
from backend.models import Domain, AcmeLog
|
||||
from backend.acme_service import AcmeService
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
from datetime import datetime
|
||||
|
||||
logger.info("[定时任务] 开始检查证书续签...")
|
||||
try:
|
||||
async with async_session() as db:
|
||||
settings = get_settings()
|
||||
result = await db.execute(select(Domain).options(selectinload(Domain.acme_config)))
|
||||
domains = result.scalars().all()
|
||||
|
||||
renewed = 0
|
||||
for d in domains:
|
||||
try:
|
||||
config = await _get_domain_acme_config(d, db)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
service = AcmeService(config, settings.cert_dir)
|
||||
need_renew, days_left = service.check_expiry(d.domain)
|
||||
if not need_renew:
|
||||
continue
|
||||
|
||||
logger.info(f"[定时任务] {d.domain} 需要续签(剩余 {days_left} 天)")
|
||||
log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"自动续签 {d.domain}")
|
||||
db.add(log)
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
success, msg = service.renew_certificate(d.domain)
|
||||
log.status = "success" if success else "failed"
|
||||
log.message = msg
|
||||
if success:
|
||||
info = service.get_cert_info(d.domain)
|
||||
if info:
|
||||
d.cert_not_after = datetime.fromisoformat(info["not_after"])
|
||||
d.version = str(int(d.version or "0") + 1)
|
||||
renewed += 1
|
||||
logger.info(f"[定时任务] {d.domain}: {'成功' if success else '失败'} - {msg}")
|
||||
except Exception as e:
|
||||
log.status = "failed"
|
||||
log.message = str(e)
|
||||
logger.error(f"[定时任务] {d.domain} 续签异常: {e}")
|
||||
|
||||
await db.commit()
|
||||
logger.info(f"[定时任务] 检查完成,续签 {renewed} 个证书")
|
||||
except Exception as e:
|
||||
logger.error(f"[定时任务] 执行异常: {e}")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
await init_db()
|
||||
settings = get_settings()
|
||||
ensure_self_signed_cert(settings.base_url, settings.cert_dir)
|
||||
|
||||
# 启动定时续签任务(每天凌晨3点执行)
|
||||
scheduler.add_job(auto_renew_job, "cron", hour=3, minute=0, id="auto_renew", replace_existing=True)
|
||||
scheduler.start()
|
||||
logger.info("=== 定时续签任务已启动(每天 03:00) ===")
|
||||
|
||||
logger.info("=== 服务启动完成 ===")
|
||||
# 打印所有已注册的路由
|
||||
for route in app.routes:
|
||||
@@ -41,6 +106,7 @@ async def lifespan(app: FastAPI):
|
||||
else:
|
||||
logger.info(f" [included router] {type(route).__name__}")
|
||||
yield
|
||||
scheduler.shutdown()
|
||||
|
||||
|
||||
app = FastAPI(title="CertCenter", version="1.0.0", lifespan=lifespan)
|
||||
|
||||
@@ -247,6 +247,16 @@ async def list_domains(db: AsyncSession = Depends(get_db), _: bool = Depends(req
|
||||
select(Domain).options(selectinload(Domain.server), selectinload(Domain.acme_config)).order_by(Domain.id)
|
||||
)
|
||||
domains = result.scalars().all()
|
||||
|
||||
# 查询每个域名的最新部署日志
|
||||
deploy_status = {}
|
||||
log_result = await db.execute(
|
||||
select(DeployLog).order_by(DeployLog.id.desc())
|
||||
)
|
||||
for log in log_result.scalars().all():
|
||||
if log.domain_id not in deploy_status:
|
||||
deploy_status[log.domain_id] = log.status == "success"
|
||||
|
||||
return [
|
||||
{
|
||||
"id": d.id,
|
||||
@@ -260,6 +270,7 @@ async def list_domains(db: AsyncSession = Depends(get_db), _: bool = Depends(req
|
||||
"reload_cmd": d.reload_cmd,
|
||||
"version": d.version,
|
||||
"cert_not_after": d.cert_not_after.isoformat() if d.cert_not_after else None,
|
||||
"deploy_success": deploy_status.get(d.id),
|
||||
}
|
||||
for d in domains
|
||||
]
|
||||
@@ -486,6 +497,10 @@ async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool
|
||||
if info:
|
||||
domain.cert_not_after = datetime.fromisoformat(info["not_after"])
|
||||
domain.version = str(int(domain.version or "0") + 1)
|
||||
logger.info(f"证书信息已更新: {domain.domain}, 过期时间: {info['not_after']}")
|
||||
else:
|
||||
logger.warning(f"无法获取证书信息: {domain.domain}")
|
||||
domain.version = str(int(domain.version or "0") + 1)
|
||||
|
||||
await db.flush()
|
||||
return {"success": success, "message": msg}
|
||||
|
||||
@@ -6,6 +6,7 @@ jinja2>=3.1.0
|
||||
pydantic>=2.9.0
|
||||
pydantic-settings>=2.5.0
|
||||
python-dotenv>=1.0.0
|
||||
apscheduler>=3.10.0
|
||||
|
||||
# ACME 相关
|
||||
acme>=2.10.0
|
||||
|
||||
Reference in New Issue
Block a user