Archived
213 lines
7.9 KiB
Python
213 lines
7.9 KiB
Python
from __future__ import annotations
|
|
|
|
from ipaddress import ip_address
|
|
|
|
from .html_utils import SafeHtml, attr, escape, trusted
|
|
from .icons import _svg_icon
|
|
|
|
_EVENT_LABELS = {
|
|
"admin_hostname_deleted": "hostname deleted by admin",
|
|
"admin_management_url_rotated": "dashboard link rotated by admin",
|
|
"admin_update_url_rotated": "update URL rotated by admin",
|
|
"dashboard_hostname_activated": "hostname registered",
|
|
"dashboard_hostname_deleted": "hostname deleted",
|
|
"dashboard_password_changed": "dashboard password changed",
|
|
"dashboard_password_enabled": "dashboard password enabled",
|
|
"dashboard_password_removed": "dashboard password removed",
|
|
"dashboard_update_url_rotated": "update URL rotated",
|
|
"dns_delete_failed": "DNS delete failed",
|
|
"dns_failed": "DNS update failed",
|
|
"dns_reconcile_failed": "DNS reconciliation failed",
|
|
"unchanged": "unchanged",
|
|
"updated": "updated",
|
|
}
|
|
|
|
|
|
def _event_label(status: object) -> str:
|
|
value = str(status or "-")
|
|
return _EVENT_LABELS.get(value, value.replace("_", " "))
|
|
|
|
|
|
def _copy_row(label: str, value: str, sensitivity: str | None = None, *, mask_secret: bool = True) -> SafeHtml:
|
|
safe_label = escape(label)
|
|
safe_value = escape(value)
|
|
attr_label = attr(label)
|
|
attr_value = attr(value)
|
|
is_secret = bool(sensitivity and sensitivity.lower() == "secret")
|
|
should_mask = is_secret and mask_secret
|
|
value_field = (
|
|
f'<input class="copy-pill-value copy-value" type="password" value="{attr_value}" readonly data-secret-input>'
|
|
if should_mask
|
|
else f'<code class="copy-pill-value copy-value">{safe_value}</code>'
|
|
)
|
|
reveal_button = (
|
|
f'<button type="button" class="btn btn-outline-secondary rounded-pill copy-reveal-button" data-secret-toggle aria-label="Show {attr_label}">View</button>'
|
|
if should_mask
|
|
else ""
|
|
)
|
|
row_class = "copy-field copy-field-secret" if is_secret else "copy-field copy-field-public"
|
|
return trusted(f"""
|
|
<div class="{row_class}">
|
|
<div class="copy-pill">
|
|
<span class="copy-pill-label">{safe_label}</span>
|
|
{value_field}
|
|
{reveal_button}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-secondary rounded-pill copy-button" data-copy="{attr_value}" aria-label="Copy {attr_label}">
|
|
{_svg_icon("clipboard")}<span data-copy-label>Copy</span>{_svg_icon("check", "check-icon")}
|
|
</button>
|
|
</div>
|
|
""")
|
|
|
|
|
|
def _copy_button(label: str, value: str, *, style: str = "outline-primary") -> SafeHtml:
|
|
safe_label = escape(label)
|
|
safe_value = attr(value)
|
|
safe_style = attr(style)
|
|
return trusted(
|
|
f'<button type="button" class="btn btn-{safe_style} rounded-pill copy-button" data-copy="{safe_value}" '
|
|
f'aria-label="Copy {safe_label}">{_svg_icon("clipboard")}<span data-copy-label>{safe_label}</span>{_svg_icon("check", "check-icon")}</button>'
|
|
)
|
|
|
|
|
|
def _copy_icon_button(label: str, value: str) -> SafeHtml:
|
|
safe_label = escape(label)
|
|
safe_value = attr(value)
|
|
return trusted(
|
|
f'<button type="button" class="copy-icon-button" data-copy="{safe_value}" aria-label="Copy {safe_label}" title="Copy">'
|
|
f'{_svg_icon("clipboard")}<span class="visually-hidden" data-copy-label>Copy {safe_label}</span>{_svg_icon("check", "check-icon")}</button>'
|
|
)
|
|
|
|
|
|
def _copy_menu(label: str, items: list[tuple[str, str]]) -> SafeHtml:
|
|
safe_label = escape(label)
|
|
buttons = []
|
|
for item_label, item_value in items:
|
|
buttons.append(
|
|
f'<button type="button" class="copy-menu-item" data-copy="{attr(item_value)}" aria-label="Copy {attr(item_label)}">'
|
|
f'{_svg_icon("clipboard")}<span data-copy-label>{escape(item_label)}</span>{_svg_icon("check", "check-icon")}</button>'
|
|
)
|
|
return trusted(f"""
|
|
<details class="copy-menu">
|
|
<summary aria-label="{safe_label}" title="{safe_label}">
|
|
{_svg_icon("more", "more-icon")}
|
|
</summary>
|
|
<div class="copy-menu-popover" role="menu">
|
|
{''.join(buttons)}
|
|
</div>
|
|
</details>
|
|
""")
|
|
|
|
|
|
def _code_window(
|
|
commands: str, *, copy_label: str = "code", class_name: str = "", copy_value: str | None = None
|
|
) -> SafeHtml:
|
|
safe_class = f" {attr(class_name)}" if class_name else ""
|
|
code_lines = "\n".join(f"<code>{escape(line)}</code>" for line in commands.splitlines())
|
|
copy_value = copy_value if copy_value is not None else _code_copy_value(commands)
|
|
return trusted(f"""
|
|
<div class="terminal-card code-window{safe_class}">
|
|
{_copy_icon_button(copy_label, copy_value)}
|
|
{code_lines}
|
|
</div>
|
|
""")
|
|
|
|
|
|
def _code_copy_value(commands: str) -> str:
|
|
lines = commands.splitlines()
|
|
prompt_lines = [line[2:] for line in lines if line.startswith("$ ")]
|
|
if prompt_lines:
|
|
return "\n".join(prompt_lines)
|
|
return "\n".join(line[2:] if line.startswith("$ ") else line for line in lines)
|
|
|
|
|
|
def _example_update_ips(request_ip: str | None = None) -> tuple[str, str]:
|
|
ipv4 = "203.0.113.10"
|
|
ipv6 = "2001:db8::10"
|
|
if not request_ip:
|
|
return ipv4, ipv6
|
|
try:
|
|
parsed = ip_address(request_ip.strip())
|
|
except ValueError:
|
|
return ipv4, ipv6
|
|
if not parsed.is_global:
|
|
return ipv4, ipv6
|
|
if parsed.version == 4:
|
|
return str(parsed), ipv6
|
|
return ipv4, str(parsed)
|
|
|
|
|
|
def _manual_update_commands(update_url: str, request_ip: str | None = None) -> str:
|
|
update_base = update_url.split("?", 1)[0]
|
|
request_version, request_address = _known_request_ip(request_ip)
|
|
lines = []
|
|
if request_version in {None, 4}:
|
|
ipv4, _ = _example_update_ips(request_ip)
|
|
lines.append(f"curl -sS '{update_base}?myip={ipv4}'")
|
|
if request_version == 6:
|
|
lines.append(f"curl -sS '{update_base}?myipv6={request_address}'")
|
|
lines.append(f"curl -sS '{update_base}' # use request IP")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _known_request_ip(request_ip: str | None = None) -> tuple[int | None, str]:
|
|
if not request_ip:
|
|
return None, ""
|
|
try:
|
|
parsed = ip_address(request_ip.strip())
|
|
except ValueError:
|
|
return None, ""
|
|
if not parsed.is_global:
|
|
return None, ""
|
|
return parsed.version, str(parsed)
|
|
|
|
|
|
def _manual_update_panel(commands: str) -> SafeHtml:
|
|
return trusted(f"""
|
|
<div class="card card-body gap-3 mt-4">
|
|
<div>
|
|
<h3>Manual updates</h3>
|
|
<p class="section-copy">FRITZ!Box fills in the IP placeholders automatically. Scripts can use curl. If no IP is sent, KarlDNS uses the request IP.</p>
|
|
</div>
|
|
{_code_window(commands, copy_label="manual update commands", class_name="manual-code")}
|
|
</div>
|
|
""")
|
|
|
|
|
|
def _fritzbox_fields(
|
|
update_url: str,
|
|
hostname: str,
|
|
management_url: str | None = None,
|
|
) -> str:
|
|
lines = [
|
|
f"Update-URL: {update_url}",
|
|
f"Domainnamen: {hostname}",
|
|
f"CNAME target: {hostname}",
|
|
]
|
|
if management_url:
|
|
lines.append(f"Private dashboard: {management_url}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _secret_notice(*, include_registration: bool = False) -> SafeHtml:
|
|
secret_items = (
|
|
"Update URL, register URL, and dashboard link" if include_registration else "Update URL and dashboard link"
|
|
)
|
|
return trusted(f"""
|
|
<div class="alert alert-info my-3 py-2 small" role="note">
|
|
<strong>Public:</strong> Hostname and CNAME target can be shared.
|
|
<strong>Secret:</strong> {escape(secret_items)} can update or manage this hostname.
|
|
Secrets are hidden on screen. Copy still copies the real value.
|
|
</div>
|
|
""")
|
|
|
|
|
|
def _message_band(message: str | None) -> SafeHtml:
|
|
if not message:
|
|
return trusted("")
|
|
return trusted(f"""
|
|
<div class="alert alert-warning mb-0 rounded-0 border-start-0 border-end-0" role="status">
|
|
<div class="container">{escape(message)}</div>
|
|
</div>
|
|
""")
|