148 lines
4.7 KiB
Python
148 lines
4.7 KiB
Python
"""YakPanel - SSL/Domains API - Let's Encrypt via certbot"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from pydantic import BaseModel
|
|
|
|
from app.core.database import get_db
|
|
from app.core.config import get_runtime_config
|
|
from app.core.utils import environment_with_system_path
|
|
from app.api.auth import get_current_user
|
|
from app.models.user import User
|
|
from app.models.site import Site, Domain
|
|
from app.services.site_service import regenerate_site_vhost
|
|
|
|
router = APIRouter(prefix="/ssl", tags=["ssl"])
|
|
|
|
|
|
def _certbot_executable() -> str:
|
|
w = shutil.which("certbot")
|
|
if w:
|
|
return w
|
|
for p in ("/usr/bin/certbot", "/usr/local/bin/certbot"):
|
|
if os.path.isfile(p):
|
|
return p
|
|
return "certbot"
|
|
|
|
|
|
@router.get("/domains")
|
|
async def ssl_domains(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""List all domains from sites with site path for certbot webroot"""
|
|
result = await db.execute(
|
|
select(Domain, Site).join(Site, Domain.pid == Site.id).order_by(Domain.name)
|
|
)
|
|
rows = result.all()
|
|
return [
|
|
{
|
|
"id": d.id,
|
|
"name": d.name,
|
|
"port": d.port,
|
|
"site_id": s.id,
|
|
"site_name": s.name,
|
|
"site_path": s.path,
|
|
}
|
|
for d, s in rows
|
|
]
|
|
|
|
|
|
class RequestCertRequest(BaseModel):
|
|
domain: str
|
|
webroot: str
|
|
email: str
|
|
|
|
|
|
@router.post("/request")
|
|
async def ssl_request_cert(
|
|
body: RequestCertRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Request Let's Encrypt certificate via certbot (webroot challenge)."""
|
|
if not body.domain or not body.webroot or not body.email:
|
|
raise HTTPException(status_code=400, detail="domain, webroot and email required")
|
|
if ".." in body.domain or ".." in body.webroot:
|
|
raise HTTPException(status_code=400, detail="Invalid path")
|
|
cfg = get_runtime_config()
|
|
allowed = [os.path.abspath(cfg["www_root"]), os.path.abspath(cfg["setup_path"])]
|
|
webroot_abs = os.path.abspath(body.webroot)
|
|
if not any(webroot_abs.startswith(a + os.sep) or webroot_abs == a for a in allowed):
|
|
raise HTTPException(status_code=400, detail="Webroot must be under www_root or setup_path")
|
|
|
|
dom = body.domain.split(":")[0].strip()
|
|
certbot_bin = _certbot_executable()
|
|
cmd = [
|
|
certbot_bin,
|
|
"certonly",
|
|
"--webroot",
|
|
"-w",
|
|
body.webroot,
|
|
"-d",
|
|
dom,
|
|
"--non-interactive",
|
|
"--agree-tos",
|
|
"--email",
|
|
body.email,
|
|
"--preferred-challenges",
|
|
"http",
|
|
"--no-eff-email",
|
|
]
|
|
|
|
try:
|
|
proc = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=180,
|
|
env=environment_with_system_path(),
|
|
)
|
|
except FileNotFoundError:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="certbot not found. Install it (e.g. apt install certbot) and ensure it is on PATH.",
|
|
) from None
|
|
except subprocess.TimeoutExpired:
|
|
raise HTTPException(status_code=500, detail="certbot timed out (180s)") from None
|
|
|
|
if proc.returncode != 0:
|
|
msg = (proc.stderr or proc.stdout or "").strip() or f"certbot exited with code {proc.returncode}"
|
|
raise HTTPException(status_code=500, detail=msg[:8000])
|
|
|
|
result = await db.execute(select(Domain).where(Domain.name == dom).limit(1))
|
|
row = result.scalar_one_or_none()
|
|
if row:
|
|
regen = await regenerate_site_vhost(db, row.pid)
|
|
if not regen.get("status"):
|
|
return {
|
|
"status": True,
|
|
"msg": "Certificate issued but nginx vhost update failed: " + str(regen.get("msg", "")),
|
|
"output": (proc.stdout or "")[-2000:],
|
|
}
|
|
|
|
return {
|
|
"status": True,
|
|
"msg": "Certificate issued and nginx updated",
|
|
"output": (proc.stdout or "")[-2000:],
|
|
}
|
|
|
|
|
|
@router.get("/certificates")
|
|
async def ssl_list_certificates(current_user: User = Depends(get_current_user)):
|
|
"""List existing Let's Encrypt certificates"""
|
|
live_dir = "/etc/letsencrypt/live"
|
|
if not os.path.isdir(live_dir):
|
|
return {"certificates": []}
|
|
certs = []
|
|
for name in os.listdir(live_dir):
|
|
if name.startswith("."):
|
|
continue
|
|
path = os.path.join(live_dir, name)
|
|
if os.path.isdir(path) and os.path.isfile(os.path.join(path, "fullchain.pem")):
|
|
certs.append({"name": name, "path": path})
|
|
return {"certificates": sorted(certs, key=lambda x: x["name"])}
|