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 87744e90..228b2b25 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 cd78daac..c7ce1feb 100644 --- a/YakPanel-server/backend/app/api/ssl.py +++ b/YakPanel-server/backend/app/api/ssl.py @@ -14,6 +14,7 @@ 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.core.utils import exec_shell_sync from app.services.site_service import regenerate_site_vhost router = APIRouter(prefix="/ssl", tags=["ssl"]) @@ -97,6 +98,27 @@ def _certbot_missing_message() -> str: ) +def _reload_panel_and_common_nginx() -> None: + """Reload nginx so new vhost (ACME path) is live before certbot HTTP-01.""" + cfg = get_runtime_config() + seen: set[str] = set() + binaries: list[str] = [] + panel_ngx = os.path.join(cfg.get("setup_path") or "", "nginx", "sbin", "nginx") + if os.path.isfile(panel_ngx): + binaries.append(panel_ngx) + seen.add(os.path.realpath(panel_ngx)) + for alt in ("/usr/sbin/nginx", "/usr/bin/nginx", "/usr/local/nginx/sbin/nginx"): + if not os.path.isfile(alt): + continue + rp = os.path.realpath(alt) + if rp in seen: + continue + binaries.append(alt) + seen.add(rp) + for ngx in binaries: + exec_shell_sync(f'"{ngx}" -t && "{ngx}" -s reload', timeout=60) + + @router.get("/domains") async def ssl_domains( current_user: User = Depends(get_current_user), @@ -144,6 +166,25 @@ 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() + webroot_norm = webroot_abs.rstrip(os.sep) + + result_dom = await db.execute(select(Domain).where(Domain.name == dom).limit(1)) + dom_row = result_dom.scalar_one_or_none() + if dom_row: + regen_pre = await regenerate_site_vhost(db, dom_row.pid) + if not regen_pre.get("status"): + raise HTTPException( + status_code=500, + detail="Cannot refresh nginx vhost before certificate request: " + str(regen_pre.get("msg", "")), + ) + _reload_panel_and_common_nginx() + + challenge_dir = os.path.join(webroot_norm, ".well-known", "acme-challenge") + try: + os.makedirs(challenge_dir, mode=0o755, exist_ok=True) + except OSError as e: + raise HTTPException(status_code=500, detail=f"Cannot create ACME webroot directory: {e}") from e + prefix = _certbot_command() if not prefix: raise HTTPException(status_code=500, detail=_certbot_missing_message()) @@ -152,7 +193,7 @@ async def ssl_request_cert( "certonly", "--webroot", "-w", - body.webroot, + webroot_norm, "-d", dom, "--non-interactive", @@ -180,10 +221,13 @@ async def ssl_request_cert( 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]) + 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)." + ) + raise HTTPException(status_code=500, detail=(msg + hint)[:8000]) - result = await db.execute(select(Domain).where(Domain.name == dom).limit(1)) - row = result.scalar_one_or_none() + row = dom_row if row: regen = await regenerate_site_vhost(db, row.pid) if not regen.get("status"): diff --git a/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc b/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc index bb05cbcc..540cc0c5 100644 Binary files a/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc and b/YakPanel-server/backend/app/services/__pycache__/site_service.cpython-314.pyc differ diff --git a/YakPanel-server/backend/app/services/site_service.py b/YakPanel-server/backend/app/services/site_service.py index c5c53fc0..933bcf2b 100644 --- a/YakPanel-server/backend/app/services/site_service.py +++ b/YakPanel-server/backend/app/services/site_service.py @@ -126,9 +126,10 @@ def _build_ssl_server_block( f" error_page 404 /404.html;\n" f" error_page 502 /502.html;\n" f" location ^~ /.well-known/acme-challenge/ {{\n" + f" root {root_path};\n" f' default_type "text/plain";\n' f" allow all;\n" - f" try_files $uri =404;\n" + f" access_log off;\n" f" }}\n" f"{redirect_block}\n" r" location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {" + "\n" @@ -480,16 +481,17 @@ async def set_site_status(db: AsyncSession, site_id: int, status: int) -> dict: async def regenerate_site_vhost(db: AsyncSession, site_id: int) -> dict: - """Regenerate nginx vhost for a site (e.g. after redirect changes).""" + """Regenerate nginx vhost for a site (e.g. after redirect changes or before LE validation).""" result = await db.execute(select(Site).where(Site.id == site_id)) site = result.scalar_one_or_none() if not site: return {"status": False, "msg": "Site not found"} cfg = get_runtime_config() - vhost_path = os.path.join(cfg["setup_path"], "panel", "vhost", "nginx") - conf_path = os.path.join(vhost_path, f"{site.name}.conf") - if site.status != 1: - return {"status": True, "msg": "Site disabled, vhost not active"} + conf_path, disabled_path = _vhost_path(site.name) + if site.status == 1: + write_path = conf_path + else: + write_path = disabled_path if os.path.isfile(disabled_path) else conf_path panel_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) template_path = os.path.join(panel_root, "webserver", "templates", "nginx_site.conf") if not os.path.exists(template_path): @@ -507,7 +509,7 @@ async def regenerate_site_vhost(db: AsyncSession, site_id: int) -> dict: content = _render_vhost( template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts ) - write_file(conf_path, content) + write_file(write_path, content) nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx") if os.path.exists(nginx_bin): exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload") diff --git a/YakPanel-server/webserver/templates/nginx_site.conf b/YakPanel-server/webserver/templates/nginx_site.conf index d5e87dc1..62f9a373 100644 --- a/YakPanel-server/webserver/templates/nginx_site.conf +++ b/YakPanel-server/webserver/templates/nginx_site.conf @@ -8,11 +8,12 @@ server { error_page 404 /404.html; error_page 502 /502.html; - # ACME HTTP-01 (Let's Encrypt). Prefix match wins over regex locations. + # ACME HTTP-01 (Let's Encrypt). Prefix match beats regex; explicit root; no try_files so server error_page cannot mask failures. location ^~ /.well-known/acme-challenge/ { + root {ROOT_PATH}; default_type "text/plain"; allow all; - try_files $uri =404; + access_log off; } # Force HTTPS (skipped for ACME — see if block)