From 09d0e2e03365bb0f45d51ff860ae2cb4f0bdc06f Mon Sep 17 00:00:00 2001 From: Niranjan Date: Tue, 7 Apr 2026 10:23:05 +0530 Subject: [PATCH] new changes --- .../app/api/__pycache__/ssl.cpython-314.pyc | Bin 6908 -> 9406 bytes YakPanel-server/backend/app/api/ssl.py | 82 ++++++++++++-- .../__pycache__/site_service.cpython-314.pyc | Bin 32648 -> 36883 bytes .../backend/app/services/site_service.py | 103 +++++++++++++++++- .../webserver/templates/nginx_site.conf | 9 +- 5 files changed, 177 insertions(+), 17 deletions(-) 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 3b55a964520a77efdebad945740f059a51585d4e..678fc7b3bc753690b7ea6d258fd30c3b6f69b79b 100644 GIT binary patch delta 4574 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 W-Vu>dy(DlvU0TOj^C}z1$A1BdD&2hm diff --git a/YakPanel-server/backend/app/api/ssl.py b/YakPanel-server/backend/app/api/ssl.py index e0f0f387..1cb82fd0 100644 --- a/YakPanel-server/backend/app/api/ssl.py +++ b/YakPanel-server/backend/app/api/ssl.py @@ -1,5 +1,7 @@ """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 @@ -7,14 +9,25 @@ from pydantic import BaseModel from app.core.database import get_db from app.core.config import get_runtime_config -from app.core.utils import exec_shell_sync +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), @@ -48,8 +61,9 @@ class RequestCertRequest(BaseModel): 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)""" + """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: @@ -59,14 +73,62 @@ async def ssl_request_cert( 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") - cmd = ( - f'certbot certonly --webroot -w "{body.webroot}" -d "{body.domain}" ' - f'--non-interactive --agree-tos --email "{body.email}"' - ) - out, err = exec_shell_sync(cmd, timeout=120) - if err and "error" in err.lower() and "successfully" not in err.lower(): - raise HTTPException(status_code=500, detail=err.strip() or out.strip()) - return {"status": True, "msg": "Certificate requested", "output": out} + + 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") 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 dbeb6e8b2c9b72e206850cf6642d2bf448258662..bb05cbcc275cd062b9a207cc162283c303db8997 100644 GIT binary patch delta 7058 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 QRgC@z^q&Rdy#vet4@Qh=lK=n! diff --git a/YakPanel-server/backend/app/services/site_service.py b/YakPanel-server/backend/app/services/site_service.py index 2287690b..c5c53fc0 100644 --- a/YakPanel-server/backend/app/services/site_service.py +++ b/YakPanel-server/backend/app/services/site_service.py @@ -83,6 +83,73 @@ def _best_ssl_for_hostnames(hostnames: list[str]) -> dict: return none +def _letsencrypt_paths(hostname: str) -> tuple[str, str] | None: + """Return (fullchain, privkey) if Let's Encrypt files exist for this hostname.""" + h = (hostname or "").strip().lower().split(":")[0] + if not h or ".." in h: + return None + base = os.path.join(LETSENCRYPT_LIVE, h) + fc = os.path.join(base, "fullchain.pem") + pk = os.path.join(base, "privkey.pem") + if os.path.isfile(fc) and os.path.isfile(pk): + return fc, pk + return None + + +def _build_ssl_server_block( + server_names: str, + root_path: str, + logs_path: str, + site_name: str, + php_version: str, + fullchain: str, + privkey: str, + redirects: list[tuple[str, str, int]] | None, +) -> str: + """Second server {} for HTTPS when LE certs exist.""" + pv = php_version or "74" + redirect_lines: list[str] = [] + for src, tgt, code in (redirects or []): + if src and tgt: + redirect_lines.append(f" location = {src} {{ return {code} {tgt}; }}") + redirect_block = ("\n" + "\n".join(redirect_lines)) if redirect_lines else "" + q_fc = fullchain.replace("\\", "\\\\").replace('"', '\\"') + q_pk = privkey.replace("\\", "\\\\").replace('"', '\\"') + return ( + f"server {{\n" + f" listen 443 ssl;\n" + f" server_name {server_names};\n" + f' ssl_certificate "{q_fc}";\n' + f' ssl_certificate_key "{q_pk}";\n' + f" index index.php index.html index.htm default.php default.htm default.html;\n" + f" root {root_path};\n" + f" error_page 404 /404.html;\n" + f" error_page 502 /502.html;\n" + f" location ^~ /.well-known/acme-challenge/ {{\n" + f' default_type "text/plain";\n' + f" allow all;\n" + f" try_files $uri =404;\n" + f" }}\n" + f"{redirect_block}\n" + r" location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {" + "\n" + f" expires 30d;\n" + f" access_log off;\n" + f" }}\n" + r" location ~ .*\.(js|css)?$ {" + "\n" + f" expires 12h;\n" + f" access_log off;\n" + f" }}\n" + r" location ~ \.php$ {" + "\n" + f" fastcgi_pass unix:/tmp/php-cgi-{pv}.sock;\n" + f" fastcgi_index index.php;\n" + f" include fastcgi.conf;\n" + f" }}\n" + f" access_log {logs_path}/{site_name}.log;\n" + f" error_log {logs_path}/{site_name}.error.log;\n" + f"}}\n" + ) + + def _render_vhost( template: str, server_names: str, @@ -92,14 +159,32 @@ def _render_vhost( php_version: str, force_https: int, redirects: list[tuple[str, str, int]] | None = None, + le_hostnames: list[str] | None = None, ) -> str: """Render nginx vhost template. redirects: [(source, target, code), ...]""" - force_block = "return 301 https://$host$request_uri;" if force_https else "" + if force_https: + force_block = ( + ' if ($request_uri !~ "^/.well-known/acme-challenge/") {\n' + " return 301 https://$host$request_uri;\n" + " }" + ) + else: + force_block = "" redirect_lines = [] for src, tgt, code in (redirects or []): if src and tgt: redirect_lines.append(f" location = {src} {{ return {code} {tgt}; }}") redirect_block = "\n".join(redirect_lines) if redirect_lines else "" + hosts = le_hostnames if le_hostnames is not None else [p for p in server_names.split() if p] + ssl_block = "" + for h in hosts: + le = _letsencrypt_paths(h) + if le: + fc, pk = le + ssl_block = _build_ssl_server_block( + server_names, root_path, logs_path, site_name, php_version, fc, pk, redirects + ) + break content = template.replace("{SERVER_NAMES}", server_names) content = content.replace("{ROOT_PATH}", root_path) content = content.replace("{LOGS_PATH}", logs_path) @@ -107,6 +192,7 @@ def _render_vhost( content = content.replace("{PHP_VERSION}", php_version or "74") content = content.replace("{FORCE_HTTPS_BLOCK}", force_block) content = content.replace("{REDIRECTS_BLOCK}", redirect_block) + content = content.replace("{SSL_SERVER_BLOCK}", ssl_block) return content @@ -183,7 +269,10 @@ async def create_site( if os.path.exists(template_path): template = read_file(template_path) or "" server_names = " ".join(d.split(":")[0] for d in domains) - content = _render_vhost(template, server_names, site_path, www_logs, name, php_version or "74", force_https or 0, []) + le_hosts = [d.split(":")[0] for d in domains] + content = _render_vhost( + template, server_names, site_path, www_logs, name, php_version or "74", force_https or 0, [], le_hosts + ) write_file(conf_path, content) # Reload Nginx if available @@ -337,7 +426,10 @@ async def update_site( fhttps = getattr(site, "force_https", 0) or 0 redir_result = await db.execute(select(SiteRedirect).where(SiteRedirect.site_id == site.id)) redirects = [(r.source, r.target, r.code or 301) for r in redir_result.scalars().all()] - content = _render_vhost(template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects) + le_hosts = [d.name for d in domain_rows] + content = _render_vhost( + template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts + ) write_file(conf_path, content) nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx") if os.path.exists(nginx_bin): @@ -411,7 +503,10 @@ async def regenerate_site_vhost(db: AsyncSession, site_id: int) -> dict: fhttps = getattr(site, "force_https", 0) or 0 redir_result = await db.execute(select(SiteRedirect).where(SiteRedirect.site_id == site.id)) redirects = [(r.source, r.target, r.code or 301) for r in redir_result.scalars().all()] - content = _render_vhost(template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects) + le_hosts = [d.name for d in domain_rows] + content = _render_vhost( + template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts + ) write_file(conf_path, content) nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx") if os.path.exists(nginx_bin): diff --git a/YakPanel-server/webserver/templates/nginx_site.conf b/YakPanel-server/webserver/templates/nginx_site.conf index f03593f8..d5e87dc1 100644 --- a/YakPanel-server/webserver/templates/nginx_site.conf +++ b/YakPanel-server/webserver/templates/nginx_site.conf @@ -8,12 +8,14 @@ server { error_page 404 /404.html; error_page 502 /502.html; - # ACME challenge - location ~ \.well-known { + # ACME HTTP-01 (Let's Encrypt). Prefix match wins over regex locations. + location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; allow all; + try_files $uri =404; } - # Force HTTPS redirect (when enabled) + # Force HTTPS (skipped for ACME — see if block) {FORCE_HTTPS_BLOCK} # Custom redirects @@ -39,3 +41,4 @@ server { access_log {LOGS_PATH}/{SITE_NAME}.log; error_log {LOGS_PATH}/{SITE_NAME}.error.log; } +{SSL_SERVER_BLOCK}