new changes
This commit is contained in:
@@ -5,6 +5,7 @@ import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
@@ -403,9 +404,42 @@ async def ssl_diagnostics(current_user: User = Depends(get_current_user)):
|
||||
"127.0.0.1:443 accepts TCP, but nginx -T from panel binaries did not show listen 443 — another process may own 443; check ss -tlnp and which nginx serves port 80."
|
||||
)
|
||||
|
||||
debian_sites = os.path.isdir("/etc/nginx/sites-available")
|
||||
rhel_conf = os.path.isdir("/etc/nginx/conf.d")
|
||||
layout = "unknown"
|
||||
if debian_sites:
|
||||
layout = "debian_sites_available"
|
||||
elif rhel_conf:
|
||||
layout = "rhel_conf_d"
|
||||
drop_deb = "/etc/nginx/sites-available/yakpanel-vhosts.conf"
|
||||
drop_rhel = "/etc/nginx/conf.d/yakpanel-vhosts.conf"
|
||||
nginx_wizard = {
|
||||
"detected_layout": layout,
|
||||
"include_snippet": include_snippet,
|
||||
"dropin_file_suggested": drop_deb if debian_sites else drop_rhel,
|
||||
"debian": {
|
||||
"sites_available_file": drop_deb,
|
||||
"sites_enabled_symlink": "/etc/nginx/sites-enabled/yakpanel-vhosts.conf",
|
||||
"steps": [
|
||||
f"printf '%s\\n' '{include_snippet}' | sudo tee {drop_deb}",
|
||||
f"sudo ln -sf {drop_deb} /etc/nginx/sites-enabled/yakpanel-vhosts.conf",
|
||||
"sudo nginx -t && sudo systemctl reload nginx",
|
||||
],
|
||||
},
|
||||
"rhel": {
|
||||
"conf_d_file": drop_rhel,
|
||||
"steps": [
|
||||
f"printf '%s\\n' '{include_snippet}' | sudo tee {drop_rhel}",
|
||||
"sudo nginx -t && sudo systemctl reload nginx",
|
||||
],
|
||||
},
|
||||
"note": "Run the steps for your distro as root. The include line must appear inside the main http { } context (conf.d files do automatically).",
|
||||
}
|
||||
|
||||
return {
|
||||
"vhost_dir": vhost_dir,
|
||||
"include_snippet": include_snippet,
|
||||
"nginx_wizard": nginx_wizard,
|
||||
"vhosts": vhost_summaries,
|
||||
"any_vhost_listen_ssl": any_vhost_443,
|
||||
"nginx_effective_listen_443": effective_listen_443,
|
||||
@@ -417,6 +451,132 @@ async def ssl_diagnostics(current_user: User = Depends(get_current_user)):
|
||||
}
|
||||
|
||||
|
||||
class DnsCertCloudflareRequest(BaseModel):
|
||||
domain: str
|
||||
email: str
|
||||
api_token: str
|
||||
|
||||
|
||||
class DnsManualInstructionsRequest(BaseModel):
|
||||
domain: str
|
||||
|
||||
|
||||
@router.post("/dns-request/cloudflare")
|
||||
async def ssl_dns_cloudflare_cert(
|
||||
body: DnsCertCloudflareRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Request Let's Encrypt certificate using DNS-01 via Cloudflare (requires certbot-dns-cloudflare)."""
|
||||
dom = (body.domain or "").split(":")[0].strip()
|
||||
if not dom or ".." in dom or not body.email or not body.api_token:
|
||||
raise HTTPException(status_code=400, detail="domain, email, and api_token required")
|
||||
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: " + str(regen_pre.get("msg", "")),
|
||||
)
|
||||
ok_ngx, err_ngx = _reload_panel_and_common_nginx()
|
||||
if not ok_ngx:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Nginx reload failed: " + err_ngx,
|
||||
)
|
||||
|
||||
prefix = _certbot_command()
|
||||
if not prefix:
|
||||
raise HTTPException(status_code=500, detail=_certbot_missing_message())
|
||||
|
||||
hostnames = await _le_hostnames_for_domain_row(db, dom_row, dom)
|
||||
if not hostnames:
|
||||
hostnames = [dom]
|
||||
cred_lines = f'dns_cloudflare_api_token = {body.api_token.strip()}\n'
|
||||
fd, cred_path = tempfile.mkstemp(suffix=".ini", prefix="yakpanel_cf_")
|
||||
try:
|
||||
os.write(fd, cred_lines.encode())
|
||||
os.close(fd)
|
||||
os.chmod(cred_path, 0o600)
|
||||
except OSError as e:
|
||||
try:
|
||||
os.close(fd)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=f"Cannot write credentials temp file: {e}") from e
|
||||
|
||||
base_flags = [
|
||||
"--non-interactive",
|
||||
"--agree-tos",
|
||||
"--email",
|
||||
body.email.strip(),
|
||||
"--no-eff-email",
|
||||
"--dns-cloudflare",
|
||||
"--dns-cloudflare-credentials",
|
||||
cred_path,
|
||||
]
|
||||
cmd = prefix + ["certonly"] + base_flags
|
||||
for h in hostnames:
|
||||
cmd.extend(["-d", h])
|
||||
env = environment_with_system_path()
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600, env=env)
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired) as e:
|
||||
try:
|
||||
os.unlink(cred_path)
|
||||
except OSError:
|
||||
pass
|
||||
raise HTTPException(status_code=500, detail=str(e)) from e
|
||||
finally:
|
||||
try:
|
||||
os.unlink(cred_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
if proc.returncode != 0:
|
||||
err = (proc.stderr or proc.stdout or "").strip() or f"exit {proc.returncode}"
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="certbot DNS failed. Install certbot-dns-cloudflare (pip or OS package) if missing. " + err[:6000],
|
||||
)
|
||||
|
||||
if dom_row:
|
||||
regen = await regenerate_site_vhost(db, dom_row.pid)
|
||||
if not regen.get("status"):
|
||||
return {
|
||||
"status": True,
|
||||
"msg": "Certificate issued but vhost regen failed: " + str(regen.get("msg", "")),
|
||||
"output": (proc.stdout or "")[-2000:],
|
||||
}
|
||||
|
||||
return {
|
||||
"status": True,
|
||||
"msg": "Certificate issued via Cloudflare DNS-01",
|
||||
"output": (proc.stdout or "")[-2000:],
|
||||
}
|
||||
|
||||
|
||||
@router.post("/dns-request/manual-instructions")
|
||||
async def ssl_dns_manual_instructions(
|
||||
body: DnsManualInstructionsRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Return TXT record host for ACME DNS-01 (user creates record then runs certbot --manual)."""
|
||||
d = (body.domain or "").split(":")[0].strip()
|
||||
if not d or ".." in d:
|
||||
raise HTTPException(status_code=400, detail="Invalid domain")
|
||||
return {
|
||||
"txt_record_name": f"_acme-challenge.{d}",
|
||||
"certbot_example": (
|
||||
f"sudo certbot certonly --manual --preferred-challenges dns --email you@example.com "
|
||||
f"--agree-tos -d {d}"
|
||||
),
|
||||
"note": "Certbot will display the exact TXT value to create. After DNS propagates, continue in the terminal.",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/certificates")
|
||||
async def ssl_list_certificates(current_user: User = Depends(get_current_user)):
|
||||
"""List existing Let's Encrypt certificates"""
|
||||
|
||||
Reference in New Issue
Block a user