diff --git a/YakPanel-server/backend/app/api/__pycache__/ssl.cpython-314.pyc b/YakPanel-server/backend/app/api/__pycache__/ssl.cpython-314.pyc index 228b2b25..c69cef0f 100644 Binary files a/YakPanel-server/backend/app/api/__pycache__/ssl.cpython-314.pyc and b/YakPanel-server/backend/app/api/__pycache__/ssl.cpython-314.pyc differ diff --git a/YakPanel-server/backend/app/api/ssl.py b/YakPanel-server/backend/app/api/ssl.py index c7ce1feb..53931dd6 100644 --- a/YakPanel-server/backend/app/api/ssl.py +++ b/YakPanel-server/backend/app/api/ssl.py @@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel +from typing import Optional from app.core.database import get_db from app.core.config import get_runtime_config @@ -98,6 +99,27 @@ def _certbot_missing_message() -> str: ) +async def _le_hostnames_for_domain_row(db: AsyncSession, dom_row: Optional[Domain], primary: str) -> list[str]: + """All distinct hostnames for the site (for -d flags). Falls back to primary.""" + if not dom_row: + return [primary] if primary else [] + result = await db.execute(select(Domain).where(Domain.pid == dom_row.pid).order_by(Domain.id)) + rows = result.scalars().all() + seen: set[str] = set() + out: list[str] = [] + for d in rows: + n = (d.name or "").strip() + if not n: + continue + key = n.lower() + if key not in seen: + seen.add(key) + out.append(n) + if primary and primary.lower() not in seen: + out.insert(0, primary) + return out if out else ([primary] if primary else []) + + def _reload_panel_and_common_nginx() -> None: """Reload nginx so new vhost (ACME path) is live before certbot HTTP-01.""" cfg = get_runtime_config() @@ -189,41 +211,53 @@ async def ssl_request_cert( if not prefix: raise HTTPException(status_code=500, detail=_certbot_missing_message()) - cmd = prefix + [ - "certonly", - "--webroot", - "-w", - webroot_norm, - "-d", - dom, + hostnames = await _le_hostnames_for_domain_row(db, dom_row, dom) + + base_flags = [ "--non-interactive", "--agree-tos", "--email", body.email, - "--preferred-challenges", - "http", "--no-eff-email", ] - env = environment_with_system_path() - try: - proc = subprocess.run( - cmd, - capture_output=True, - text=True, - timeout=180, - env=env, - ) - except FileNotFoundError: - raise HTTPException(status_code=500, detail=_certbot_missing_message()) from None - except subprocess.TimeoutExpired: - raise HTTPException(status_code=500, detail="certbot timed out (180s)") from None + cmd_webroot = prefix + ["certonly", "--webroot", "-w", webroot_norm, *base_flags] + for h in hostnames: + cmd_webroot.extend(["-d", h]) + cmd_webroot.extend(["--preferred-challenges", "http"]) - if proc.returncode != 0: - msg = (proc.stderr or proc.stdout or "").strip() or f"certbot exited with code {proc.returncode}" + cmd_nginx = prefix + ["certonly", "--nginx", *base_flags] + for h in hostnames: + cmd_nginx.extend(["-d", h]) + + env = environment_with_system_path() + proc: subprocess.CompletedProcess[str] | None = None + last_err = "" + for cmd, label in ((cmd_webroot, "webroot"), (cmd_nginx, "nginx")): + try: + proc = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=300, + env=env, + ) + except FileNotFoundError: + raise HTTPException(status_code=500, detail=_certbot_missing_message()) from None + except subprocess.TimeoutExpired: + raise HTTPException(status_code=500, detail="certbot timed out (300s)") from None + if proc.returncode == 0: + break + chunk = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}" + last_err = f"[{label}] {chunk}" + + if proc is None or proc.returncode != 0: + msg = last_err or "certbot failed" hint = ( - " Check: DNS A/AAAA for this domain points to this server; port 80 is reachable; " - "the website is enabled in YakPanel; nginx on port 80 loads this site’s vhost (same server as panel nginx if used)." + " Webroot and nginx plugins both failed. Check: " + "DNS A/AAAA for every -d name points to this server; port 80 reaches the nginx that serves these hosts; " + "site is enabled; install python3-certbot-nginx if the nginx method reports a missing plugin. " + "If you use a CDN proxy, pause it or use DNS validation instead." ) raise HTTPException(status_code=500, detail=(msg + hint)[:8000]) diff --git a/YakPanel-server/backend/requirements.txt b/YakPanel-server/backend/requirements.txt index 0dee7631..6b52c6c1 100644 --- a/YakPanel-server/backend/requirements.txt +++ b/YakPanel-server/backend/requirements.txt @@ -22,6 +22,7 @@ celery>=5.3.0 # Let's Encrypt (optional if system certbot/snap not used; enables python -m certbot from panel venv) certbot>=3.0.0 +certbot-nginx>=3.0.0 # Utils psutil>=5.9.0