Prepare KarlDNS production deployment

This commit is contained in:
StasonJatham committed 2026-05-21 17:23:08 +02:00
1 parent 5a90bd9d87
commit ce5b4e0ff2
101 files changed
+3364 -2018

No files matched your search

+31 -45
View File
@@ -6,10 +6,10 @@ from pathlib import Path
import pytest
from fastapi.testclient import TestClient
import router_dyndns.ddns_service as ddns_service
from router_dyndns.ddns import DdnsSettings, DdnsStore, DnsProviderError, DnsReconciliationError, UpdateResult, make_app
from router_dyndns.ddns_dns import validate_dns_provider_settings
from router_dyndns.ddns_security import DASHBOARD_UNLOCK_COOKIE
import karldns.services.ddns as ddns_module
from karldns import DdnsSettings, DdnsStore, DnsProviderError, DnsReconciliationError, UpdateResult, make_app
from karldns.dns import validate_dns_provider_settings
from karldns.security import DASHBOARD_UNLOCK_COOKIE
def test_generated_hostname_is_not_an_update_secret(tmp_path: Path) -> None:
@@ -20,14 +20,11 @@ def test_generated_hostname_is_not_an_update_secret(tmp_path: Path) -> None:
client = TestClient(app)
response = client.get(
"/nic/update",
params={
"hostname": "public-target.ddns.example.net",
"myip": "203.0.113.9",
},
f"/api/v1/router/update/{'a' * 43}",
params={"hostname": "public-target.ddns.example.net", "myip": "203.0.113.9"},
)
assert response.status_code == 401
assert response.status_code == 404
assert store.list_records() == []
@@ -39,16 +36,13 @@ def test_generated_account_cannot_update_other_hostname(tmp_path: Path) -> None:
client = TestClient(app)
response = client.get(
"/nic/update",
params={
"hostname": "other.example.net",
"myip": "203.0.113.9",
"username": account["username"],
"password": account["password"],
},
f"/api/v1/router/update/{account['update_slug']}",
params={"hostname": "other.example.net", "myip": "203.0.113.9"},
)
assert response.status_code == 401
assert response.status_code == 200
assert store.list_records()[0]["hostname"] == "home.example.net"
assert store.get_account_by_hostname("other.example.net") is None
def test_bearer_link_routes_share_rate_limit_bucket(tmp_path: Path) -> None:
@@ -70,7 +64,7 @@ def test_bearer_link_routes_share_rate_limit_bucket(tmp_path: Path) -> None:
def test_redacted_access_log_masks_bearer_paths_and_query_strings(tmp_path: Path, caplog) -> None:
caplog.set_level(logging.INFO, logger="router_dyndns.access")
caplog.set_level(logging.INFO, logger="karldns.access")
app = make_app(
DdnsSettings(
database_path=tmp_path / "ddns.sqlite3",
@@ -81,30 +75,22 @@ def test_redacted_access_log_masks_bearer_paths_and_query_strings(tmp_path: Path
account = app.state.store.create_account("home.example.net", "router")
client = TestClient(app)
client.get(f"/u/{account['update_slug']}", params={"myip": "203.0.113.1"})
client.get(
"/nic/update",
params={
"hostname": account["hostname"],
"username": account["username"],
"password": account["password"],
"myip": "203.0.113.2",
},
)
client.get(f"/api/v1/router/update/{account['update_slug']}", params={"myip": "203.0.113.1"})
client.get(f"/api/v1/hostnames/{account['management_slug']}")
assert "/u/<secret>" in caplog.text
assert "/nic/update?<redacted>" in caplog.text
assert "/api/v1/router/update/<secret>" in caplog.text
assert "/api/v1/hostnames/<secret>" in caplog.text
assert account["update_slug"] not in caplog.text
assert account["password"] not in caplog.text
assert account["management_slug"] not in caplog.text
@pytest.mark.parametrize(
"path",
[
"/u/short",
"/api/v1/updates/short",
"/api/v1/router/update/short",
"/api/v1/router/register/short",
"/api/v1/hostnames/short",
"/m/short",
"/api/v1/management/short",
],
)
def test_secret_path_tokens_are_constrained(tmp_path: Path, path: str) -> None:
@@ -174,7 +160,7 @@ def test_secret_html_responses_are_not_cached(tmp_path: Path) -> None:
)
client = TestClient(app)
created = client.post("/magic", data={"username": "router"})
created = client.post("/")
assert created.status_code == 200
assert created.headers["cache-control"] == "no-store"
@@ -192,7 +178,7 @@ def test_oversized_request_body_is_rejected(tmp_path: Path) -> None:
)
client = TestClient(app)
response = client.post("/magic", data={"username": "x" * 2000})
response = client.post("/", data={"username": "x" * 2000})
assert response.status_code == 413
@@ -208,7 +194,7 @@ def test_invalid_utf8_form_body_is_rejected(tmp_path: Path) -> None:
client = TestClient(app)
response = client.post(
"/magic",
"/",
content=b"username=\xff",
headers={"content-type": "application/x-www-form-urlencoded"},
)
@@ -229,7 +215,7 @@ def test_too_many_form_fields_are_rejected(tmp_path: Path) -> None:
body = "&".join(f"k{index}=v" for index in range(101))
response = client.post(
"/magic",
"/",
content=body.encode("ascii"),
headers={"content-type": "application/x-www-form-urlencoded"},
)
@@ -249,8 +235,8 @@ def test_untrusted_forwarded_for_does_not_bypass_rate_limit(tmp_path: Path) -> N
)
client = TestClient(app)
first = client.post("/magic", data={"username": "alice"}, headers={"x-forwarded-for": "198.51.100.10"})
second = client.post("/magic", data={"username": "bob"}, headers={"x-forwarded-for": "198.51.100.11"})
first = client.post("/", headers={"x-forwarded-for": "198.51.100.10"})
second = client.post("/", headers={"x-forwarded-for": "198.51.100.11"})
assert first.status_code == 200
assert second.status_code == 429
@@ -260,8 +246,8 @@ def test_dns_delete_failure_is_logged_without_secret_management_slug(tmp_path: P
def fail_delete(hostname, settings):
raise DnsProviderError("dns down")
caplog.set_level(logging.WARNING, logger="router_dyndns.ddns_service")
monkeypatch.setattr(ddns_service, "delete_dns_records", fail_delete)
caplog.set_level(logging.WARNING, logger="karldns.services.ddns")
monkeypatch.setattr(ddns_module, "delete_dns_records", fail_delete)
database_path = tmp_path / "ddns.sqlite3"
store = DdnsStore(database_path)
account = store.create_account("home.example.net", "alice")
@@ -363,7 +349,7 @@ def test_required_rfc2136_provider_requires_server_and_zone(tmp_path: Path) -> N
def test_cloudflare_publish_reconciles_provider_state(monkeypatch) -> None:
import router_dyndns.dns.cloudflare as cloudflare
import karldns.dns.cloudflare as cloudflare
calls = {"find": 0}
@@ -394,7 +380,7 @@ def test_rfc2136_delete_checks_response_code(monkeypatch) -> None:
import dns.query
import dns.rcode
from router_dyndns.dns.rfc2136 import delete_rfc2136_records
from karldns.dns.rfc2136 import delete_rfc2136_records
class RefusedResponse:
def rcode(self) -> int: