fix: ECC cert support, ACME deploy hook path, NAT detection, and account config fallback

- Add find_cert_dir() to resolve both RSA and ECC (domain_ecc/) cert dirs
- Copy acme deploy hook to /deploy/ where acme.sh resolves it
- _parse_account_conf checks both legacy .account.conf and declarative config
- Skip public DNS check when all local IPs are private (NAT)
- Improve check message strings for validity and expiry status
- Support timezone-aware date formats in _days_until parsing
- Filter out "no" SAN domains in cert listing
- Bump frontend asset version cache keys
- Fix DOMContentLoaded race condition in app.js boot
- Fix spread operator in certs.js modal template
This commit is contained in:
2026-06-27 14:23:40 +00:00
parent 398831b6e2
commit 8feb56faf6
12 changed files with 250 additions and 105 deletions
+52 -12
View File
@@ -18,7 +18,9 @@ logger = logging.getLogger(__name__)
PROJECT_DIR = Path(__file__).resolve().parent.parent
_ACME_HOME = PROJECT_DIR / "data" / "acme"
_DEPLOY_HOOK = str(PROJECT_DIR / "system" / "acme-deploy.sh")
# acme.sh resolves deploy hooks from $ACME_HOME/deploy/ -- _findHook
# only searches the deploy subdirectory, never accepts absolute paths.
_DEPLOY_HOOK = "acme-deploy.sh"
_ACME_ENVIRON = {
"HOME": str(PROJECT_DIR),
@@ -157,10 +159,12 @@ def _read_acme_email() -> str:
except OSError as exc:
logger.warning("Could not read account config: %s", exc)
# Fallback: read from declarative ACME config
# Derive project root from acme_home (acme_home is at <root>/data/acme).
try:
from lib.common import load_json
acme_cfg = PROJECT_DIR / "config" / "acme" / "config.json"
project_root = acme_home.parent.parent
acme_cfg = project_root / "config" / "acme" / "config.json"
conf = load_json(acme_cfg)
if conf and "email" in conf:
return conf["email"]
@@ -257,10 +261,12 @@ def list_certs() -> list[dict]:
continue
san_domains = [
d.strip() for d in entry.get("san_domains", "").split(",") if d.strip()
d.strip()
for d in entry.get("san_domains", "").split(",")
if d.strip() and d.strip().lower() != "no"
]
cert_dir = acme_home / main
cert_dir = find_cert_dir(main, acme_home)
cert_path = str(cert_dir / "fullchain.cer")
key_path = str(cert_dir / f"{main}.key")
ca_path = str(cert_dir / "ca.cer")
@@ -390,6 +396,34 @@ def copy_cert(domain: str, dest_dir: str) -> dict:
}
def find_cert_dir(domain: str, acme_home: Path | None = None) -> Path:
"""Find the certificate directory for a domain.
acme.sh may name the directory {domain}/ (RSA) or {domain}_ecc/ (ECC).
Checks both and returns whichever exists. Falls back to {domain}/ if
neither exists (preserves original behaviour for forward compatibility).
Args:
domain: The domain name.
acme_home: Override ACME home directory. Defaults to _ACME_HOME.
Returns:
Path to the directory containing the cert files.
"""
if acme_home is None:
acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
acme_home = Path(acme_home_env)
ecc_dir = acme_home / f"{domain}_ecc"
rsa_dir = acme_home / domain
if ecc_dir.is_dir():
return ecc_dir
if rsa_dir.is_dir():
return rsa_dir
return rsa_dir
def get_cert_paths(domain: str) -> dict:
"""Return the file paths for all certificate components.
@@ -399,13 +433,12 @@ def get_cert_paths(domain: str) -> dict:
Returns:
Dict with keys 'cert', 'key', 'ca', 'fullchain' mapped to paths.
"""
acme_home_env = os.environ.get("ACME_HOME", str(_ACME_HOME))
acme_home = str(Path(acme_home_env) / domain)
cert_dir = str(find_cert_dir(domain))
return {
"cert": f"{acme_home}/{domain}.cert",
"key": f"{acme_home}/{domain}.key",
"ca": f"{acme_home}/ca.cer",
"fullchain": f"{acme_home}/fullchain.cer",
"cert": f"{cert_dir}/{domain}.cert",
"key": f"{cert_dir}/{domain}.key",
"ca": f"{cert_dir}/ca.cer",
"fullchain": f"{cert_dir}/fullchain.cer",
}
@@ -497,9 +530,15 @@ def _days_until(date_str: str) -> int | None:
"""Parse an ISO date string and return days until that date from now."""
if not date_str:
return None
for fmt in ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%S"):
for fmt in (
"%Y-%m-%d",
"%Y-%m-%dT%H:%M:%SZ",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%S%z",
):
try:
dt = datetime.strptime(date_str, fmt).replace(tzinfo=UTC)
dt = datetime.strptime(date_str, fmt)
dt = dt.replace(tzinfo=UTC) if dt.tzinfo is None else dt.astimezone(UTC)
delta = dt - datetime.now(UTC)
return delta.days
except ValueError:
@@ -529,6 +568,7 @@ __all__ = [
"copy_cert",
"days_until_expiry",
"deploy",
"find_cert_dir",
"get_cert_info",
"get_cert_paths",
"get_email",