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 678fc7b3..87744e90 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 1cb82fd0..cd78daac 100644 --- a/YakPanel-server/backend/app/api/ssl.py +++ b/YakPanel-server/backend/app/api/ssl.py @@ -2,6 +2,7 @@ import os import shutil import subprocess +import sys from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select @@ -17,15 +18,83 @@ from app.services.site_service import regenerate_site_vhost router = APIRouter(prefix="/ssl", tags=["ssl"]) +_CERTBOT_PATH_CANDIDATES = ( + "/usr/bin/certbot", + "/usr/local/bin/certbot", + "/snap/bin/certbot", +) -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" + +def _certbot_command() -> list[str] | None: + """Resolve argv prefix to run certbot: [binary] or [python, -m, certbot].""" + env = environment_with_system_path() + path_var = env.get("PATH", "") + + exe = getattr(sys, "executable", None) or "" + if exe and os.path.isfile(exe): + try: + r = subprocess.run( + [exe, "-m", "certbot", "--version"], + capture_output=True, + text=True, + timeout=20, + env=env, + ) + if r.returncode == 0: + return [exe, "-m", "certbot"] + except (FileNotFoundError, OSError, subprocess.TimeoutExpired): + pass + + tried: list[str] = [] + w = shutil.which("certbot", path=path_var) + if w and os.path.isfile(w): + tried.append(w) + + for p in _CERTBOT_PATH_CANDIDATES: + if p not in tried and os.path.isfile(p): + tried.append(p) + + for exe in tried: + try: + r = subprocess.run( + [exe, "--version"], + capture_output=True, + text=True, + timeout=15, + env=env, + ) + if r.returncode == 0: + return [exe] + except (FileNotFoundError, OSError, subprocess.TimeoutExpired): + continue + + for py_name in ("python3", "python"): + py = shutil.which(py_name, path=path_var) + if not py or not os.path.isfile(py): + continue + try: + r = subprocess.run( + [py, "-m", "certbot", "--version"], + capture_output=True, + text=True, + timeout=20, + env=env, + ) + if r.returncode == 0: + return [py, "-m", "certbot"] + except (FileNotFoundError, OSError, subprocess.TimeoutExpired): + continue + + return None + + +def _certbot_missing_message() -> str: + return ( + "certbot is not installed or not reachable from the panel process. " + "On the server, run one of: apt install certbot | dnf install certbot | yum install certbot | snap install certbot. " + "Alternatively: pip install certbot (panel can use python3 -m certbot). " + "If certbot is already installed, ensure /usr/bin is on PATH for the YakPanel service." + ) @router.get("/domains") @@ -75,9 +144,11 @@ async def ssl_request_cert( 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, + prefix = _certbot_command() + if not prefix: + raise HTTPException(status_code=500, detail=_certbot_missing_message()) + + cmd = prefix + [ "certonly", "--webroot", "-w", @@ -93,19 +164,17 @@ async def ssl_request_cert( "--no-eff-email", ] + env = environment_with_system_path() try: proc = subprocess.run( cmd, capture_output=True, text=True, timeout=180, - env=environment_with_system_path(), + env=env, ) 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 + 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 diff --git a/YakPanel-server/backend/requirements.txt b/YakPanel-server/backend/requirements.txt index 793f3d7e..0dee7631 100644 --- a/YakPanel-server/backend/requirements.txt +++ b/YakPanel-server/backend/requirements.txt @@ -20,6 +20,9 @@ python-dotenv>=1.0.0 redis>=5.0.0 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 + # Utils psutil>=5.9.0 croniter>=2.0.0