new changes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,6 @@
|
|||||||
"""YakPanel - SSL/Domains API - Let's Encrypt via certbot"""
|
"""YakPanel - SSL/Domains API - Let's Encrypt via certbot"""
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
@@ -11,11 +12,10 @@ from typing import Optional
|
|||||||
|
|
||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.core.config import get_runtime_config
|
from app.core.config import get_runtime_config
|
||||||
from app.core.utils import environment_with_system_path
|
from app.core.utils import environment_with_system_path, read_file, nginx_reload_all_known, nginx_binary_candidates
|
||||||
from app.api.auth import get_current_user
|
from app.api.auth import get_current_user
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.site import Site, Domain
|
from app.models.site import Site, Domain
|
||||||
from app.core.utils import exec_shell_sync
|
|
||||||
from app.services.site_service import regenerate_site_vhost
|
from app.services.site_service import regenerate_site_vhost
|
||||||
|
|
||||||
router = APIRouter(prefix="/ssl", tags=["ssl"])
|
router = APIRouter(prefix="/ssl", tags=["ssl"])
|
||||||
@@ -120,25 +120,9 @@ async def _le_hostnames_for_domain_row(db: AsyncSession, dom_row: Optional[Domai
|
|||||||
return out if out else ([primary] if primary else [])
|
return out if out else ([primary] if primary else [])
|
||||||
|
|
||||||
|
|
||||||
def _reload_panel_and_common_nginx() -> None:
|
def _reload_panel_and_common_nginx() -> tuple[bool, str]:
|
||||||
"""Reload nginx so new vhost (ACME path) is live before certbot HTTP-01."""
|
"""Reload nginx so new vhost (ACME path) is live before certbot HTTP-01."""
|
||||||
cfg = get_runtime_config()
|
return nginx_reload_all_known(timeout=60)
|
||||||
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")
|
@router.get("/domains")
|
||||||
@@ -199,7 +183,12 @@ async def ssl_request_cert(
|
|||||||
status_code=500,
|
status_code=500,
|
||||||
detail="Cannot refresh nginx vhost before certificate request: " + str(regen_pre.get("msg", "")),
|
detail="Cannot refresh nginx vhost before certificate request: " + str(regen_pre.get("msg", "")),
|
||||||
)
|
)
|
||||||
_reload_panel_and_common_nginx()
|
ok_ngx, err_ngx = _reload_panel_and_common_nginx()
|
||||||
|
if not ok_ngx:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500,
|
||||||
|
detail="Nginx test/reload failed before certificate request (fix config, then retry): " + err_ngx,
|
||||||
|
)
|
||||||
|
|
||||||
challenge_dir = os.path.join(webroot_norm, ".well-known", "acme-challenge")
|
challenge_dir = os.path.join(webroot_norm, ".well-known", "acme-challenge")
|
||||||
try:
|
try:
|
||||||
@@ -278,6 +267,118 @@ async def ssl_request_cert(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/diagnostics")
|
||||||
|
async def ssl_diagnostics(current_user: User = Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Help debug HTTP vs HTTPS: compares panel-written vhosts with what nginx -T actually loads.
|
||||||
|
ERR_CONNECTION_REFUSED on 443 usually means no listen 443 in the active nginx, or a firewall.
|
||||||
|
"""
|
||||||
|
cfg = get_runtime_config()
|
||||||
|
setup_abs = os.path.abspath((cfg.get("setup_path") or "").strip() or ".")
|
||||||
|
vhost_dir = os.path.join(setup_abs, "panel", "vhost", "nginx")
|
||||||
|
include_snippet = "include " + vhost_dir.replace(os.sep, "/") + "/*.conf;"
|
||||||
|
|
||||||
|
vhost_summaries: list[dict] = []
|
||||||
|
if os.path.isdir(vhost_dir):
|
||||||
|
try:
|
||||||
|
names = sorted(os.listdir(vhost_dir))
|
||||||
|
except OSError:
|
||||||
|
names = []
|
||||||
|
for fn in names:
|
||||||
|
if not fn.endswith(".conf") or fn.startswith("."):
|
||||||
|
continue
|
||||||
|
fp = os.path.join(vhost_dir, fn)
|
||||||
|
if not os.path.isfile(fp):
|
||||||
|
continue
|
||||||
|
body = read_file(fp) or ""
|
||||||
|
vhost_summaries.append({
|
||||||
|
"file": fn,
|
||||||
|
"has_listen_80": bool(re.search(r"\blisten\s+80\b", body)),
|
||||||
|
"has_listen_443": bool(re.search(r"\blisten\s+.*443", body)),
|
||||||
|
"has_ssl_directives": "ssl_certificate" in body,
|
||||||
|
})
|
||||||
|
|
||||||
|
any_vhost_443 = any(
|
||||||
|
v.get("has_listen_443") and v.get("has_ssl_directives") for v in vhost_summaries
|
||||||
|
)
|
||||||
|
effective_listen_443 = False
|
||||||
|
panel_include_in_effective_config = False
|
||||||
|
nginx_t_errors: list[str] = []
|
||||||
|
norm_vhost = vhost_dir.replace(os.sep, "/")
|
||||||
|
env = environment_with_system_path()
|
||||||
|
|
||||||
|
for ngx in nginx_binary_candidates():
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
[ngx, "-T"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=25,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as e:
|
||||||
|
nginx_t_errors.append(f"{ngx}: {e}")
|
||||||
|
continue
|
||||||
|
dump = (r.stdout or "") + (r.stderr or "")
|
||||||
|
if r.returncode != 0:
|
||||||
|
nginx_t_errors.append(f"{ngx}: " + (dump.strip()[:800] or f"-T exit {r.returncode}"))
|
||||||
|
continue
|
||||||
|
if re.search(r"\blisten\s+.*443", dump):
|
||||||
|
effective_listen_443 = True
|
||||||
|
if norm_vhost in dump or "panel/vhost/nginx" in dump:
|
||||||
|
panel_include_in_effective_config = True
|
||||||
|
|
||||||
|
hints: list[str] = []
|
||||||
|
if not os.path.isdir(vhost_dir):
|
||||||
|
hints.append(f"The panel vhost directory is missing ({vhost_dir}). Create a website in YakPanel first.")
|
||||||
|
elif not vhost_summaries:
|
||||||
|
hints.append("There are no .conf files under the panel nginx vhost directory.")
|
||||||
|
|
||||||
|
le_live = "/etc/letsencrypt/live"
|
||||||
|
le_present = False
|
||||||
|
if os.path.isdir(le_live):
|
||||||
|
try:
|
||||||
|
le_present = any(
|
||||||
|
n and not n.startswith(".")
|
||||||
|
for n in os.listdir(le_live)
|
||||||
|
)
|
||||||
|
except OSError:
|
||||||
|
le_present = False
|
||||||
|
|
||||||
|
if le_present and vhost_summaries and not any_vhost_443:
|
||||||
|
hints.append(
|
||||||
|
"Let's Encrypt certs exist on this server but panel vhosts do not include an HTTPS (listen 443 ssl) block. "
|
||||||
|
"Regenerate the vhost: edit the site and save, or use Request SSL again."
|
||||||
|
)
|
||||||
|
|
||||||
|
if any_vhost_443 and not effective_listen_443:
|
||||||
|
hints.append(
|
||||||
|
"Your panel .conf files define HTTPS, but nginx -T does not show any listen 443 — the daemon that handles traffic is not loading YakPanel vhosts. "
|
||||||
|
"Add the include line below inside http { } for that nginx (e.g. /etc/nginx/nginx.conf), then nginx -t && reload."
|
||||||
|
)
|
||||||
|
elif vhost_summaries and not panel_include_in_effective_config:
|
||||||
|
hints.append(
|
||||||
|
"If http://domain shows the default 'Welcome to nginx' page, stock nginx is answering and likely does not include YakPanel vhosts. "
|
||||||
|
"Add the include below (or symlink this directory into /etc/nginx/conf.d/)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if effective_listen_443:
|
||||||
|
hints.append(
|
||||||
|
"Loaded nginx configuration includes a 443 listener. If HTTPS still fails, open TCP port 443 on the OS firewall and cloud/VPS security group."
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"vhost_dir": vhost_dir,
|
||||||
|
"include_snippet": include_snippet,
|
||||||
|
"vhosts": vhost_summaries,
|
||||||
|
"any_vhost_listen_ssl": any_vhost_443,
|
||||||
|
"nginx_effective_listen_443": effective_listen_443,
|
||||||
|
"panel_vhost_path_in_nginx_t": panel_include_in_effective_config,
|
||||||
|
"nginx_t_probe_errors": nginx_t_errors,
|
||||||
|
"hints": hints,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/certificates")
|
@router.get("/certificates")
|
||||||
async def ssl_list_certificates(current_user: User = Depends(get_current_user)):
|
async def ssl_list_certificates(current_user: User = Depends(get_current_user)):
|
||||||
"""List existing Let's Encrypt certificates"""
|
"""List existing Let's Encrypt certificates"""
|
||||||
|
|||||||
Binary file not shown.
@@ -130,3 +130,86 @@ def exec_shell_sync(cmd: str, timeout: Optional[float] = None, cwd: Optional[str
|
|||||||
return "", "Timed out"
|
return "", "Timed out"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return "", str(e)
|
return "", str(e)
|
||||||
|
|
||||||
|
|
||||||
|
def nginx_test_and_reload(nginx_bin: str, timeout: float = 60.0) -> Tuple[bool, str]:
|
||||||
|
"""Run ``nginx -t`` then ``nginx -s reload``. Returns (success, error_message)."""
|
||||||
|
if not nginx_bin or not os.path.isfile(nginx_bin):
|
||||||
|
return True, ""
|
||||||
|
env = environment_with_system_path()
|
||||||
|
try:
|
||||||
|
t = subprocess.run(
|
||||||
|
[nginx_bin, "-t"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as e:
|
||||||
|
return False, str(e)
|
||||||
|
if t.returncode != 0:
|
||||||
|
err = (t.stderr or t.stdout or "").strip()
|
||||||
|
return False, err or f"nginx -t exited {t.returncode}"
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
[nginx_bin, "-s", "reload"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as e:
|
||||||
|
return False, str(e)
|
||||||
|
if r.returncode != 0:
|
||||||
|
err = (r.stderr or r.stdout or "").strip()
|
||||||
|
return False, err or f"nginx -s reload exited {r.returncode}"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def nginx_binary_candidates() -> list[str]:
|
||||||
|
"""Nginx binaries to operate on: panel-bundled first, then common system paths (deduped by realpath)."""
|
||||||
|
from app.core.config import get_runtime_config
|
||||||
|
|
||||||
|
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)
|
||||||
|
try:
|
||||||
|
seen.add(os.path.realpath(panel_ngx))
|
||||||
|
except OSError:
|
||||||
|
seen.add(panel_ngx)
|
||||||
|
for alt in ("/usr/sbin/nginx", "/usr/bin/nginx", "/usr/local/nginx/sbin/nginx"):
|
||||||
|
if not os.path.isfile(alt):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
rp = os.path.realpath(alt)
|
||||||
|
except OSError:
|
||||||
|
rp = alt
|
||||||
|
if rp in seen:
|
||||||
|
continue
|
||||||
|
binaries.append(alt)
|
||||||
|
seen.add(rp)
|
||||||
|
return binaries
|
||||||
|
|
||||||
|
|
||||||
|
def nginx_reload_all_known(timeout: float = 60.0) -> Tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Test and reload panel nginx (setup_path/nginx/sbin/nginx) and distinct system nginx
|
||||||
|
binaries so vhost changes apply regardless of which daemon serves sites.
|
||||||
|
"""
|
||||||
|
binaries = nginx_binary_candidates()
|
||||||
|
if not binaries:
|
||||||
|
return True, ""
|
||||||
|
errs: list[str] = []
|
||||||
|
ok_any = False
|
||||||
|
for ngx in binaries:
|
||||||
|
ok, err = nginx_test_and_reload(ngx, timeout=timeout)
|
||||||
|
if ok:
|
||||||
|
ok_any = True
|
||||||
|
else:
|
||||||
|
errs.append(f"{ngx}: {err}")
|
||||||
|
if ok_any:
|
||||||
|
return True, ""
|
||||||
|
return False, "; ".join(errs) if errs else "nginx reload failed for all candidates"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -8,13 +8,55 @@ from sqlalchemy import select
|
|||||||
from app.models.site import Site, Domain
|
from app.models.site import Site, Domain
|
||||||
from app.models.redirect import SiteRedirect
|
from app.models.redirect import SiteRedirect
|
||||||
from app.core.config import get_runtime_config, get_settings
|
from app.core.config import get_runtime_config, get_settings
|
||||||
from app.core.utils import path_safe_check, write_file, read_file, exec_shell_sync
|
from app.core.utils import path_safe_check, write_file, read_file, exec_shell_sync, nginx_reload_all_known
|
||||||
|
|
||||||
|
|
||||||
DOMAIN_REGEX = re.compile(r"^([\w\-\*]{1,100}\.){1,8}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$")
|
DOMAIN_REGEX = re.compile(r"^([\w\-\*]{1,100}\.){1,8}([\w\-]{1,24}|[\w\-]{1,24}\.[\w\-]{1,24})$")
|
||||||
|
|
||||||
LETSENCRYPT_LIVE = "/etc/letsencrypt/live"
|
LETSENCRYPT_LIVE = "/etc/letsencrypt/live"
|
||||||
SSL_EXPIRING_DAYS = 14
|
SSL_EXPIRING_DAYS = 14
|
||||||
|
_SAN_CACHE: dict[str, tuple[float, frozenset[str]]] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_hostname(h: str) -> str:
|
||||||
|
return (h or "").strip().lower().split(":")[0]
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_le_pairs_sorted() -> list[tuple[str, str]]:
|
||||||
|
if not os.path.isdir(LETSENCRYPT_LIVE):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
names = sorted(os.listdir(LETSENCRYPT_LIVE))
|
||||||
|
except OSError:
|
||||||
|
return []
|
||||||
|
out: list[tuple[str, str]] = []
|
||||||
|
for entry in names:
|
||||||
|
if entry.startswith(".") or ".." in entry:
|
||||||
|
continue
|
||||||
|
fc = os.path.join(LETSENCRYPT_LIVE, entry, "fullchain.pem")
|
||||||
|
pk = os.path.join(LETSENCRYPT_LIVE, entry, "privkey.pem")
|
||||||
|
if os.path.isfile(fc) and os.path.isfile(pk):
|
||||||
|
out.append((fc, pk))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _cert_san_names(fullchain: str) -> frozenset[str]:
|
||||||
|
try:
|
||||||
|
st = os.stat(fullchain)
|
||||||
|
mtime = st.st_mtime
|
||||||
|
except OSError:
|
||||||
|
return frozenset()
|
||||||
|
hit = _SAN_CACHE.get(fullchain)
|
||||||
|
if hit is not None and hit[0] == mtime:
|
||||||
|
return hit[1]
|
||||||
|
out, _err = exec_shell_sync(f'openssl x509 -in "{fullchain}" -noout -text', timeout=8)
|
||||||
|
names: set[str] = set()
|
||||||
|
if out:
|
||||||
|
for m in re.finditer(r"DNS:([^,\s\n]+)", out, flags=re.IGNORECASE):
|
||||||
|
names.add(m.group(1).strip().lower())
|
||||||
|
froz = frozenset(names)
|
||||||
|
_SAN_CACHE[fullchain] = (mtime, froz)
|
||||||
|
return froz
|
||||||
|
|
||||||
|
|
||||||
def _nginx_site_template_path() -> str | None:
|
def _nginx_site_template_path() -> str | None:
|
||||||
@@ -77,30 +119,40 @@ def _parse_cert_not_after(cert_path: str) -> datetime | None:
|
|||||||
|
|
||||||
|
|
||||||
def _best_ssl_for_hostnames(hostnames: list[str]) -> dict:
|
def _best_ssl_for_hostnames(hostnames: list[str]) -> dict:
|
||||||
"""Match LE certs by live/<domain>/fullchain.pem; pick longest validity."""
|
"""Pick the LE cert (live/ or SAN) that covers site hostnames with longest validity."""
|
||||||
none = {"status": "none", "days_left": None, "cert_name": None}
|
none = {"status": "none", "days_left": None, "cert_name": None}
|
||||||
live_root = LETSENCRYPT_LIVE
|
seen: set[str] = set()
|
||||||
|
want_list: list[str] = []
|
||||||
|
for host in hostnames:
|
||||||
|
n = _normalize_hostname(host)
|
||||||
|
if n and ".." not in n and n not in seen:
|
||||||
|
seen.add(n)
|
||||||
|
want_list.append(n)
|
||||||
|
if not want_list:
|
||||||
|
return none
|
||||||
|
want = set(want_list)
|
||||||
try:
|
try:
|
||||||
if not os.path.isdir(live_root):
|
if not os.path.isdir(LETSENCRYPT_LIVE):
|
||||||
return none
|
return none
|
||||||
best_days: int | None = None
|
best_days: int | None = None
|
||||||
best_name: str | None = None
|
best_name: str | None = None
|
||||||
for host in hostnames:
|
for fc, _pk in _iter_le_pairs_sorted():
|
||||||
h = (host or "").split(":")[0].strip().lower()
|
live_name = os.path.basename(os.path.dirname(fc)).lower()
|
||||||
if not h:
|
if live_name in want:
|
||||||
|
match_names = {live_name}
|
||||||
|
else:
|
||||||
|
match_names = want & _cert_san_names(fc)
|
||||||
|
if not match_names:
|
||||||
continue
|
continue
|
||||||
folder = os.path.join(live_root, h)
|
end = _parse_cert_not_after(fc)
|
||||||
if not os.path.isdir(folder):
|
|
||||||
continue
|
|
||||||
fullchain = os.path.join(folder, "fullchain.pem")
|
|
||||||
end = _parse_cert_not_after(fullchain)
|
|
||||||
if end is None:
|
if end is None:
|
||||||
continue
|
continue
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
days = int((end - now).total_seconds() // 86400)
|
days = int((end - now).total_seconds() // 86400)
|
||||||
|
pick = min(match_names)
|
||||||
if best_days is None or days > best_days:
|
if best_days is None or days > best_days:
|
||||||
best_days = days
|
best_days = days
|
||||||
best_name = h
|
best_name = pick
|
||||||
if best_days is None:
|
if best_days is None:
|
||||||
return none
|
return none
|
||||||
if best_days < 0:
|
if best_days < 0:
|
||||||
@@ -127,6 +179,31 @@ def _letsencrypt_paths(hostname: str) -> tuple[str, str] | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _letsencrypt_paths_any(hostnames: list[str]) -> tuple[str, str] | None:
|
||||||
|
"""First matching LE cert: exact live/<host>/, then live dir name, then SAN match."""
|
||||||
|
seen: set[str] = set()
|
||||||
|
want_ordered: list[str] = []
|
||||||
|
for h in hostnames:
|
||||||
|
n = _normalize_hostname(h)
|
||||||
|
if n and ".." not in n and n not in seen:
|
||||||
|
seen.add(n)
|
||||||
|
want_ordered.append(n)
|
||||||
|
if not want_ordered:
|
||||||
|
return None
|
||||||
|
want = set(want_ordered)
|
||||||
|
for n in want_ordered:
|
||||||
|
p = _letsencrypt_paths(n)
|
||||||
|
if p:
|
||||||
|
return p
|
||||||
|
for fc, pk in _iter_le_pairs_sorted():
|
||||||
|
live_name = os.path.basename(os.path.dirname(fc)).lower()
|
||||||
|
if live_name in want:
|
||||||
|
return fc, pk
|
||||||
|
if want & _cert_san_names(fc):
|
||||||
|
return fc, pk
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _build_ssl_server_block(
|
def _build_ssl_server_block(
|
||||||
server_names: str,
|
server_names: str,
|
||||||
root_path: str,
|
root_path: str,
|
||||||
@@ -307,13 +384,16 @@ async def create_site(
|
|||||||
)
|
)
|
||||||
write_file(conf_path, content)
|
write_file(conf_path, content)
|
||||||
|
|
||||||
# Reload Nginx if available
|
reload_ok, reload_err = nginx_reload_all_known()
|
||||||
nginx_bin = os.path.join(setup_path, "nginx", "sbin", "nginx")
|
|
||||||
if os.path.exists(nginx_bin):
|
|
||||||
exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")
|
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": True, "msg": "Site created", "id": site.id}
|
if reload_ok:
|
||||||
|
return {"status": True, "msg": "Site created", "id": site.id}
|
||||||
|
return {
|
||||||
|
"status": True,
|
||||||
|
"msg": f"Site created but nginx reload failed (HTTPS may not work): {reload_err}",
|
||||||
|
"id": site.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def list_sites(db: AsyncSession) -> list[dict]:
|
async def list_sites(db: AsyncSession) -> list[dict]:
|
||||||
@@ -364,12 +444,12 @@ async def delete_site(db: AsyncSession, site_id: int) -> dict:
|
|||||||
if os.path.exists(conf_path):
|
if os.path.exists(conf_path):
|
||||||
os.remove(conf_path)
|
os.remove(conf_path)
|
||||||
|
|
||||||
nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
|
reload_ok, reload_err = nginx_reload_all_known()
|
||||||
if os.path.exists(nginx_bin):
|
|
||||||
exec_shell_sync(f"{nginx_bin} -s reload")
|
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": True, "msg": "Site deleted"}
|
if reload_ok:
|
||||||
|
return {"status": True, "msg": "Site deleted"}
|
||||||
|
return {"status": True, "msg": f"Site deleted but nginx reload failed: {reload_err}"}
|
||||||
|
|
||||||
|
|
||||||
async def get_site_count(db: AsyncSession) -> int:
|
async def get_site_count(db: AsyncSession) -> int:
|
||||||
@@ -462,9 +542,10 @@ async def update_site(
|
|||||||
template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts
|
template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts
|
||||||
)
|
)
|
||||||
write_file(conf_path, content)
|
write_file(conf_path, content)
|
||||||
nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
|
reload_ok, reload_err = nginx_reload_all_known()
|
||||||
if os.path.exists(nginx_bin):
|
if not reload_ok:
|
||||||
exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")
|
await db.commit()
|
||||||
|
return {"status": False, "msg": f"Vhost updated but nginx test/reload failed: {reload_err}"}
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
return {"status": True, "msg": "Site updated"}
|
return {"status": True, "msg": "Site updated"}
|
||||||
@@ -503,9 +584,12 @@ async def set_site_status(db: AsyncSession, site_id: int, status: int) -> dict:
|
|||||||
site.status = status
|
site.status = status
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
nginx_bin = os.path.join(get_runtime_config()["setup_path"], "nginx", "sbin", "nginx")
|
reload_ok, reload_err = nginx_reload_all_known()
|
||||||
if os.path.exists(nginx_bin):
|
if not reload_ok:
|
||||||
exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")
|
return {
|
||||||
|
"status": False,
|
||||||
|
"msg": f"Site {'enabled' if status == 1 else 'disabled'} but nginx test/reload failed: {reload_err}",
|
||||||
|
}
|
||||||
|
|
||||||
return {"status": True, "msg": "Site " + ("enabled" if status == 1 else "disabled")}
|
return {"status": True, "msg": "Site " + ("enabled" if status == 1 else "disabled")}
|
||||||
|
|
||||||
@@ -543,7 +627,7 @@ async def regenerate_site_vhost(db: AsyncSession, site_id: int) -> dict:
|
|||||||
template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts
|
template, server_names, site.path, cfg["www_logs"], site.name, php_ver, fhttps, redirects, le_hosts
|
||||||
)
|
)
|
||||||
write_file(write_path, content)
|
write_file(write_path, content)
|
||||||
nginx_bin = os.path.join(cfg["setup_path"], "nginx", "sbin", "nginx")
|
reload_ok, reload_err = nginx_reload_all_known()
|
||||||
if os.path.exists(nginx_bin):
|
if not reload_ok:
|
||||||
exec_shell_sync(f"{nginx_bin} -t && {nginx_bin} -s reload")
|
return {"status": False, "msg": f"Vhost written but nginx test/reload failed: {reload_err}"}
|
||||||
return {"status": True, "msg": "Vhost regenerated"}
|
return {"status": True, "msg": "Vhost regenerated"}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -17,9 +17,21 @@ interface Certificate {
|
|||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SslDiagnostics {
|
||||||
|
vhost_dir: string
|
||||||
|
include_snippet: string
|
||||||
|
vhosts: { file: string; has_listen_80: boolean; has_listen_443: boolean; has_ssl_directives: boolean }[]
|
||||||
|
any_vhost_listen_ssl: boolean
|
||||||
|
nginx_effective_listen_443: boolean
|
||||||
|
panel_vhost_path_in_nginx_t: boolean
|
||||||
|
nginx_t_probe_errors: string[]
|
||||||
|
hints: string[]
|
||||||
|
}
|
||||||
|
|
||||||
export function DomainsPage() {
|
export function DomainsPage() {
|
||||||
const [domains, setDomains] = useState<Domain[]>([])
|
const [domains, setDomains] = useState<Domain[]>([])
|
||||||
const [certificates, setCertificates] = useState<Certificate[]>([])
|
const [certificates, setCertificates] = useState<Certificate[]>([])
|
||||||
|
const [diag, setDiag] = useState<SslDiagnostics | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [requesting, setRequesting] = useState<string | null>(null)
|
const [requesting, setRequesting] = useState<string | null>(null)
|
||||||
@@ -86,6 +98,34 @@ export function DomainsPage() {
|
|||||||
Request Let's Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain.
|
Request Let's Encrypt certificates for your site domains. Requires certbot and nginx configured for the domain.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{diag ? (
|
||||||
|
<div className="alert alert-info small mb-4">
|
||||||
|
<div className="fw-semibold mb-2">HTTPS / nginx check</div>
|
||||||
|
{diag.hints.length ? (
|
||||||
|
<ul className="mb-3 ps-3">
|
||||||
|
{diag.hints.map((h, i) => (
|
||||||
|
<li key={i} className="mb-1">
|
||||||
|
{h}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
<div className="text-secondary mb-1">Include YakPanel vhosts inside the <code>http</code> block of the nginx process that serves your sites:</div>
|
||||||
|
<code className="d-block user-select-all bg-body-secondary p-2 rounded small text-break">{diag.include_snippet}</code>
|
||||||
|
{diag.vhosts.length > 0 ? (
|
||||||
|
<div className="mt-2 text-secondary">
|
||||||
|
Panel configs scanned:{' '}
|
||||||
|
{diag.vhosts.map((v) => `${v.file}${v.has_listen_443 && v.has_ssl_directives ? ' (HTTPS block)' : ''}`).join(', ')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{diag.nginx_t_probe_errors.length > 0 ? (
|
||||||
|
<div className="mt-2 small text-danger">
|
||||||
|
nginx -T probe: {diag.nginx_t_probe_errors.join(' | ')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div className="row g-4">
|
<div className="row g-4">
|
||||||
<div className="col-lg-6">
|
<div className="col-lg-6">
|
||||||
<div className="card h-100">
|
<div className="card h-100">
|
||||||
|
|||||||
Reference in New Issue
Block a user