Archived
Allow registration before first IP update
This commit is contained in:
1 parent
9f5cee8a0e
commit
c19e5170a0
5 files changed
+22
-13
No files matched your search
@@ -335,7 +335,7 @@ Example:
|
|||||||
curl -sS -X POST https://karldns.de/api/v1/hostnames
|
curl -sS -X POST https://karldns.de/api/v1/hostnames
|
||||||
```
|
```
|
||||||
|
|
||||||
The create response includes `registration_url`, `update_url`, and `dashboard_url`. Save the returned secrets, then `POST` the registration URL before using the update URL. Registration publishes the initial DNS record from the request IP; scripts may also pass `myip` or `myipv6` on the registration URL. Pending hostnames that are never registered are cleaned up automatically.
|
The create response includes `registration_url`, `update_url`, and `dashboard_url`. Save the returned secrets, then `POST` the registration URL before using the update URL. Registration publishes an initial DNS record when a public request IP or explicit `myip`/`myipv6` value is available; otherwise the first router update publishes the record. Pending hostnames that are never registered are cleaned up automatically.
|
||||||
|
|
||||||
Use the dashboard secret to rotate the update URL:
|
Use the dashboard secret to rotate the update URL:
|
||||||
|
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ async def activate_dashboard(
|
|||||||
management_slug,
|
management_slug,
|
||||||
account,
|
account,
|
||||||
dashboard_csrf_token(management_slug, account),
|
dashboard_csrf_token(management_slug, account),
|
||||||
"Registration needs a public source IP. Try again from your home connection or use the API with myip.",
|
"The supplied IP address is invalid. Leave IP fields empty and let the router send the first update.",
|
||||||
client_ip(request, settings),
|
client_ip(request, settings),
|
||||||
),
|
),
|
||||||
status_code=400,
|
status_code=400,
|
||||||
|
|||||||
@@ -72,12 +72,14 @@ class DdnsService:
|
|||||||
ipaddr: str | None,
|
ipaddr: str | None,
|
||||||
myipv6: str | None,
|
myipv6: str | None,
|
||||||
ip6addr: str | None,
|
ip6addr: str | None,
|
||||||
) -> None:
|
) -> bool:
|
||||||
if not dns_backend_configured(self.settings):
|
if not dns_backend_configured(self.settings):
|
||||||
return
|
return False
|
||||||
source_ip = client_ip(request, self.settings)
|
source_ip = client_ip(request, self.settings)
|
||||||
source_fallback = source_ip if _is_global_ip(source_ip) else ""
|
source_fallback = source_ip if _is_global_ip(source_ip) else ""
|
||||||
try:
|
supplied_ip = any(value and value.strip() for value in (myip, ipaddr, myipv6, ip6addr))
|
||||||
|
if not supplied_ip and not source_fallback:
|
||||||
|
return False
|
||||||
ipv4, ipv6, update_ipv4, update_ipv6 = parse_update_ips(
|
ipv4, ipv6, update_ipv4, update_ipv6 = parse_update_ips(
|
||||||
source_fallback,
|
source_fallback,
|
||||||
myip,
|
myip,
|
||||||
@@ -85,8 +87,6 @@ class DdnsService:
|
|||||||
myipv6,
|
myipv6,
|
||||||
ip6addr,
|
ip6addr,
|
||||||
)
|
)
|
||||||
except BadIpError as exc:
|
|
||||||
raise BadIpError("registration needs a public source IP or myip/myipv6 parameter") from exc
|
|
||||||
lock = self._lock_for(hostname)
|
lock = self._lock_for(hostname)
|
||||||
with lock:
|
with lock:
|
||||||
planned = self.store.preview_update(hostname, ipv4, ipv6, update_ipv4=update_ipv4, update_ipv6=update_ipv6)
|
planned = self.store.preview_update(hostname, ipv4, ipv6, update_ipv4=update_ipv4, update_ipv6=update_ipv6)
|
||||||
@@ -115,6 +115,7 @@ class DdnsService:
|
|||||||
raise
|
raise
|
||||||
result = self.store.commit_update_result(planned)
|
result = self.store.commit_update_result(planned)
|
||||||
self.store.log_update_event(hostname, result.ipv4, result.ipv6, UPDATED, "initial dns registered", source_ip)
|
self.store.log_update_event(hostname, result.ipv4, result.ipv6, UPDATED, "initial dns registered", source_ip)
|
||||||
|
return True
|
||||||
|
|
||||||
def apply_update(
|
def apply_update(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None)
|
|||||||
<h2>Endpoints</h2>
|
<h2>Endpoints</h2>
|
||||||
<ol class="simple-steps text-start">
|
<ol class="simple-steps text-start">
|
||||||
<li><strong>POST /api/v1/hostnames.</strong> Create a pending hostname and private setup values. The response includes <code>registration_url</code>.</li>
|
<li><strong>POST /api/v1/hostnames.</strong> Create a pending hostname and private setup values. The response includes <code>registration_url</code>.</li>
|
||||||
<li><strong>GET or POST /api/v1/router/register/<karl_secret>.</strong> Register the hostname after secrets are saved. Uses the request IP, or <code>myip</code>/<code>myipv6</code>, for the initial DNS record.</li>
|
<li><strong>GET or POST /api/v1/router/register/<karl_secret>.</strong> Register the hostname after secrets are saved. Publishes an initial DNS record when a public request IP or explicit <code>myip</code>/<code>myipv6</code> value is available; otherwise the first router update publishes it.</li>
|
||||||
<li><strong>GET or POST /api/v1/router/update/<karl_secret>.</strong> Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>, <code>myipv6</code>, and <code>ip6addr</code>.</li>
|
<li><strong>GET or POST /api/v1/router/update/<karl_secret>.</strong> Router-friendly update endpoint. Supports <code>myip</code>, <code>ipaddr</code>, <code>myipv6</code>, and <code>ip6addr</code>.</li>
|
||||||
<li><strong>GET /api/v1/hostnames/<karl_secret>.</strong> Read hostname status and recent history.</li>
|
<li><strong>GET /api/v1/hostnames/<karl_secret>.</strong> Read hostname status and recent history.</li>
|
||||||
<li><strong>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
<li><strong>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong> Replace the private router update URL.</li>
|
||||||
|
|||||||
+13
-5
@@ -273,8 +273,9 @@ def test_registration_publishes_initial_dns_when_provider_is_configured(tmp_path
|
|||||||
assert published[0].ipv4 == "203.0.113.77"
|
assert published[0].ipv4 == "203.0.113.77"
|
||||||
|
|
||||||
|
|
||||||
def test_registration_requires_ip_when_provider_source_ip_is_not_public(tmp_path: Path, monkeypatch) -> None:
|
def test_registration_without_public_source_ip_waits_for_first_router_update(tmp_path: Path, monkeypatch) -> None:
|
||||||
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: None)
|
published = []
|
||||||
|
monkeypatch.setattr(ddns_module, "publish_dns", lambda result, settings: published.append(result))
|
||||||
app = make_app(
|
app = make_app(
|
||||||
DdnsSettings(
|
DdnsSettings(
|
||||||
database_path=tmp_path / "ddns.sqlite3",
|
database_path=tmp_path / "ddns.sqlite3",
|
||||||
@@ -289,12 +290,19 @@ def test_registration_requires_ip_when_provider_source_ip_is_not_public(tmp_path
|
|||||||
client = TestClient(app)
|
client = TestClient(app)
|
||||||
|
|
||||||
body = client.post("/api/v1/hostnames").json()
|
body = client.post("/api/v1/hostnames").json()
|
||||||
|
update_path = _path(body["update_url"])
|
||||||
registered = client.post(_path(body["registration_url"]))
|
registered = client.post(_path(body["registration_url"]))
|
||||||
status = client.get(f"/api/v1/hostnames/{body['dashboard_url'].rsplit('/', 1)[1]}")
|
status = client.get(f"/api/v1/hostnames/{body['dashboard_url'].rsplit('/', 1)[1]}")
|
||||||
|
update = client.get(update_path, params={"myip": "203.0.113.80"})
|
||||||
|
|
||||||
assert registered.status_code == 400
|
assert registered.status_code == 200
|
||||||
assert registered.text.startswith("badip registration needs a public source IP")
|
assert registered.text == "good registered"
|
||||||
assert status.json()["status"] == "pending"
|
assert status.json()["status"] == "active"
|
||||||
|
assert status.json()["ipv4"] is None
|
||||||
|
assert update.status_code == 200
|
||||||
|
assert update.text == "good 203.0.113.80"
|
||||||
|
assert len(published) == 1
|
||||||
|
assert published[0].ipv4 == "203.0.113.80"
|
||||||
|
|
||||||
|
|
||||||
def test_api_update_publishes_before_recording_ip(tmp_path: Path, monkeypatch) -> None:
|
def test_api_update_publishes_before_recording_ip(tmp_path: Path, monkeypatch) -> None:
|
||||||
|
|||||||
Reference in new issue
Block a user