Archived
Preserve the GitHub history and the newer deployed source from ct_134_karldns. Exclude runtime data, credentials, AppleDouble files and local backup copies. Make Gitea the canonical source for documentation, package metadata and project links.
310 lines
11 KiB
Python
310 lines
11 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from ..settings import DdnsSettings
|
|
from .components import _code_window, _example_update_ips, _message_band
|
|
from .html_utils import trusted
|
|
from .layout import PageOptions, _page, _top_nav
|
|
from .sections.public_home import _manual_updates_section, _public_home_hero, _setup_flow_section
|
|
from .templates_public_sections import _linux_installer_section
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class _ApiDocsExamples:
|
|
create: str
|
|
register: str
|
|
update: str
|
|
status: str
|
|
rotate: str
|
|
schema: str
|
|
|
|
|
|
def _render_public_home(
|
|
settings: DdnsSettings,
|
|
message: str | None,
|
|
request_ip: str | None = None,
|
|
) -> str:
|
|
base_url = settings.public_base_url.rstrip("/")
|
|
hostname_suffix = settings.hostname_suffix or "your KarlDNS suffix"
|
|
demo_update_url = f"{base_url}/api/v1/router/update/<secret>?myip=<ipaddr>"
|
|
example_ipv4, _ = _example_update_ips(request_ip)
|
|
create_demo_command = f"curl -sS '{base_url}/api/v1/router/update/<secret>?myip={example_ipv4}'"
|
|
create_demo = f"{create_demo_command}\ngood {example_ipv4}"
|
|
manual_demo = "\n".join(
|
|
[
|
|
"# explicit IPv4",
|
|
f"curl -sS '{base_url}/api/v1/router/update/<secret>?myip={example_ipv4}'",
|
|
"# source IP fallback",
|
|
f"curl -sS '{base_url}/api/v1/router/update/<secret>'",
|
|
]
|
|
)
|
|
message_html = _message_band(message) if message else ""
|
|
description = (
|
|
"KarlDNS is a free self-hosted Dynamic DNS service for FRITZ!Box routers, "
|
|
"generated KarlDNS hostnames, Linux cron jobs, and home networks."
|
|
)
|
|
return _page(
|
|
"KarlDNS",
|
|
trusted(f"""
|
|
<main id="main">
|
|
{_top_nav()}
|
|
{_public_home_hero(suffix=hostname_suffix, demo_update_url=demo_update_url)}
|
|
{message_html}
|
|
{_setup_flow_section(create_demo=create_demo, create_demo_command=create_demo_command)}
|
|
{_manual_updates_section(manual_demo=manual_demo)}
|
|
{_linux_installer_section(base_url)}
|
|
</main>
|
|
"""),
|
|
PageOptions(
|
|
description=description,
|
|
preload_logo=True,
|
|
canonical_url=f"{base_url}/",
|
|
image_url=f"{base_url}/social-preview.png",
|
|
image_alt="KarlDNS landing page with private Dynamic DNS update URL",
|
|
structured_data={
|
|
"@context": "https://schema.org",
|
|
"@type": "SoftwareApplication",
|
|
"name": "KarlDNS",
|
|
"applicationCategory": "NetworkingApplication",
|
|
"operatingSystem": "Linux",
|
|
"url": f"{base_url}/",
|
|
"description": description,
|
|
"image": f"{base_url}/social-preview.png",
|
|
"offers": {"@type": "Offer", "price": "0", "priceCurrency": "EUR"},
|
|
"featureList": [
|
|
"Dynamic DNS for FRITZ!Box routers",
|
|
"Accountless private update URLs",
|
|
"CNAME target for external DNS names",
|
|
"Linux cron updater",
|
|
],
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def _render_privacy_page(settings: DdnsSettings) -> str:
|
|
base_url = settings.public_base_url.rstrip("/")
|
|
description = (
|
|
"Privacy and legal information for KarlDNS, including stored hostnames, update links, "
|
|
"IP history, and cleanup behavior."
|
|
)
|
|
return _page(
|
|
"Legal and Privacy",
|
|
trusted(f"""
|
|
<main id="main">
|
|
{_top_nav()}
|
|
<section class="hero-band compact">
|
|
<div class="container text-center d-grid justify-items-center gap-3">
|
|
<p class="eyebrow">Legal and privacy</p>
|
|
<h1>Privacy basics.</h1>
|
|
<p class="lead">KarlDNS stores only the data needed to keep your hostname updated.</p>
|
|
</div>
|
|
</section>
|
|
<section class="section">
|
|
<div class="container narrow d-grid gap-4">
|
|
<div>
|
|
<h2>Data processed</h2>
|
|
<p class="section-copy">
|
|
KarlDNS stores hostnames, protected dashboard tokens, private update links,
|
|
recent IP history, timestamps, and service events.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h2>Secrets</h2>
|
|
<p class="section-copy">
|
|
Dashboard links, registration URLs, and update URLs are secrets.
|
|
Anyone with them may update or manage the hostname.
|
|
Dashboard lookup values are not stored as readable text.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h2>Logs and retention</h2>
|
|
<p class="section-copy">
|
|
KarlDNS keeps recent update events for troubleshooting. Cleanup removes unused hostnames.
|
|
Server logs may also include IP addresses and request details.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h2>Contact</h2>
|
|
<p class="section-copy">
|
|
For the public KarlDNS instance, contact Karl through
|
|
<a href="https://karl.fail" rel="noopener">karl.fail</a> or
|
|
<a href="https://karlcom.de" rel="noopener">karlcom.de</a>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
"""),
|
|
PageOptions(
|
|
description=description,
|
|
canonical_url=f"{base_url}/privacy",
|
|
image_url=f"{base_url}/social-preview.png",
|
|
image_alt="KarlDNS legal and privacy information",
|
|
structured_data={
|
|
"@context": "https://schema.org",
|
|
"@type": "WebPage",
|
|
"name": "KarlDNS Legal and Privacy",
|
|
"url": f"{base_url}/privacy",
|
|
"description": description,
|
|
"isPartOf": {
|
|
"@type": "WebSite",
|
|
"name": "KarlDNS",
|
|
"url": f"{base_url}/",
|
|
},
|
|
},
|
|
),
|
|
)
|
|
|
|
|
|
def _render_api_docs_page(settings: DdnsSettings, request_ip: str | None = None) -> str:
|
|
base_url = settings.public_base_url.rstrip("/")
|
|
example_ipv4, _ = _example_update_ips(request_ip)
|
|
examples = _api_docs_examples(base_url, example_ipv4)
|
|
description = (
|
|
"Small KarlDNS API guide for creating, activating, updating, reading, and deleting "
|
|
"generated Dynamic DNS hostnames."
|
|
)
|
|
return _page(
|
|
"KarlDNS API",
|
|
trusted(f"""
|
|
<main id="main">
|
|
{_top_nav()}
|
|
{_api_docs_hero()}
|
|
{_api_docs_content(examples)}
|
|
</main>
|
|
"""),
|
|
PageOptions(
|
|
description=description,
|
|
canonical_url=f"{base_url}/docs",
|
|
image_url=f"{base_url}/social-preview.png",
|
|
image_alt="KarlDNS API documentation",
|
|
),
|
|
)
|
|
|
|
|
|
def _api_docs_examples(base_url: str, example_ipv4: str) -> _ApiDocsExamples:
|
|
return _ApiDocsExamples(
|
|
create=f"curl -sS -X POST -H \"Content-Type: application/x-www-form-urlencoded\" {base_url}/api/v1/hostnames",
|
|
register=f"curl -sS -X POST -H \"Content-Type: application/x-www-form-urlencoded\" '{base_url}/api/v1/router/register/<karl_dashboard_secret>?myip={example_ipv4}'",
|
|
update=f"curl -sS '{base_url}/api/v1/router/update/<karl_update_secret>?myip={example_ipv4}'",
|
|
status=f"curl -sS '{base_url}/api/v1/hostnames/<karl_dashboard_secret>?history_limit=10'",
|
|
rotate=f"curl -sS -X POST -H \"Content-Type: application/x-www-form-urlencoded\" {base_url}/api/v1/hostnames/<karl_dashboard_secret>/rotate-update-url",
|
|
schema=f"curl -sS {base_url}/openapi.json",
|
|
)
|
|
|
|
|
|
def _api_docs_hero() -> str:
|
|
return """
|
|
<section class="hero-band compact">
|
|
<div class="container text-center d-grid justify-items-center gap-3">
|
|
<p class="eyebrow">API</p>
|
|
<h1>Small API surface.</h1>
|
|
<p class="lead">Create a hostname, register it, update it, and read its status.</p>
|
|
</div>
|
|
</section>
|
|
"""
|
|
|
|
|
|
def _api_docs_content(examples: _ApiDocsExamples) -> str:
|
|
return f"""
|
|
<section class="section section-compact">
|
|
<div class="container narrow d-grid gap-4">
|
|
{_api_endpoints_overview()}
|
|
{_api_example_block("Create", examples.create, "create command")}
|
|
{_api_example_block("Register", examples.register, "register command")}
|
|
{_api_example_block("Update", examples.update, "update command")}
|
|
{_api_status_block(examples.status)}
|
|
{_api_rotate_block(examples.rotate)}
|
|
{_api_schema_block(examples.schema)}
|
|
</div>
|
|
</section>
|
|
"""
|
|
|
|
|
|
def _api_endpoints_overview() -> str:
|
|
return """
|
|
<div class="api-endpoints">
|
|
<h2>Endpoints</h2>
|
|
<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>GET or POST /api/v1/router/register/<karl_secret>.</strong>
|
|
Register the hostname after secrets are saved. Publishes an initial DNS record
|
|
from a public request IP or explicit <code>myip</code>/<code>myipv6</code> value.
|
|
If no public IP is visible, publishes a temporary <code>192.0.2.1</code> A record
|
|
until the first router update.
|
|
</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>POST /api/v1/hostnames/<karl_secret>/rotate-update-url.</strong>
|
|
Replace the private router update URL.
|
|
</li>
|
|
<li>
|
|
<strong>DELETE /api/v1/hostnames/<karl_secret>.</strong>
|
|
Delete hostname and DNS records.
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
"""
|
|
|
|
|
|
def _api_example_block(title: str, command: str, copy_label: str) -> str:
|
|
return f"""
|
|
<div>
|
|
<h2>{title}</h2>
|
|
{_code_window(command, copy_label=copy_label, class_name="mt-3")}
|
|
</div>
|
|
"""
|
|
|
|
|
|
def _api_status_block(command: str) -> str:
|
|
return f"""
|
|
<div>
|
|
<h2>Status</h2>
|
|
<p class="section-copy">
|
|
Use the private dashboard secret to read current DNS state and recent updates
|
|
for only that hostname.
|
|
</p>
|
|
{_code_window(command, copy_label="status command", class_name="mt-3")}
|
|
</div>
|
|
"""
|
|
|
|
|
|
def _api_rotate_block(command: str) -> str:
|
|
return f"""
|
|
<div>
|
|
<h2>Rotate update URL</h2>
|
|
<p class="section-copy">
|
|
Use the private dashboard secret to replace the router update URL.
|
|
The old update URL stops working immediately.
|
|
</p>
|
|
{_code_window(command, copy_label="rotate command", class_name="mt-3")}
|
|
</div>
|
|
"""
|
|
|
|
|
|
def _api_schema_block(command: str) -> str:
|
|
return f"""
|
|
<div>
|
|
<h2>Schema</h2>
|
|
<p class="section-copy">
|
|
Machine-readable OpenAPI is available for the public hostname and router endpoints.
|
|
</p>
|
|
{_code_window(command, copy_label="schema command", class_name="mt-3")}
|
|
</div>
|
|
"""
|