Archived
Polish KarlDNS production UI and docs
This commit is contained in:
1 parent
ca92dc211a
commit
5beee0e5c6
61 files changed
+1566
-523
No files matched your search
@@ -6,16 +6,17 @@ import re
|
||||
import secrets
|
||||
import sqlite3
|
||||
import threading
|
||||
import time
|
||||
import urllib.parse
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import HTTPException, Request
|
||||
|
||||
from .ddns_dns import DnsProviderError, delete_dns_records, dns_backend_configured, publish_dns
|
||||
from .ddns_dns import DnsProviderError, DnsReconciliationError, delete_dns_records, dns_backend_configured, publish_dns
|
||||
from .ddns_models import DdnsSettings, UpdateResult
|
||||
from .ddns_security import client_ip
|
||||
from .ddns_store import DdnsStore
|
||||
from .event_statuses import DNS_DELETE_FAILED, DNS_FAILED, UNCHANGED, UPDATED
|
||||
from .event_statuses import DNS_DELETE_FAILED, DNS_FAILED, DNS_RECONCILE_FAILED, UNCHANGED, UPDATED
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -121,36 +122,75 @@ class DdnsService:
|
||||
myipv6: str | None,
|
||||
ip6addr: str | None,
|
||||
) -> UpdateOutcome:
|
||||
started_at = time.perf_counter()
|
||||
is_success = False
|
||||
is_dns_failure = False
|
||||
is_reconcile_failure = False
|
||||
is_bad_ip = False
|
||||
source_ip = client_ip(request, self.settings)
|
||||
ipv4, ipv6, update_ipv4, update_ipv6 = parse_update_ips(
|
||||
source_ip,
|
||||
myip,
|
||||
ipaddr,
|
||||
myipv6,
|
||||
ip6addr,
|
||||
)
|
||||
lock = self._lock_for(hostname)
|
||||
with lock:
|
||||
planned = self.store.preview_update(hostname, ipv4, ipv6, update_ipv4=update_ipv4, update_ipv6=update_ipv6)
|
||||
if planned.changed:
|
||||
try:
|
||||
publish_dns(planned, self.settings)
|
||||
except DnsProviderError as exc:
|
||||
LOGGER.warning(
|
||||
"DNS publish failed for hostname=%s update_ipv4=%s update_ipv6=%s error_type=%s",
|
||||
hostname,
|
||||
planned.update_ipv4,
|
||||
planned.update_ipv6,
|
||||
type(exc).__name__,
|
||||
)
|
||||
self.store.log_update_event(hostname, ipv4, ipv6, DNS_FAILED, f"{type(exc).__name__}: {exc}", source_ip)
|
||||
raise
|
||||
result = self.store.commit_update_result(planned)
|
||||
self.store.log_update_event(hostname, result.ipv4, result.ipv6, UPDATED, "ok", source_ip)
|
||||
else:
|
||||
result = planned
|
||||
self.store.log_update_rollup(hostname, result.ipv4, result.ipv6, UNCHANGED, "ok", source_ip)
|
||||
return UpdateOutcome(result=result, source_ip=source_ip)
|
||||
try:
|
||||
ipv4, ipv6, update_ipv4, update_ipv6 = parse_update_ips(
|
||||
source_ip,
|
||||
myip,
|
||||
ipaddr,
|
||||
myipv6,
|
||||
ip6addr,
|
||||
)
|
||||
lock = self._lock_for(hostname)
|
||||
with lock:
|
||||
planned = self.store.preview_update(hostname, ipv4, ipv6, update_ipv4=update_ipv4, update_ipv6=update_ipv6)
|
||||
if planned.changed:
|
||||
try:
|
||||
publish_dns(planned, self.settings)
|
||||
except DnsReconciliationError as exc:
|
||||
is_reconcile_failure = True
|
||||
LOGGER.warning(
|
||||
"DNS reconciliation failed for hostname=%s update_ipv4=%s update_ipv6=%s error_type=%s",
|
||||
hostname,
|
||||
planned.update_ipv4,
|
||||
planned.update_ipv6,
|
||||
type(exc).__name__,
|
||||
)
|
||||
self.store.log_update_event(
|
||||
hostname,
|
||||
ipv4,
|
||||
ipv6,
|
||||
DNS_RECONCILE_FAILED,
|
||||
f"{type(exc).__name__}: {exc}",
|
||||
source_ip,
|
||||
)
|
||||
raise
|
||||
except DnsProviderError as exc:
|
||||
is_dns_failure = True
|
||||
LOGGER.warning(
|
||||
"DNS publish failed for hostname=%s update_ipv4=%s update_ipv6=%s error_type=%s",
|
||||
hostname,
|
||||
planned.update_ipv4,
|
||||
planned.update_ipv6,
|
||||
type(exc).__name__,
|
||||
)
|
||||
self.store.log_update_event(hostname, ipv4, ipv6, DNS_FAILED, f"{type(exc).__name__}: {exc}", source_ip)
|
||||
raise
|
||||
result = self.store.commit_update_result(planned)
|
||||
is_success = True
|
||||
self.store.log_update_event(hostname, result.ipv4, result.ipv6, UPDATED, "ok", source_ip)
|
||||
else:
|
||||
result = planned
|
||||
is_success = True
|
||||
self.store.log_update_rollup(hostname, result.ipv4, result.ipv6, UNCHANGED, "ok", source_ip)
|
||||
return UpdateOutcome(result=result, source_ip=source_ip)
|
||||
except BadIpError:
|
||||
is_bad_ip = True
|
||||
raise
|
||||
finally:
|
||||
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
||||
self.store.record_update_metrics(
|
||||
elapsed_ms,
|
||||
is_success=is_success,
|
||||
is_dns_failure=is_dns_failure,
|
||||
is_reconcile_failure=is_reconcile_failure,
|
||||
is_bad_ip=is_bad_ip,
|
||||
)
|
||||
|
||||
def fritz_update_url(self, account: dict[str, str]) -> str:
|
||||
base = self.settings.public_base_url.rstrip("/")
|
||||
@@ -275,14 +315,21 @@ def normalize_hostname(value: str, settings: DdnsSettings, expand_suffix: bool =
|
||||
suffix = settings.hostname_suffix.strip().lower().strip(".")
|
||||
if expand_suffix and suffix and "." not in hostname:
|
||||
hostname = f"{hostname}.{suffix}"
|
||||
label = r"[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?"
|
||||
if not re.fullmatch(rf"{label}(\.{label})+", hostname):
|
||||
if not _valid_domain_name(hostname):
|
||||
return ""
|
||||
return hostname
|
||||
|
||||
|
||||
def normalize_domain(value: str) -> str:
|
||||
return normalize_hostname(value, DdnsSettings(), expand_suffix=False)
|
||||
domain = value.strip().lower().rstrip(".")
|
||||
if not _valid_domain_name(domain):
|
||||
return ""
|
||||
return domain
|
||||
|
||||
|
||||
def _valid_domain_name(value: str) -> bool:
|
||||
label = r"[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?"
|
||||
return bool(re.fullmatch(rf"{label}(\.{label})+", value))
|
||||
|
||||
|
||||
def dns_txt_contains(name: str, expected: str) -> bool:
|
||||
|
||||
Reference in new issue
Block a user