This repository has been archived on 2026-10-28. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
karldns.de/karldns/routers/updates.py
T

200 lines
6.9 KiB
Python

from __future__ import annotations
import json
from urllib.parse import parse_qs
from fastapi import APIRouter, HTTPException, Query, Request
from fastapi.responses import PlainTextResponse
from starlette.concurrency import run_in_threadpool
from ..dependencies import ServiceDep, StoreDep, apply_update_response
from ..dns import DnsProviderError
from ..event_statuses import DASHBOARD_HOSTNAME_ACTIVATED
from ..security import client_ip
from ..services.ddns import BadIpError
from .path_params import ManagementSlugPath, UpdateSlugPath
router = APIRouter()
@router.get(
"/api/v1/router/register/{dashboard_secret}",
response_class=PlainTextResponse,
response_model=None,
operation_id="registerHostnameFromRouterGet",
tags=["router"],
)
async def register_hostname_get(
dashboard_secret: ManagementSlugPath,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None = Query(default=None),
ipaddr: str | None = Query(default=None),
myipv6: str | None = Query(default=None),
ip6addr: str | None = Query(default=None),
) -> str | PlainTextResponse:
return await _register_hostname(dashboard_secret, request, store, service, myip, ipaddr, myipv6, ip6addr)
@router.post(
"/api/v1/router/register/{dashboard_secret}",
response_class=PlainTextResponse,
response_model=None,
operation_id="registerHostnameFromRouterPost",
tags=["router"],
)
async def register_hostname_post(
dashboard_secret: ManagementSlugPath,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None = Query(default=None),
ipaddr: str | None = Query(default=None),
myipv6: str | None = Query(default=None),
ip6addr: str | None = Query(default=None),
) -> str | PlainTextResponse:
return await _register_hostname(dashboard_secret, request, store, service, myip, ipaddr, myipv6, ip6addr)
async def _register_hostname(
dashboard_secret: str,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None,
ipaddr: str | None,
myipv6: str | None,
ip6addr: str | None,
) -> str | PlainTextResponse:
myip, ipaddr, myipv6, ip6addr = await _ip_params_from_request(request, myip, ipaddr, myipv6, ip6addr)
account = await run_in_threadpool(store.get_account_by_management_slug, dashboard_secret)
if not account or account["disabled"]:
raise HTTPException(status_code=404, detail="not found")
hostname = str(account["hostname"])
if not service.hostname_is_managed(hostname):
raise HTTPException(status_code=404, detail="not found")
if not account["accepted_at"]:
try:
await run_in_threadpool(service.publish_initial_dns, request, hostname, myip, ipaddr, myipv6, ip6addr)
except BadIpError as exc:
return PlainTextResponse(f"badip {exc}", status_code=400)
except DnsProviderError:
return PlainTextResponse("911 dns registration failed", status_code=500)
activated = await run_in_threadpool(store.activate_account, hostname)
if not activated:
raise HTTPException(status_code=404, detail="not found")
await run_in_threadpool(
store.log_update_event,
hostname,
None,
None,
DASHBOARD_HOSTNAME_ACTIVATED,
"hostname registered",
client_ip(request, service.settings),
)
return "good registered"
@router.get(
"/api/v1/router/update/{update_slug}",
response_class=PlainTextResponse,
response_model=None,
operation_id="updateHostnameFromRouterGet",
tags=["router"],
)
async def update_hostname_get(
update_slug: UpdateSlugPath,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None = Query(default=None),
ipaddr: str | None = Query(default=None),
myipv6: str | None = Query(default=None),
ip6addr: str | None = Query(default=None),
) -> str | PlainTextResponse:
return await _update_hostname(update_slug, request, store, service, myip, ipaddr, myipv6, ip6addr)
@router.post(
"/api/v1/router/update/{update_slug}",
response_class=PlainTextResponse,
response_model=None,
operation_id="updateHostnameFromRouterPost",
tags=["router"],
)
async def update_hostname_post(
update_slug: UpdateSlugPath,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None = Query(default=None),
ipaddr: str | None = Query(default=None),
myipv6: str | None = Query(default=None),
ip6addr: str | None = Query(default=None),
) -> str | PlainTextResponse:
return await _update_hostname(update_slug, request, store, service, myip, ipaddr, myipv6, ip6addr)
async def _update_hostname(
update_slug: str,
request: Request,
store: StoreDep,
service: ServiceDep,
myip: str | None,
ipaddr: str | None,
myipv6: str | None,
ip6addr: str | None,
) -> str | PlainTextResponse:
myip, ipaddr, myipv6, ip6addr = await _ip_params_from_request(request, myip, ipaddr, myipv6, ip6addr)
account = await run_in_threadpool(store.get_account_by_slug, update_slug)
if not account or account["disabled"] or not account["accepted_at"]:
raise HTTPException(status_code=404, detail="not found")
if not service.hostname_is_managed(str(account["hostname"])):
raise HTTPException(status_code=404, detail="not found")
return await run_in_threadpool(apply_update_response, service, request, str(account["hostname"]), myip, ipaddr, myipv6, ip6addr)
async def _ip_params_from_request(
request: Request,
myip: str | None,
ipaddr: str | None,
myipv6: str | None,
ip6addr: str | None,
) -> tuple[str | None, str | None, str | None, str | None]:
if request.method != "POST":
return myip, ipaddr, myipv6, ip6addr
body_values = await _post_values(request)
return (
myip or body_values.get("myip"),
ipaddr or body_values.get("ipaddr"),
myipv6 or body_values.get("myipv6"),
ip6addr or body_values.get("ip6addr"),
)
async def _post_values(request: Request) -> dict[str, str]:
content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().lower()
body = await request.body()
if not body:
return {}
if content_type == "application/json":
try:
data = json.loads(body)
except json.JSONDecodeError:
return {}
if not isinstance(data, dict):
return {}
return {str(key): str(value) for key, value in data.items() if isinstance(value, str)}
if content_type == "application/x-www-form-urlencoded":
try:
form_body = body.decode("utf-8", errors="strict")
except UnicodeDecodeError:
return {}
try:
parsed = parse_qs(form_body, keep_blank_values=True, max_num_fields=8)
except ValueError:
return {}
return {key: values[0] for key, values in parsed.items() if values}
return {}