Refactor UI assets and harden quality checks

This commit is contained in:
StasonJatham committed 2026-06-17 23:18:00 +02:00
1 parent 23ab466c52
commit 4ecc8eb874
62 files changed
+3314 -3206

No files matched your search

+24 -10
View File
@@ -55,7 +55,7 @@ def delete_dns_records(hostname: str, settings: DdnsSettings) -> None:
def dns_backend_configured(settings: DdnsSettings) -> bool:
validate_dns_provider_settings(settings)
provider = _normalized_provider(settings)
return provider == "cloudflare" or provider == "rfc2136" or bool(settings.rfc2136_server)
return _dns_backend_enabled(provider, settings)
def validate_dns_provider_settings(settings: DdnsSettings) -> None:
@@ -65,19 +65,33 @@ def validate_dns_provider_settings(settings: DdnsSettings) -> None:
if not settings.require_dns_provider:
return
if provider == "cloudflare":
if not settings.cloudflare_api_token or not settings.cloudflare_zone_id:
raise DnsConfigurationError(
"DDNS_CLOUDFLARE_API_TOKEN and DDNS_CLOUDFLARE_ZONE_ID are required when DDNS_DNS_PROVIDER=cloudflare"
)
_validate_cloudflare_settings(settings)
return
if provider == "rfc2136" or settings.rfc2136_server:
if not settings.rfc2136_server or not settings.rfc2136_zone:
raise DnsConfigurationError(
"DDNS_RFC2136_SERVER and DDNS_RFC2136_ZONE are required when RFC2136 DNS publishing is enabled"
)
if _dns_backend_enabled(provider, settings):
_validate_rfc2136_settings(settings)
return
raise DnsConfigurationError("DDNS_REQUIRE_DNS_PROVIDER is set but no DNS backend is configured")
def _dns_backend_enabled(provider: str, settings: DdnsSettings) -> bool:
return provider == "cloudflare" or provider == "rfc2136" or bool(settings.rfc2136_server)
def _validate_cloudflare_settings(settings: DdnsSettings) -> None:
if settings.cloudflare_api_token and settings.cloudflare_zone_id:
return
raise DnsConfigurationError(
"DDNS_CLOUDFLARE_API_TOKEN and DDNS_CLOUDFLARE_ZONE_ID are required when DDNS_DNS_PROVIDER=cloudflare"
)
def _validate_rfc2136_settings(settings: DdnsSettings) -> None:
if settings.rfc2136_server and settings.rfc2136_zone:
return
raise DnsConfigurationError(
"DDNS_RFC2136_SERVER and DDNS_RFC2136_ZONE are required when RFC2136 DNS publishing is enabled"
)
def _normalized_provider(settings: DdnsSettings) -> str:
return settings.dns_provider.strip().lower()
+10 -2
View File
@@ -78,15 +78,23 @@ def _cloudflare_request(
request = urllib.request.Request(url, data=body, method=method)
request.add_header("Authorization", f"Bearer {settings.cloudflare_api_token}")
request.add_header("Content-Type", "application/json")
parsed = _read_cloudflare_json(request)
_raise_for_cloudflare_error(parsed)
return parsed
def _read_cloudflare_json(request: urllib.request.Request) -> dict[str, Any]:
try:
with urllib.request.urlopen(request, timeout=10) as response:
parsed = json.loads(response.read().decode("utf-8"))
return cast(dict[str, Any], json.loads(response.read().decode("utf-8")))
except urllib.error.HTTPError as exc:
raise DnsProviderError(f"Cloudflare API returned HTTP {exc.code}") from exc
except urllib.error.URLError as exc:
raise DnsProviderError("Cloudflare API request failed") from exc
except json.JSONDecodeError as exc:
raise DnsProviderError("Cloudflare API returned invalid JSON") from exc
def _raise_for_cloudflare_error(parsed: dict[str, Any]) -> None:
if not parsed.get("success", False):
raise DnsProviderError("Cloudflare API returned an unsuccessful response")
return parsed
+89 -42
View File
@@ -1,67 +1,114 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Protocol
from ..models import UpdateResult
from ..settings import DdnsSettings
from .base import DnsProviderError
class _RcodeResponse(Protocol):
def rcode(self) -> int: ...
@dataclass(frozen=True, slots=True)
class _RecordUpdate:
ttl: int
record_type: str
value: str | None
enabled: bool
def publish_rfc2136(result: UpdateResult, settings: DdnsSettings) -> None:
if not settings.rfc2136_server:
return
import dns.exception
import dns.query
import dns.rcode
import dns.tsigkeyring
import dns.update
keyring = None
if settings.rfc2136_key_name and settings.rfc2136_key_secret:
keyring = dns.tsigkeyring.from_text({settings.rfc2136_key_name: settings.rfc2136_key_secret})
zone = settings.rfc2136_zone or ".".join(result.hostname.split(".")[1:])
update = dns.update.Update(zone, keyring=keyring)
relative = result.hostname.removesuffix("." + zone).rstrip(".") or "@"
if result.update_ipv4 and result.ipv4:
update.replace(relative, settings.ttl, "A", result.ipv4)
elif result.update_ipv4:
update.delete(relative, "A")
if result.update_ipv6 and result.ipv6:
update.replace(relative, settings.ttl, "AAAA", result.ipv6)
elif result.update_ipv6:
update.delete(relative, "AAAA")
try:
response = dns.query.tcp(update, settings.rfc2136_server, timeout=5)
except dns.exception.DNSException as exc:
raise DnsProviderError("RFC2136 DNS update failed") from exc
rcode = response.rcode()
if rcode != 0:
raise DnsProviderError(f"RFC2136 update failed: {dns.rcode.to_text(rcode)}")
update, relative = _rfc2136_update(result.hostname, settings)
_apply_update_records(update, relative, result, settings)
_send_rfc2136_update(update, settings.rfc2136_server, "update")
def delete_rfc2136_records(hostname: str, settings: DdnsSettings) -> None:
if not settings.rfc2136_server:
return
update, relative = _rfc2136_update(hostname, settings)
update.delete(relative, "A")
update.delete(relative, "AAAA")
_send_rfc2136_update(update, settings.rfc2136_server, "delete")
def _rfc2136_update(hostname: str, settings: DdnsSettings) -> tuple[Any, str]:
import dns.update
zone = _rfc2136_zone(hostname, settings)
update = dns.update.Update(zone, keyring=_rfc2136_keyring(settings))
return update, _rfc2136_relative_name(hostname, zone)
def _rfc2136_zone(hostname: str, settings: DdnsSettings) -> str:
return settings.rfc2136_zone or ".".join(hostname.split(".")[1:])
def _rfc2136_relative_name(hostname: str, zone: str) -> str:
hostname = hostname.rstrip(".")
zone = zone.rstrip(".")
if hostname == zone:
return "@"
return hostname.removesuffix("." + zone).rstrip(".") or "@"
def _rfc2136_keyring(settings: DdnsSettings) -> Any | None:
if not settings.rfc2136_key_name or not settings.rfc2136_key_secret:
return None
import dns.tsigkeyring
return dns.tsigkeyring.from_text({settings.rfc2136_key_name: settings.rfc2136_key_secret})
def _apply_update_records(update: Any, relative: str, result: UpdateResult, settings: DdnsSettings) -> None:
_apply_record_update(
update,
relative,
_RecordUpdate(ttl=settings.ttl, record_type="A", value=result.ipv4, enabled=result.update_ipv4),
)
_apply_record_update(
update,
relative,
_RecordUpdate(ttl=settings.ttl, record_type="AAAA", value=result.ipv6, enabled=result.update_ipv6),
)
def _apply_record_update(
update: Any,
relative: str,
record: _RecordUpdate,
) -> None:
if not record.enabled:
return
if record.value is None:
update.delete(relative, record.record_type)
return
update.replace(relative, record.ttl, record.record_type, record.value)
def _send_rfc2136_update(update: Any, server: str, operation: str) -> None:
import dns.exception
import dns.query
import dns.rcode
import dns.tsigkeyring
import dns.update
keyring = None
if settings.rfc2136_key_name and settings.rfc2136_key_secret:
keyring = dns.tsigkeyring.from_text({settings.rfc2136_key_name: settings.rfc2136_key_secret})
zone = settings.rfc2136_zone or ".".join(hostname.split(".")[1:])
update = dns.update.Update(zone, keyring=keyring)
relative = hostname.removesuffix("." + zone).rstrip(".") or "@"
update.delete(relative, "A")
update.delete(relative, "AAAA")
try:
response = dns.query.tcp(update, settings.rfc2136_server, timeout=5)
response = dns.query.tcp(update, server, timeout=5)
except dns.exception.DNSException as exc:
raise DnsProviderError("RFC2136 DNS delete failed") from exc
raise DnsProviderError(f"RFC2136 DNS {operation} failed") from exc
_raise_for_rfc2136_response(response, operation)
def _raise_for_rfc2136_response(response: _RcodeResponse, operation: str) -> None:
import dns.rcode
rcode = response.rcode()
if rcode != 0:
raise DnsProviderError(f"RFC2136 delete failed: {dns.rcode.to_text(rcode)}")
raise DnsProviderError(f"RFC2136 {operation} failed: {dns.rcode.to_text(dns.rcode.Rcode(rcode))}")