Archived
Refactor UI assets and harden quality checks
This commit is contained in:
1 parent
23ab466c52
commit
4ecc8eb874
62 files changed
+3314
-3206
No files matched your search
+24
-10
@@ -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()
|
||||
@@ -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
@@ -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))}")
|
||||
Reference in new issue
Block a user