Ctrl K
ComunidadGuías…

src/webui/server_identity.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Stable identity of one DiceFrame server installation.

Clients that sync content with several servers need to tell those servers
apart. A base URL is not an identity: a LAN address or a tunnel host changes
while the server (its data directory) stays the same. The instance id is a
random value generated once and stored in the data directory, so it follows
the data rather than the host, and it reveals nothing about the machine.
"""

from __future__ import annotations

import json
import logging
import os
import re
import uuid
from pathlib import Path

from aiohttp import web

logger = logging.getLogger("trpg")

SERVER_IDENTITY_FILE = "server_identity.json"
#: Error code for clients when the id cannot be produced.
SERVER_IDENTITY_UNAVAILABLE = "SERVER_IDENTITY_UNAVAILABLE"
_INSTANCE_ID_PATTERN = re.compile(r"^srv-[0-9a-f]{32}$")


class ServerIdentityError(RuntimeError):
    """The stored identity exists but cannot be trusted: fail closed."""


class ServerIdentityStore:
    def __init__(self, data_dir: Path) -> None:
        self.path = Path(data_dir) / SERVER_IDENTITY_FILE
        self._instance_id: str | None = None
        # A damaged file stays damaged until an operator repairs it: remember
        # the failure instead of re-reading (and re-logging) on every request.
        self._failure: ServerIdentityError | None = None
        self.failure_logged = False

    def instance_id(self) -> str:
        """Return the persisted id, generating it on first use.

        A present but unreadable or malformed file is never replaced: a fresh
        id would silently make every client treat this server as a new one.
        """

        if self._instance_id is not None:
            return self._instance_id
        if self._failure is not None:
            raise self._failure
        try:
            self._instance_id = self._read() if self.path.exists() else self._create()
        except ServerIdentityError as exc:
            self._failure = exc
            raise
        return self._instance_id

    def _create(self) -> str:
        """Publish a new id atomically; whoever publishes first wins.

        The content is written to a private temporary file and then hard-linked
        to the final name. The link fails if the name already exists, so two
        processes starting together can never both win, and a reader never
        sees a half-written file.
        """

        instance_id = f"srv-{uuid.uuid4().hex}"
        self.path.parent.mkdir(parents=True, exist_ok=True)
        temporary = self.path.with_name(f".{self.path.name}.{uuid.uuid4().hex}.tmp")
        try:
            with open(temporary, "x", encoding="utf-8") as handle:
                handle.write(json.dumps({"version": 1, "instance_id": instance_id}))
                handle.flush()
                os.fsync(handle.fileno())
            try:
                os.link(temporary, self.path)
            except FileExistsError:
                return self._read()
        finally:
            temporary.unlink(missing_ok=True)
        return instance_id

    def _read(self) -> str:
        try:
            payload = json.loads(self.path.read_text(encoding="utf-8"))
        except (OSError, ValueError) as exc:
            raise ServerIdentityError("server identity file is unreadable") from exc
        if not isinstance(payload, dict) or payload.get("version") != 1:
            raise ServerIdentityError("server identity file has an unknown shape")
        instance_id = payload.get("instance_id")
        if not isinstance(instance_id, str) or not _INSTANCE_ID_PATTERN.fullmatch(instance_id):
            raise ServerIdentityError("server identity file holds an invalid id")
        return instance_id


SERVER_IDENTITY_KEY = web.AppKey("server_identity", ServerIdentityStore)


def server_instance_id(app: web.Application) -> str | None:
    """The instance id for responses; ``None`` (and a log) if it is unusable."""

    store = app.get(SERVER_IDENTITY_KEY)
    if store is None:
        return None
    try:
        return store.instance_id()
    except (ServerIdentityError, OSError):
        if not store.failure_logged:
            store.failure_logged = True
            logger.error("服务器实例标识不可用,已从响应中省略", exc_info=True)
        return None