Selkies

CentralizedStreamServer

Supervisor that owns the aiohttp application and the streaming services.

Exactly one registered service (websockets or webrtc) is active at a time; switch_to_mode serializes transitions under a lock so capture is never owned twice. The supervisor also serves the control-plane API, the static frontend, file uploads/downloads, and — when HTTPS is enabled — hot-reloads certificates by rebuilding the listening site without restarting the app.

On Wayland the constructor brings the pixelflux compositor up before any capture or session app starts and mirrors its socket name (the external compositor's, in host-capture mode) into WAYLAND_DISPLAY, so every child spawned with a copied environment reaches it.

Attributes

attributesettings
= settings
attributeservices
= services or {}
attributecurrent_modeOptional[str]
= None
attributelock
= asyncio.Lock()
attributeactive_taskOptional[asyncio.Task]
= None
attributeappOptional[web.Application]
= None
attributerunnerOptional[web.AppRunner]
= None
attributesitesList[web.BaseSite]
= []
attributecert_watcherOptional[asyncio.Task]
= None
attributetokens_watcherOptional[asyncio.Task]
= None
attributessl_contextOptional[ssl.SSLContext]
= None
attributestatic_fs_pathstr
= ''
attributetransfer_cap
= TransferPacer(static_bps=(int(limit_mbps * 125000)))

The operator's static file-transfer cap, one bucket shared by every download and upload; inactive when unset.

attributedownload_pacer
= TransferPacer(adaptive=True)

The downloads' adaptive allowance, fed per-chunk verdicts from an UplinkGauge over the downloader's own session socket.

attributeupload_pacer
= TransferPacer(adaptive=True)

The uploads' adaptive allowance, fed the same way from the uploader's session socket. The two directions queue at opposite ends of the path, so one's verdicts say nothing about the other's rate.

attributeupload_dir
= pathlib.Path(os.path.expanduser(self.settings.file_manager_path)).resolve()
attributeprint_spool
= pathlib.Path(printing.spool_path(self.settings.print_spool_path)).resolve()
attributeprint_watcherOptional[printing.SpoolWatcher]
= None
attributeprint_queueOptional[printing.PrintQueue]
= None
attribute_recording_audioOptional[Any]
= None
attribute_chunked_uploadsDict[str, Dict[str, Any]]
= {}

In-flight chunked uploads by destination path: transfer id, next expected offset, .part path, last-activity stamp, and a busy flag that refuses interleaved writes to the same destination.

attributeweb_files_ctxOptional[tempfile.TemporaryDirectory]
= None
attribute_clients_presentbool
= False
attribute_client_hook_tasksSet[asyncio.Task]
= set()
attributeSTREAMING_MODE_WEBRTC
= 'webrtc'
attributeSTREAMING_MODE_WEBSOCKETS
= 'websockets'
attributeSTATIC_CONTENT_PATH
= 'selkies.selkies_web'
attributeMIME_TYPES
= {'html': 'text/html', 'js': 'text/javascript', 'css': 'text/css', 'json': 'application/json', 'png': 'image/png', 'jpg': 'image/jpeg', 'ico': 'image/x-icon', 'svg': 'image/svg+xml'}

Functions

constructor__init__(settings, services=None) -> None
Source Code
def __init__(
    self,
    settings: Any,
    services: Optional[Dict[str, BaseStreamingService]] = None,
) -> None:
    self.settings = settings
    self.services = services or {}
    self.current_mode: Optional[str] = None
    self.lock = asyncio.Lock()
    self.active_task: Optional[asyncio.Task] = None

    self.app: Optional[web.Application] = None
    self.runner: Optional[web.AppRunner] = None
    self.sites: List[web.BaseSite] = []
    self.cert_watcher: Optional[asyncio.Task] = None
    self.tokens_watcher: Optional[asyncio.Task] = None
    self.ssl_context: Optional[ssl.SSLContext] = None
    self.static_fs_path: str = ""
    limit_mbps = float(self.settings.file_transfer_limit_mbps or 0.0)
    if not math.isfinite(limit_mbps) or limit_mbps < 0:
        logger.warning(
            f"Ignoring file_transfer_limit_mbps={limit_mbps!r}: not a usable rate."
        )
        limit_mbps = 0.0
    self.transfer_cap = TransferPacer(static_bps=int(limit_mbps * 125000))
    self.download_pacer = TransferPacer(adaptive=True)
    self.upload_pacer = TransferPacer(adaptive=True)
    self.upload_dir = pathlib.Path(
        os.path.expanduser(self.settings.file_manager_path)
    ).resolve()
    self.print_spool = pathlib.Path(
        printing.spool_path(self.settings.print_spool_path)
    ).resolve()
    self.print_watcher: Optional[printing.SpoolWatcher] = None
    self.print_queue: Optional[printing.PrintQueue] = None
    self._recording_audio: Optional[Any] = None
    self._chunked_uploads: Dict[str, Dict[str, Any]] = {}
    self.web_files_ctx: Optional[tempfile.TemporaryDirectory] = None

    self._clients_present: bool = False
    self._client_hook_tasks: Set[asyncio.Task] = set()

    if bool(self.settings.wayland[0]):
        try:
            from pixelflux import ensure_wayland_display
            socket_name = ensure_wayland_display(
                width=int(self.settings.manual_width or 0),
                height=int(self.settings.manual_height or 0),
                render_node=self.settings.render_dri or "",
                auto_gpu=str(self.settings.auto_gpu or ""),
                cursor_size=int(self.settings.cursor_size),
            )
            if socket_name:
                host_display = str(
                    getattr(self.settings, "wayland_host_display", "") or "")
                os.environ["WAYLAND_DISPLAY"] = host_display or socket_name
                logger.info(f"Wayland compositor socket: {socket_name}")
            else:
                logger.warning(
                    "Wayland compositor socket did not come up within its "
                    "startup window; WAYLAND_DISPLAY left unchanged."
                )
        except ImportError:
            logger.warning("pixelflux unavailable; Wayland display not initialized.")

    self.STREAMING_MODE_WEBRTC = "webrtc"
    self.STREAMING_MODE_WEBSOCKETS = "websockets"
    self.STATIC_CONTENT_PATH = "selkies.selkies_web"
    self.MIME_TYPES = {
        "html": "text/html",
        "js": "text/javascript",
        "css": "text/css",
        "json": "application/json",
        "png": "image/png",
        "jpg": "image/jpeg",
        "ico": "image/x-icon",
        "svg": "image/svg+xml",
    }
paramsettingsAny
paramservicesOptional[Dict[str, BaseStreamingService]]
= None

Returns

None
funcset_clients_present(present) -> None

Record client presence and fire the configured presence hook.

Both streaming modes report here; the run_after_connect / run_after_disconnect hook command runs when the first client connects or the last one disconnects.

Source Code
def set_clients_present(self, present: bool) -> None:
    """Record client presence and fire the configured presence hook.

    Both streaming modes report here; the ``run_after_connect`` /
    ``run_after_disconnect`` hook command runs when the first client
    connects or the last one disconnects.

    Args:
        present: Whether at least one client is currently connected.
    """
    if present == self._clients_present:
        return
    self._clients_present = present
    cmd = (self.settings.run_after_connect if present
           else self.settings.run_after_disconnect)
    if cmd:
        # Hold a strong reference: the loop only keeps weak refs to tasks.
        task = asyncio.create_task(self._run_client_hook(cmd, present))
        self._client_hook_tasks.add(task)
        task.add_done_callback(self._client_hook_tasks.discard)
parampresentbool

Whether at least one client is currently connected.

Returns

None
func_run_client_hook(cmd, present) -> None

Run a presence hook shell command, killing it if it wedges.

Source Code
async def _run_client_hook(self, cmd: str, present: bool) -> None:
    """Run a presence hook shell command, killing it if it wedges."""
    name = "run_after_connect" if present else "run_after_disconnect"
    try:
        proc = await asyncio.create_subprocess_shell(cmd)
        try:
            returncode = await asyncio.wait_for(proc.wait(), timeout=300)
        except asyncio.TimeoutError:
            logger.warning(f"{name} command timed out after 300s; killing: {cmd}")
            try:
                proc.kill()
            except ProcessLookupError:
                pass
            returncode = await proc.wait()
        if returncode != 0:
            logger.warning(f"{name} command exited with status {returncode}: {cmd}")
    except OSError as e:
        logger.error(f"Failed to run {name} command {cmd!r}: {e}")
paramcmdstr
parampresentbool

Returns

None
func_b64_decode(data) -> str
Source Code
def _b64_decode(self, data: str) -> str:
    return base64.b64decode(data).decode("utf-8")
paramdatastr

Returns

str
func_get_https_certs() -> Tuple[Optional[str], Optional[str]]

Return absolute paths of the configured cert and key files, each None when unset or missing on disk.

Source Code
def _get_https_certs(self) -> Tuple[Optional[str], Optional[str]]:
    """Return absolute paths of the configured cert and key files, each None
    when unset or missing on disk."""
    https_cert = getattr(self.settings, "https_cert", None)
    https_key = getattr(self.settings, "https_key", None)
    cert_pem = (
        os.path.abspath(https_cert)
        if https_cert and os.path.isfile(https_cert)
        else None
    )
    key_pem = (
        os.path.abspath(https_key)
        if https_key and os.path.isfile(https_key)
        else None
    )
    return cert_pem, key_pem

Returns

typing.Tuple[typing.Optional[str], typing.Optional[str]]
func_self_signed_candidates() -> List[Tuple[str, str]]

Where a generated pair may live, most preferred first.

The configured path leads so an operator who pointed at one gets the pair there; the state directory catches a user install, which for the default ssl-cert-snakeoil path means anyone not running as root.

Source Code
def _self_signed_candidates(self) -> List[Tuple[str, str]]:
    """Where a generated pair may live, most preferred first.

    The configured path leads so an operator who pointed at one gets the
    pair there; the state directory catches a user install, which for the
    default `ssl-cert-snakeoil` path means anyone not running as root.

    Returns:
        `(certificate, key)` absolute path pairs; a pair with no key path
        is dropped, since a generated key is always written separately.
    """
    candidates = []
    configured = getattr(self.settings, "https_cert", None)
    if configured:
        candidates.append((os.path.abspath(configured),
                           os.path.abspath(getattr(self.settings, "https_key", "") or "")))
    state = os.path.join(
        os.environ.get("XDG_STATE_HOME") or os.path.expanduser("~/.local/state"),
        "selkies")
    candidates.append((os.path.join(state, "selkies.pem"),
                       os.path.join(state, "selkies.key")))
    return [(cert, key) for cert, key in candidates if cert and key]

Returns

typing.List

(certificate, key) absolute path pairs; a pair with no key path

func_self_signed_names() -> Tuple[list, list]

The names a generated certificate is issued for.

Loopback under every spelling, since that is what a published container port is reached by, plus the host's own name. Its routable address is deliberately absent: the pair outlives the address a container is given on each run, and one naming last week's is worse than one claiming none.

Source Code
def _self_signed_names(self) -> Tuple[list, list]:
    """The names a generated certificate is issued for.

    Loopback under every spelling, since that is what a published container
    port is reached by, plus the host's own name. Its routable address is
    deliberately absent: the pair outlives the address a container is given
    on each run, and one naming last week's is worse than one claiming none.

    Returns:
        `(dns names, ip addresses)`, both ready for `SubjectAlternativeName`.
    """
    from cryptography import x509

    dns = ["localhost"]
    hostname = socket.gethostname()
    if hostname and hostname != "localhost":
        dns.append(hostname)
    addresses = [ipaddress.ip_address("127.0.0.1"), ipaddress.ip_address("::1")]
    return ([x509.DNSName(name) for name in dns],
            [x509.IPAddress(address) for address in addresses])

Returns

typing.Tuple

(dns names, ip addresses), both ready for SubjectAlternativeName.

func_self_signed_pair_usable(cert_path, key_path) -> bool

Whether an existing generated pair can be served again as it is.

Reused only if it loads, has not expired, and names loopback by address: one issued before those SANs fails hostname verification at 127.0.0.1, and keeping it would carry that forward for the life of the install. Anything unreadable counts as absent, so a truncated file is replaced.

Source Code
def _self_signed_pair_usable(self, cert_path: str, key_path: str) -> bool:
    """Whether an existing generated pair can be served again as it is.

    Reused only if it loads, has not expired, and names loopback by address:
    one issued before those SANs fails hostname verification at 127.0.0.1,
    and keeping it would carry that forward for the life of the install.
    Anything unreadable counts as absent, so a truncated file is replaced.

    Args:
        cert_path: Certificate to examine.
        key_path: Its private key.

    Returns:
        True when the pair should be reused instead of regenerated.
    """
    from cryptography import x509

    if not (os.path.isfile(cert_path) and os.path.isfile(key_path)):
        return False
    try:
        with open(cert_path, "rb") as handle:
            cert = x509.load_pem_x509_certificate(handle.read())
        # not_valid_after_utc where cryptography has it, else the naive
        # attribute it replaced, read as UTC.
        expiry = getattr(cert, "not_valid_after_utc", None)
        now = datetime.now(tz=timezone.utc)
        if expiry is None:
            expiry = cert.not_valid_after.replace(tzinfo=timezone.utc)
        if expiry <= now:
            return False
        addresses = cert.extensions.get_extension_for_class(
            x509.SubjectAlternativeName).value.get_values_for_type(x509.IPAddress)
        if ipaddress.ip_address("127.0.0.1") not in addresses:
            return False
        context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
        context.load_cert_chain(cert_path, keyfile=key_path)
    except Exception:
        return False
    return True
paramcert_pathstr

Certificate to examine.

paramkey_pathstr

Its private key.

Returns

bool

True when the pair should be reused instead of regenerated.

func_make_self_signed_cert() -> Tuple[str, str]

Return a self-signed certificate and key, writing one where none is usable.

Turning HTTPS on is otherwise a two-step job — make a certificate, then point at it — and browsers gate the clipboard, gamepads, pointer lock, and the camera on a secure context, so the step is in everyone's way. The configured paths are used when their directory is writable, which for the default ssl-cert-snakeoil pair means running as root; a user install falls back to its own state directory. That directory is not a path _get_https_certs consults, so the pair already there is found here instead — and reused, since minting one per start would invalidate the exception the browser was told to make on every restart.

Source Code
def _make_self_signed_cert(self) -> Tuple[str, str]:
    """Return a self-signed certificate and key, writing one where none is usable.

    Turning HTTPS on is otherwise a two-step job — make a certificate, then
    point at it — and browsers gate the clipboard, gamepads, pointer lock,
    and the camera on a secure context, so the step is in everyone's way.
    The configured paths are used when their directory is writable, which
    for the default `ssl-cert-snakeoil` pair means running as root; a user
    install falls back to its own state directory. That directory is not a
    path `_get_https_certs` consults, so the pair already there is found
    here instead — and reused, since minting one per start would invalidate
    the exception the browser was told to make on every restart.

    Returns:
        The certificate and key paths, both absolute.

    Raises:
        OSError: No candidate directory could be written to.
    """
    from cryptography import x509
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import ec

    candidates = self._self_signed_candidates()
    for cert_path, key_path in candidates:
        if self._self_signed_pair_usable(cert_path, key_path):
            logger.info(
                "HTTPS is running on the self-signed certificate already at %s.",
                cert_path)
            return cert_path, key_path

    key = ec.generate_private_key(ec.SECP256R1())
    name = x509.Name([x509.NameAttribute(x509.NameOID.COMMON_NAME, "localhost")])
    dns_names, addresses = self._self_signed_names()
    now = datetime.now(tz=timezone.utc)
    cert = (
        x509.CertificateBuilder()
        .subject_name(name)
        .issuer_name(name)
        .public_key(key.public_key())
        .serial_number(x509.random_serial_number())
        .not_valid_before(now - timedelta(days=1))
        .not_valid_after(now + timedelta(days=3650))
        .add_extension(x509.SubjectAlternativeName(dns_names + addresses), critical=False)
        # Not a CA: a key sitting in the session container is a far worse
        # thing to put in a trust store than a leaf is to except.
        .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
        .sign(key, hashes.SHA256())
    )
    errors = []
    for cert_path, key_path in candidates:
        try:
            os.makedirs(os.path.dirname(cert_path), exist_ok=True)
            os.makedirs(os.path.dirname(key_path), exist_ok=True)
            with open(cert_path, "wb") as handle:
                handle.write(cert.public_bytes(serialization.Encoding.PEM))
            fd = os.open(key_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
            with os.fdopen(fd, "wb") as handle:
                handle.write(key.private_bytes(
                    encoding=serialization.Encoding.PEM,
                    format=serialization.PrivateFormat.PKCS8,
                    encryption_algorithm=serialization.NoEncryption()))
            # Set after the write: O_CREAT leaves an existing file's mode alone.
            os.chmod(key_path, 0o600)
        except OSError as error:
            errors.append(f"{cert_path}: {error}")
            continue
        logger.warning(
            "No certificate at the configured path, so HTTPS runs on a self-signed one "
            "written to %s, issued for %s and no other address. Browsers will warn once "
            "until it is trusted; a certificate from an authority, or a reverse proxy "
            "terminating TLS, avoids that.",
            cert_path,
            ", ".join([n.value for n in dns_names] + [str(a.value) for a in addresses]))
        return cert_path, key_path
    raise OSError("could not write a self-signed certificate: " + "; ".join(errors))

Returns

typing.Tuple

The certificate and key paths, both absolute.

func_create_ssl_context() -> Optional[ssl.SSLContext]

Build the server TLS context from the configured cert/key.

Source Code
def _create_ssl_context(self) -> Optional[ssl.SSLContext]:
    """Build the server TLS context from the configured cert/key.

    Returns:
        The loaded context, or None when HTTPS is disabled.

    Raises:
        FileNotFoundError: HTTPS is enabled but the certificate is missing.
    """
    enable_https = getattr(self.settings, "enable_https", None)
    if not enable_https or not enable_https[0]:
        return None

    cert_pem, key_pem = self._get_https_certs()
    if not cert_pem:
        cert_pem, key_pem = self._make_self_signed_cert()

    logger.debug(
        "Creating TLS context with certificate=%s key=%s", cert_pem, key_pem
    )
    sslctx = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
    sslctx.minimum_version = ssl.TLSVersion.TLSv1_2
    sslctx.check_hostname = False
    sslctx.verify_mode = ssl.CERT_NONE
    try:
        sslctx.load_cert_chain(cert_pem, keyfile=key_pem if key_pem else None)
    except Exception:
        logger.error(
            "Certificate or private key file not found or incorrect. "
            'To use a self-signed certificate, install the package "ssl-cert" '
            'and add the group "ssl-cert" to your user in Debian-based '
            "distributions, or generate a new certificate with root using: "
            "openssl req -x509 -newkey rsa:4096 "
            "-keyout /etc/ssl/private/ssl-cert-snakeoil.key "
            "-out /etc/ssl/certs/ssl-cert-snakeoil.pem -days 3650 -nodes "
            '-subj "/CN=localhost"'
        )
        raise
    return sslctx

Returns

typing.Optional

The loaded context, or None when HTTPS is disabled.

func_get_cert_mtime() -> float

Return the most recent modification time of the cert and key files.

Falls back to the generated pair actually being served, so the reload watcher is not blind to it when the configured path holds nothing.

Source Code
def _get_cert_mtime(self) -> float:
    """Return the most recent modification time of the cert and key files.

    Falls back to the generated pair actually being served, so the reload
    watcher is not blind to it when the configured path holds nothing.
    """
    cert_pem, key_pem = self._get_https_certs()
    if not cert_pem:
        for candidate_cert, candidate_key in self._self_signed_candidates():
            if os.path.isfile(candidate_cert):
                cert_pem, key_pem = candidate_cert, candidate_key
                break
    if not cert_pem:
        return 0.0
    try:
        cert_mtime = os.stat(cert_pem).st_mtime
        key_mtime = os.stat(key_pem).st_mtime if key_pem else 0.0
        return max(cert_mtime, key_mtime)
    except OSError:
        return 0.0

Returns

float
func_watch_and_reload_certs() -> None

Poll the TLS cert/key mtimes and swap in a new listening site on change.

The new SSL context is built before the old site is stopped so a bad certificate never takes the server offline, and the recorded mtime only advances once the new site is up, so a failed reload is retried on the next poll. Both the stat and the context build run off the loop: they read files that may sit on a network mount, and this runs while the session is streaming.

Source Code
async def _watch_and_reload_certs(self) -> None:
    """Poll the TLS cert/key mtimes and swap in a new listening site on change.

    The new SSL context is built before the old site is stopped so a bad
    certificate never takes the server offline, and the recorded mtime only
    advances once the new site is up, so a failed reload is retried on the
    next poll. Both the stat and the context build run off the loop: they
    read files that may sit on a network mount, and this runs while the
    session is streaming.
    """
    reload_interval = getattr(self.settings, "cert_reload_interval", 30)
    if reload_interval <= 0:
        logger.debug("Automatic certificate reloading is disabled (interval=0)")
        return

    current_sites = self.sites
    last_mtime = await asyncio.to_thread(self._get_cert_mtime)
    logger.debug(
        "Certificate reload watcher started (interval=%ds, initial mtime=%.0f)",
        reload_interval,
        last_mtime,
    )

    while True:
        await asyncio.sleep(reload_interval)
        try:
            new_mtime = await asyncio.to_thread(self._get_cert_mtime)
        except Exception as exc:
            logger.warning("Could not stat cert/key files: %s", exc)
            continue

        if new_mtime <= last_mtime:
            continue

        logger.info(
            "Certificate change detected (mtime %.0f -> %.0f), reloading…",
            last_mtime,
            new_mtime,
        )
        try:
            new_ssl_context = await asyncio.to_thread(self._create_ssl_context)
        except Exception as exc:
            logger.error(
                "Failed to create new SSL context, keeping old certificate: %s",
                exc,
            )
            continue

        if new_ssl_context is None:
            logger.error(
                "New SSL context is None (HTTPS disabled?), keeping old site."
            )
            continue

        for site in current_sites:
            try:
                await site.stop()
            except Exception as exc:
                logger.warning("Error stopping old %s: %s", self._site_kind(), exc)
        current_sites = self.sites = []
        logger.info("Old %s stopped.", self._site_kind())

        try:
            current_sites = await self._start_sites(new_ssl_context)
            last_mtime = new_mtime
            logger.info(
                "New %s started with reloaded certificates on %s",
                self._site_kind(),
                self._site_endpoint(),
            )
        except Exception as exc:
            logger.critical(
                "Failed to start new %s: %s. HTTPS server may be down; "
                "will retry on the next certificate poll.",
                self._site_kind(),
                exc,
            )

Returns

None
func_check_master_token(auth_header, master_token) -> bool

Timing-safe check of a Bearer <master_token> header, compared as UTF-8 bytes so non-ASCII tokens are safe.

Source Code
@staticmethod
def _check_master_token(auth_header: Optional[str], master_token: Any) -> bool:
    """Timing-safe check of a ``Bearer <master_token>`` header, compared as
    UTF-8 bytes so non-ASCII tokens are safe."""
    if not auth_header or not auth_header.startswith("Bearer "):
        return False
    parts = auth_header.split()
    if len(parts) < 2:
        return False
    return hmac.compare_digest(
        parts[1].encode("utf-8"), str(master_token).encode("utf-8")
    )
paramauth_headerOptional[str]
parammaster_tokenAny

Returns

bool
func_basic_auth_challenge(request, text='Invalid Credentials') -> web.Response

A 401 that re-opens the browser's login prompt.

Wrong credentials carry the challenge too: without one the browser renders this body as a page instead of asking again, so a mistyped password ends the session. Only a page load gets the Basic scheme: a browser holds a background request (any Sec-Fetch-Mode but a navigation's) open behind its login prompt, which a fullscreen session hides, so the page stalled. Answered at once, the web client's 401 guard reloads the page and the prompt comes with the page itself. A client that sends no Sec-Fetch-Mode gets the Basic scheme.

Source Code
@staticmethod
def _basic_auth_challenge(
    request: web.Request, text: str = "Invalid Credentials"
) -> web.Response:
    """A 401 that re-opens the browser's login prompt.

    Wrong credentials carry the challenge too: without one the browser renders this
    body as a page instead of asking again, so a mistyped password ends the session.
    Only a page load gets the Basic scheme: a browser holds a background request
    (any `Sec-Fetch-Mode` but a navigation's) open behind its login prompt, which a
    fullscreen session hides, so the page stalled. Answered at once, the web
    client's 401 guard reloads the page and the prompt comes with the page itself.
    A client that sends no `Sec-Fetch-Mode` gets the Basic scheme.
    """
    mode = request.headers.get("Sec-Fetch-Mode")
    scheme = "Basic" if mode in (None, "navigate", "nested-navigate") else "PageLogin"
    return web.Response(
        status=401,
        headers={
            "WWW-Authenticate": f'{scheme} realm="{AUTH_REALM}", charset="UTF-8"'
        },
        text=text,
    )
paramrequestweb.Request
paramtextstr
= 'Invalid Credentials'

Returns

aiohttp.web.aiohttp.web.Response
func_bearer_challenge(text='Unauthorized') -> web.Response

A 401 for a route that wants a token.

Names the Bearer scheme so the web client's 401 guard leaves it alone: a reload cannot change the token a page holds, whereas a Basic challenge is exactly what a reload re-presents. Browsers show no prompt for it.

Source Code
@staticmethod
def _bearer_challenge(text: str = "Unauthorized") -> web.Response:
    """A 401 for a route that wants a token.

    Names the Bearer scheme so the web client's 401 guard leaves it alone: a
    reload cannot change the token a page holds, whereas a Basic challenge
    is exactly what a reload re-presents. Browsers show no prompt for it.
    """
    return web.Response(
        status=401,
        headers={"WWW-Authenticate": f'Bearer realm="{AUTH_REALM}"'},
        text=text,
    )
paramtextstr
= 'Unauthorized'

Returns

aiohttp.web.aiohttp.web.Response
func_session_token_carriers(request) -> List[Tuple[str, str]]

The session-token carriers a request presents, most explicit first.

The Bearer header is what scripts send; the ?token= query is what a page opened with one carries on the URLs it navigates to rather than fetches (a file the manager downloads), which a page holding its token in the fragment leaves to the cookie; the cookie is the mirror the client keeps for requests it can put neither on. The cookie value is tried as sent and URL-decoded, since the client stores it encoded.

Source Code
@staticmethod
def _session_token_carriers(request: web.Request) -> List[Tuple[str, str]]:
    """The session-token carriers a request presents, most explicit first.

    The Bearer header is what scripts send; the ``?token=`` query is what a
    page opened with one carries on the URLs it navigates to rather than
    fetches (a file the manager downloads), which a page holding its token
    in the fragment leaves to the cookie; the cookie is the mirror the
    client keeps for requests it can put neither on. The cookie value is
    tried as sent and URL-decoded, since the client stores it encoded.

    Returns:
        ``(source, token)`` pairs, source being "header", "query" or "cookie".
    """
    carriers: List[Tuple[str, str]] = []
    auth_header = request.headers.get("Authorization", "")
    if auth_header.startswith("Bearer "):
        parts = auth_header.split()
        if len(parts) >= 2:
            carriers.append(("header", parts[1]))
    query_token = request.query.get("token")
    if query_token:
        carriers.append(("query", query_token))
    cookie = request.cookies.get(SESSION_TOKEN_COOKIE)
    if cookie:
        carriers.append(("cookie", cookie))
        decoded = urllib.parse.unquote(cookie)
        if decoded != cookie:
            carriers.append(("cookie", decoded))
    return carriers
paramrequestweb.Request

Returns

typing.List

(source, token) pairs, source being "header", "query" or "cookie".

func_session_token_verdict(request, settings) -> Optional[Tuple[str, str]]

Authenticate an API request by token in secure mode.

The master token (Bearer) counts as a controller, as does a controller-role session token; a viewer-role token is tagged so the handlers that refuse view-only credentials refuse it too. Session tokens are looked up in the same constant-time table lookup the WebSocket handshakes use.

Source Code
def _session_token_verdict(
    self, request: web.Request, settings: Any
) -> Optional[Tuple[str, str]]:
    """Authenticate an API request by token in secure mode.

    The master token (Bearer) counts as a controller, as does a
    controller-role session token; a viewer-role token is tagged so the
    handlers that refuse view-only credentials refuse it too. Session
    tokens are looked up in the same constant-time table lookup the
    WebSocket handshakes use.

    Returns:
        ``(role_ceiling, source)`` for the first carrier holding a valid
        token, or None when none does.
    """
    # Resolved per call: selkies imports this module, so the token table
    # it owns cannot be imported at module load.
    from .sessions import _lookup_session_token

    if self._check_master_token(request.headers.get("Authorization"), settings.master_token):
        return "controller", "header"
    for source, token in self._session_token_carriers(request):
        perms = _lookup_session_token(token)
        if perms is not None:
            role = "controller" if perms.get("role") == "controller" else "viewer"
            return role, source
    return None
paramrequestweb.Request
paramsettingsAny

Returns

typing.Optional

(role_ceiling, source) for the first carrier holding a valid

func_fetched_cross_site(request, settings) -> bool

Whether a browser sent this request for another site's page.

A GET from another site's image, link, or form carries no Origin but does carry Sec-Fetch-Site, and the browser attaches a cached Basic login to it, so a route whose GET changes something holds it to this: an Origin the operator allows (_is_origin_allowed) still passes, and clients that are not browsers send neither header.

Source Code
@classmethod
def _fetched_cross_site(cls, request: web.Request, settings: Any) -> bool:
    """Whether a browser sent this request for another site's page.

    A GET from another site's image, link, or form carries no Origin but
    does carry `Sec-Fetch-Site`, and the browser attaches a cached Basic
    login to it, so a route whose GET changes something holds it to this:
    an Origin the operator allows (`_is_origin_allowed`) still passes, and
    clients that are not browsers send neither header.
    """
    if request.headers.get("Sec-Fetch-Site") in ("cross-site", "same-site"):
        return not (request.headers.get("Origin") and cls._is_origin_allowed(request, settings))
    return not cls._is_origin_allowed(request, settings)
paramrequestweb.Request
paramsettingsAny

Returns

bool
func_is_origin_allowed(request, settings) -> bool

Return whether a browser request's Origin is permitted.

Applied to WebSocket upgrades and to the mode-switch POST. Empty allowed_origins means same-origin only (plus non-browser clients that send no Origin); * allows any; otherwise the Origin must be listed or match the Host header. A forwarded Host without a port (nginx's proxy_set_header Host $host, as the bundled config does) is matched by hostname: a browser Origin carries any non-default port, so a strict netloc comparison would reject every same-origin connection reached via an explicit port such as the :6080 mapping.

Source Code
@staticmethod
def _is_origin_allowed(request: web.Request, settings: Any) -> bool:
    """Return whether a browser request's Origin is permitted.

    Applied to WebSocket upgrades and to the mode-switch POST. Empty
    ``allowed_origins`` means same-origin only (plus non-browser clients
    that send no Origin); ``*`` allows any; otherwise the Origin must be
    listed or match the Host header. A forwarded Host without a port
    (nginx's ``proxy_set_header Host $host``, as the bundled config does)
    is matched by hostname: a browser Origin carries any non-default port,
    so a strict netloc comparison would reject every same-origin
    connection reached via an explicit port such as the :6080 mapping.
    """
    origin = request.headers.get("Origin")
    if not origin:
        return True
    allowed = {
        o.strip()
        for o in (getattr(settings, "allowed_origins", "") or "").split(",")
        if o.strip()
    }
    if "*" in allowed or origin in allowed:
        return True
    host = request.headers.get("Host")
    if host:
        try:
            origin_parts = urllib.parse.urlsplit(origin)
            if origin_parts.netloc == host:
                return True
            host_parts = urllib.parse.urlsplit("//" + host)
            if (
                host_parts.port is None
                and origin_parts.hostname
                and origin_parts.hostname == host_parts.hostname
            ):
                return True
        except ValueError:
            pass
    return False
paramrequestweb.Request
paramsettingsAny

Returns

bool
func_auth_middleware(request, handler) -> web.StreamResponse

Global auth guard for every route on the server: see _authorize.

A refusal is deliberately taken before the request's body is read -- an unauthenticated upload must not be paid for -- so the connection is closed with it. Leaving those bytes unread on a keep-alive connection has the server parse them as the next request's method, which fails that request and every later one on the same connection.

Source Code
@web.middleware
async def _auth_middleware(
    self,
    request: web.Request,
    handler: Callable[[web.Request], Awaitable[web.StreamResponse]],
) -> web.StreamResponse:
    """Global auth guard for every route on the server: see ``_authorize``.

    A refusal is deliberately taken before the request's body is read -- an
    unauthenticated upload must not be paid for -- so the connection is closed
    with it. Leaving those bytes unread on a keep-alive connection has the
    server parse them as the next request's method, which fails that request
    and every later one on the same connection.
    """
    response = await self._authorize(request, handler)
    if response.status >= 400 and request.body_exists and request.can_read_body:
        response.force_close()
    return response
paramrequestweb.Request
paramhandlerCallable[[web.Request], Awaitable[web.StreamResponse]]

Returns

aiohttp.web.aiohttp.web.StreamResponse
func_socket_cookie_value(role, expires) -> str

The socket cookie for role ("controller" or "viewer") until expires (epoch seconds).

Source Code
@staticmethod
def _socket_cookie_value(role: str, expires: int) -> str:
    """The socket cookie for `role` ("controller" or "viewer") until `expires` (epoch seconds)."""
    body = f"{role}.{expires}"
    return f"{body}.{hmac.new(_SOCKET_COOKIE_KEY, body.encode(), hashlib.sha256).hexdigest()}"
paramrolestr
paramexpiresint

Returns

str
func_socket_cookie_role(request) -> Optional[Tuple[str, int]]

The role a request's socket cookie grants and when it ends, or None for none, one past its time, or one this process did not sign.

Source Code
@classmethod
def _socket_cookie_role(cls, request: web.Request) -> Optional[Tuple[str, int]]:
    """The role a request's socket cookie grants and when it ends, or None for none, one past its time, or
    one this process did not sign."""
    value = request.cookies.get(SOCKET_COOKIE, "")
    parts = value.split(".")
    if len(parts) != 3 or parts[0] not in ("controller", "viewer") or not parts[1].isdigit():
        return None
    expires = int(parts[1])
    if expires <= time.time() or not hmac.compare_digest(cls._socket_cookie_value(parts[0], expires), value):
        return None
    return parts[0], expires
paramrequestweb.Request

Returns

typing.Optional[typing.Tuple[str, int]]
func_renew_socket_cookie(request, response, role) -> None

Answer a Basic-authenticated request with the socket cookie for its role where it carries none for that role with more than half its time left: HttpOnly, SameSite=Strict, under the route prefix, Secure over https. A page's load sets it for its first handshake, and the reconnect's probe for the next.

Source Code
@classmethod
def _renew_socket_cookie(cls, request: web.Request, response: web.StreamResponse, role: str) -> None:
    """Answer a Basic-authenticated request with the socket cookie for its `role` where it carries none for
    that role with more than half its time left: HttpOnly, SameSite=Strict, under the route prefix, Secure
    over https. A page's load sets it for its first handshake, and the reconnect's probe for the next."""
    held = cls._socket_cookie_role(request)
    if held is not None and held[0] == role and held[1] - time.time() > SOCKET_COOKIE_S / 2:
        return
    expires = int(time.time()) + SOCKET_COOKIE_S
    secure = request.secure or (
        request.headers.get("X-Forwarded-Proto", "").split(",")[0].strip().lower() == "https")
    response.set_cookie(
        SOCKET_COOKIE, cls._socket_cookie_value(role, expires), max_age=SOCKET_COOKIE_S,
        path=(request.app["settings"].subfolder or "") + "/", httponly=True, samesite="Strict", secure=secure)
paramrequestweb.Request
paramresponseweb.StreamResponse
paramrolestr

Returns

None
func_authorize(request, handler) -> web.StreamResponse

Authorize one request and hand it to handler, or refuse it.

Layered gates, in order: cross-site WebSocket upgrades are rejected by Origin; health/liveness endpoints pass without credentials; the token endpoint and the control endpoints (the mode switch, a session disconnect, a recording start or stop) accept the Bearer master token, trying Authorization first and the MASTER_TOKEN_HEADER fallback second for callers whose Authorization header a Basic login in front already owns (a mode switch not so authenticated is held to the same Origin rule as the upgrades, since a browser attaches cached Basic credentials to a cross-site POST); in secure mode every other API route accepts a session token (Bearer header, ?token= query, or the client's cookie), which is the only credential when Basic auth is off; the mode switch takes the master token ahead of the Origin rule and otherwise authenticates like any other API route, its handler refusing every role but controller; and everything else falls through to Basic Auth when enabled. A cookie- carried token on a state-changing request is held to the Origin rule too, since the browser attaches it; and with a master token set, the WebSocket handshakes skip Basic (a browser cannot attach fresh Basic credentials to a handshake, so it would add only an undebuggable 401). A request that authenticates with the view-only password or a viewer-role token, or in secure mode reaches an API route with a Basic login and no token, is tagged with auth_role_ceiling = "viewer" for downstream handlers to enforce.

Source Code
async def _authorize(
    self,
    request: web.Request,
    handler: Callable[[web.Request], Awaitable[web.StreamResponse]],
) -> web.StreamResponse:
    """Authorize one request and hand it to `handler`, or refuse it.

    Layered gates, in order: cross-site WebSocket upgrades are rejected by
    Origin; health/liveness endpoints pass without credentials; the token
    endpoint and the control endpoints (the mode switch, a session
    disconnect, a recording start or stop) accept the Bearer master token,
    trying `Authorization` first and the `MASTER_TOKEN_HEADER` fallback
    second for callers whose Authorization header a Basic login in front
    already owns (a
    mode switch not so authenticated is held to the same Origin rule as
    the upgrades, since a browser attaches cached Basic credentials to a
    cross-site POST); in secure mode every other API route accepts a
    session token (Bearer header, ``?token=`` query, or the client's
    cookie), which is the only credential when Basic auth is off; the mode
    switch takes the master token ahead of the Origin rule and otherwise
    authenticates like any other API route, its handler refusing every
    role but controller; and everything else falls through to Basic Auth
    when enabled. A cookie-
    carried token on a state-changing request is held to the Origin rule
    too, since the browser attaches it; and with a master token set, the
    WebSocket handshakes skip Basic (a browser cannot attach fresh Basic
    credentials to a handshake, so it would add only an undebuggable
    401). A request that authenticates with the view-only password or a
    viewer-role token, or in secure mode reaches an API route with a Basic
    login and no token, is tagged with ``auth_role_ceiling = "viewer"`` for
    downstream handlers to enforce.
    """
    settings = request.app["settings"]
    auth_header = request.headers.get("Authorization")
    path = request.path
    is_ws_upgrade = request.headers.get("Upgrade", "").lower() == "websocket"
    # Match the exact route, not a suffix, so /foo/tokens isn't treated as control-plane.
    api_prefix = settings.subfolder
    is_ws_handshake = is_ws_upgrade and path.rstrip("/") in {
        f"{api_prefix}{route}" for route in WEBSOCKET_ROUTES}
    if is_ws_upgrade:
        if not self._is_origin_allowed(request, settings):
            logger.warning(
                "Rejected WebSocket upgrade from disallowed Origin: %r",
                request.headers.get("Origin", ""),
            )
            return web.Response(status=403, text="Forbidden origin")
    if path in (f"{api_prefix}/api/status", f"{api_prefix}/api/health"):
        return await handler(request)
    token_path = path == f"{api_prefix}/api/tokens"
    if settings.master_token and token_path:
        if not any(
            self._check_master_token(header, settings.master_token)
            for header in (auth_header, request.headers.get(MASTER_TOKEN_HEADER))
        ):
            return self._bearer_challenge()
        return await handler(request)

    # The operator's own paths, ahead of the Origin rule a browser is held
    # to; a session token reaches them through the verdict below.
    is_control_path = path == f"{api_prefix}/api/switch" or (
        request.method in ("POST", "DELETE")
        and (path == f"{api_prefix}/api/recording"
             or path.startswith(f"{api_prefix}/api/sessions/")))
    if settings.master_token and is_control_path:
        if any(
            self._check_master_token(header, settings.master_token)
            for header in (auth_header, request.headers.get(MASTER_TOKEN_HEADER))
        ):
            return await handler(request)
    if is_control_path and not self._is_origin_allowed(request, settings):
        logger.warning(
            "Rejected %s %s from disallowed Origin: %r",
            request.method, path, request.headers.get("Origin", ""),
        )
        return web.Response(status=403, text="Forbidden origin")

    # In secure mode the token decides an API request's role: a Basic login
    # without one counts as a viewer, as a proxy in front may send the session's
    # own login for every participant (SealSkin's rooms).
    token_bound = bool(settings.master_token and not is_ws_handshake and not token_path
                       and path.startswith(f"{api_prefix}/api/"))
    if token_bound:
        verdict = self._session_token_verdict(request, settings)
        if verdict is not None:
            ceiling, source = verdict
            if (source == "cookie" and request.method not in ("GET", "HEAD")
                    and not self._is_origin_allowed(request, settings)):
                logger.warning(
                    "Rejected cookie-authenticated %s %s from disallowed Origin: %r",
                    request.method, path, request.headers.get("Origin", ""),
                )
                return web.Response(status=403, text="Forbidden origin")
            request["auth_role_ceiling"] = ceiling
            return await handler(request)
        if not settings.enable_basic_auth[0]:
            return self._bearer_challenge()

    if not settings.enable_basic_auth[0]:
        logger.debug("Basic auth not enabled, forwarding to routers")
        return await handler(request)
    if is_ws_handshake and settings.master_token:
        return await handler(request)
    if not auth_header or not auth_header.startswith("Basic "):
        held = self._socket_cookie_role(request) if is_ws_handshake else None
        if held is not None:
            # WebKit attaches no Basic credentials to a handshake: the cookie the page's
            # Basic-authenticated requests were answered with stands for them, at its role.
            request["auth_role_ceiling"] = held[0]
            return await handler(request)
        if is_ws_upgrade:
            # A WS handshake gets no challenge/retry: without this log the
            # rejection is invisible on both ends.
            logger.warning(
                "Rejected WebSocket upgrade from %s: basic auth is enabled and the "
                "handshake carried no Authorization header (browsers only attach "
                "cached credentials; set a master token or disable basic auth for "
                "browser clients behind proxies).",
                request.remote,
            )
        return self._basic_auth_challenge(request, "Authorization Required")
    try:
        auth_decoded = self._b64_decode(auth_header[6:])
        if ":" not in auth_decoded:
            return self._basic_auth_challenge(request)
        username, password = auth_decoded.split(":", 1)
        # Compare as UTF-8 bytes; hmac.compare_digest rejects non-ASCII str.
        user_ok = hmac.compare_digest(
            username.encode("utf-8"), str(settings.basic_auth_user).encode("utf-8")
        )
        pw = password.encode("utf-8")
        main_ok = hmac.compare_digest(
            pw, str(settings.basic_auth_password).encode("utf-8")
        )
        # Both comparisons run so reply timing never reveals which password was sent.
        viewonly_secret = str(getattr(settings, "basic_auth_viewonly_password", "") or "")
        viewonly_ok = bool(viewonly_secret) and hmac.compare_digest(
            pw, viewonly_secret.encode("utf-8")
        )
        if not (user_ok and (main_ok or viewonly_ok)):
            logger.warning(
                f"Invalid credentials provided for user: {settings.basic_auth_user}"
            )
            return self._basic_auth_challenge(request)
        request["auth_role_ceiling"] = (
            "viewer" if (viewonly_ok and not main_ok) or token_bound else "controller"
        )
    except Exception:
        return self._basic_auth_challenge(request)
    response = await handler(request)
    if not is_ws_upgrade and request.method in ("GET", "HEAD") and not response.prepared:
        self._renew_socket_cookie(request, response, request["auth_role_ceiling"])
    return response
paramrequestweb.Request
paramhandlerCallable[[web.Request], Awaitable[web.StreamResponse]]

Returns

aiohttp.web.aiohttp.web.StreamResponse
func_require_configured_credentials() -> None

Refuse to serve a login that nobody chose a password for.

The built-in password is a placeholder, not a credential: reaching this point still carrying it means no password was set anywhere, and the server would otherwise put an unconfigured login on the network. What counts is that a value was supplied, on the command line or in the environment — not what the value is. An image that ships its own default is choosing it deliberately, the way most container images do, and rejecting known-weak values here would break every one of them while stopping nobody who meant it.

Source Code
def _require_configured_credentials(self) -> None:
    """Refuse to serve a login that nobody chose a password for.

    The built-in password is a placeholder, not a credential: reaching this point
    still carrying it means no password was set anywhere, and the server would
    otherwise put an unconfigured login on the network. What counts is that a value
    was supplied, on the command line or in the environment — not what the value is.
    An image that ships its own default is choosing it deliberately, the way most
    container images do, and rejecting known-weak values here would break every one
    of them while stopping nobody who meant it.

    Raises:
        SystemExit: With `EXIT_CONFIG_ERROR`, which says the settings are the
            problem and that starting again will not change the answer — a
            supervisor respawning this forever would otherwise leave a
            container that looks healthy and never answers.
    """
    if not self.settings.enable_basic_auth[0]:
        return
    if self.settings.was_provided("basic_auth_password"):
        return
    logger.error(
        "Basic authentication is enabled but no password was set. Set one with "
        "--basic-auth-password, or the SELKIES_BASIC_AUTH_PASSWORD, PASSWORD, or "
        "PASSWD environment variable; or serve without a login by passing "
        "--enable-basic-auth=false."
    )
    raise SystemExit(EXIT_CONFIG_ERROR)

Returns

None
func_require_one_listen_setting() -> None

Refuse --public beside an explicit --addr: both choose the TCP listen addresses, and letting one win in silence would leave the operator who typed the other believing it took effect.

Source Code
def _require_one_listen_setting(self) -> None:
    """Refuse `--public` beside an explicit `--addr`: both choose the TCP
    listen addresses, and letting one win in silence would leave the
    operator who typed the other believing it took effect.

    Raises:
        SystemExit: With `EXIT_CONFIG_ERROR`, as for missing credentials.
    """
    if self.settings.public[0] and self.settings.was_provided("addr"):
        logger.error(
            "--public and --addr (SELKIES_PUBLIC, SELKIES_ADDR) were both given: "
            "--public listens on every interface, --addr on the addresses it "
            "names. Pass one of them."
        )
        raise SystemExit(EXIT_CONFIG_ERROR)

Returns

None
funcswitch_to_mode(mode_name) -> None

Stop the active streaming service and start mode_name in its place.

Serialized under the supervisor lock so two switches can never overlap; switching to the already-active mode is a no-op. The service reads the settings at start, so the encoder knob is brought in line with the transport before it starts (a websockets-only encoder such as jpeg or striped h264enc cannot ride the WebRTC pipeline, and a switch back restores the operator's menu and value), and only then does an unset paint-over resolve, since its default depends on the resolved encoder (JPEG keeps it under Turbo), the order _post_process_settings keeps.

Source Code
async def switch_to_mode(self, mode_name: str) -> None:
    """Stop the active streaming service and start ``mode_name`` in its place.

    Serialized under the supervisor lock so two switches can never overlap;
    switching to the already-active mode is a no-op. The service reads
    the settings at start, so the encoder knob is brought in line with the
    transport before it starts (a websockets-only encoder such as jpeg or
    striped h264enc cannot ride the WebRTC pipeline, and a switch back
    restores the operator's menu and value), and only then does an unset
    paint-over resolve, since its default depends on the resolved encoder
    (JPEG keeps it under Turbo), the order `_post_process_settings` keeps.

    Args:
        mode_name: Registered service name ("websockets" or "webrtc").

    Raises:
        ValueError: ``mode_name`` is not a registered service.
    """
    if mode_name not in self.services:
        raise ValueError(f"Service {mode_name} not found")

    async with self.lock:
        if self.current_mode == mode_name:
            logger.info(f"Mode {mode_name} is already active.")
            return

        await self._stop_service()
        logger.info(f"Starting service: {mode_name}")
        self.settings.mode = mode_name
        self.settings.apply_webrtc_encoder_filter()
        self.settings.resolve_paint_over_default()
        self.settings.resolve_cpu_encode_threading_default()
        service = self.services[mode_name]
        task = asyncio.create_task(service.start())
        self.active_task = task
        self.current_mode = mode_name

        def _on_service_done(finished: asyncio.Task, mode: str = mode_name) -> None:
            """Clear the stale mode when the service dies unexpectedly; the
            exception is retrieved so asyncio never logs it as unretrieved."""
            if finished.cancelled():
                return
            exc = finished.exception()
            if exc is not None:
                logger.error(f"Service '{mode}' terminated unexpectedly: {exc!r}")
                if self.active_task is finished:
                    self.current_mode = None
                    self.active_task = None

        task.add_done_callback(_on_service_done)
parammode_namestr

Registered service name ("websockets" or "webrtc").

Returns

None
func_stop_service() -> None

Stop the active service, escalating to a forced cancel on timeout.

The grace period lets a teardown that includes ~2 s gamepad-close waits finish before a forced cancel that would leak resources.

Source Code
async def _stop_service(self) -> None:
    """Stop the active service, escalating to a forced cancel on timeout.

    The grace period lets a teardown that includes ~2 s gamepad-close
    waits finish before a forced cancel that would leak resources.
    """
    if not self.current_mode:
        return
    logger.info(f"Stopping service: {self.current_mode}")

    await self.services[self.current_mode].stop()
    if self.active_task:
        try:
            await asyncio.wait_for(self.active_task, timeout=15)
        except asyncio.TimeoutError:
            logger.warning(
                f"Timeout while stopping '{self.current_mode}'. Canceling task."
            )
            self.active_task.cancel()
            try:
                await self.active_task
            except asyncio.CancelledError:
                logger.info(
                    f"Task canceled after timeout for '{self.current_mode}'."
                )
            except Exception as e:
                logger.warning(f"Service task raised during forced stop: {e!r}")
    self.current_mode = None
    self.active_task = None

Returns

None
func_get_status() -> Dict[str, Any]

The /api/status body.

enable_dual_mode is surfaced here so the dashboard can render the WebSocket/WebRTC toggle from this early, transport-independent probe: serverSettings only arrives once a stream connects, so a WebRTC session that never comes up would otherwise strand the user with no way back to WebSockets. ui_layout and the layouts the operator allows are surfaced for the same reason: the dashboard paints its layout before the stream connects, and a page that painted one and switched to the operator's on connect would flash, or offer a layout toggle that a locked layout hides.

Source Code
def _get_status(self) -> Dict[str, Any]:
    """The /api/status body.

    `enable_dual_mode` is surfaced here so the dashboard can render the
    WebSocket/WebRTC toggle from this early, transport-independent probe:
    serverSettings only arrives once a stream connects, so a WebRTC
    session that never comes up would otherwise strand the user with no
    way back to WebSockets. `ui_layout` and the layouts the operator
    allows are surfaced for the same reason: the dashboard paints its
    layout before the stream connects, and a page that painted one and
    switched to the operator's on connect would flash, or offer a layout
    toggle that a locked layout hides.
    """
    return {
        "current_mode": self.current_mode,
        "available_modes": list(self.services.keys()),
        "enable_dual_mode": bool(
            getattr(self.settings, "enable_dual_mode", (False,))[0]
        ),
        "ui_layout": getattr(self.settings, "ui_layout", "default"),
        "ui_layout_allowed": enum_allowed("ui_layout"),
    }

Returns

typing.Dict[str, typing.Any]
func_viewer_ceiling(request) -> bool

Return whether the credential that authenticated this request caps it at the viewer role (the view-only basic-auth password, or a viewer-role session token in secure mode).

Every endpoint that changes host or session state refuses those requests, the way the streaming plane refuses input from a viewer, and so does every one that hands out more than a viewer watches: the files under the file-manager directory and printed documents. What the session shows anyway (its pages, a screenshot, whether a recording runs) stays available.

Source Code
@staticmethod
def _viewer_ceiling(request: web.Request) -> bool:
    """Return whether the credential that authenticated this request caps it
    at the viewer role (the view-only basic-auth password, or a viewer-role
    session token in secure mode).

    Every endpoint that changes host or session state refuses those
    requests, the way the streaming plane refuses input from a viewer, and
    so does every one that hands out more than a viewer watches: the files
    under the file-manager directory and printed documents. What the
    session shows anyway (its pages, a screenshot, whether a recording
    runs) stays available.
    """
    return request.get("auth_role_ceiling") == "viewer"
paramrequestweb.Request

Returns

bool
funchandle_switch(request) -> web.Response

POST /api/switch: change the active streaming mode.

A controller may switch: it already drives the desktop, so withholding the transport from it protects nothing. What the credential does decide is who counts as one — view-only credentials and viewer-role tokens are refused here, and the master token remains the operator's way in for a deployment that hands out tokens it does not want switching for everyone (the switch restarts the service under every connected page). Refused as well when dual mode is disabled.

Source Code
async def handle_switch(self, request: web.Request) -> web.Response:
    """POST /api/switch: change the active streaming mode.

    A controller may switch: it already drives the desktop, so withholding
    the transport from it protects nothing. What the credential does decide
    is who counts as one — view-only credentials and viewer-role tokens are
    refused here, and the master token remains the operator's way in for a
    deployment that hands out tokens it does not want switching for
    everyone (the switch restarts the service under every connected page).
    Refused as well when dual mode is disabled.
    """
    if self._viewer_ceiling(request):
        return web.json_response(
            {"status": "error", "message": "View-only credentials cannot switch streaming mode"},
            status=403,
        )
    dual_mode = getattr(self.settings, "enable_dual_mode", (False,))[0]
    if not dual_mode:
        return web.json_response(
            {"status": "error", "message": "Dual streaming mode disabled"},
            status=403,
        )

    try:
        data = await request.json()
        if not isinstance(data, dict):
            raise ValueError("Request body must be a JSON object")
        target_mode = data.get("mode")
        await self.switch_to_mode(target_mode)
        return web.json_response({"status": "success", "mode": target_mode})
    except Exception as e:
        return web.json_response({"status": "error", "message": str(e)}, status=400)
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
func_session_gauge(request) -> Optional[UplinkGauge]

The UplinkGauge for one transfer request, upload or download, or None when nothing can be gauged (the transfer then must not be throttled blindly).

The gauge wants the requester's own session connections — another client's socket crosses a different path, and its jitter would throttle a transfer that cannot be queuing there. The active service lists every client session; the requester's are narrowed by session token when the request carries one, else by peer address, and when neither discriminates the whole list is gauged: with a floor per session, a foreign session can only ever add false congestion, which slows the transfer and never harms a stream.

Gauge state (ping clock and floor history) lives in _UPLINK_SESSIONS, keyed weakly by session websocket, so the separate requests of a chunked transfer's slices share one floor and a mid-transfer slice never re-baselines against its own standing queue.

Source Code
def _session_gauge(self, request: web.Request) -> Optional[UplinkGauge]:
    """The `UplinkGauge` for one transfer request, upload or download, or
    None when nothing can be gauged (the transfer then must not be
    throttled blindly).

    The gauge wants the requester's own session connections — another
    client's socket crosses a different path, and its jitter would
    throttle a transfer that cannot be queuing there. The active service
    lists every client session; the requester's are narrowed by session
    token when the request carries one, else by peer address, and when
    neither discriminates the whole list is gauged: with a floor per
    session, a foreign session can only ever add false congestion, which
    slows the transfer and never harms a stream.

    Gauge state (ping clock and floor history) lives in
    `_UPLINK_SESSIONS`, keyed weakly by session websocket, so the separate
    requests of a chunked transfer's slices share one floor and a
    mid-transfer slice never re-baselines against its own standing queue.
    """
    service = self.services.get(self.current_mode) if self.current_mode else None
    if service is None:
        return None
    conns = service.uplink_session_conns()
    if not conns:
        return None
    tokens = {token for _source, token in self._session_token_carriers(request)}
    if tokens:
        matched = [c for c in conns if c[1] and c[1] in tokens]
        if matched:
            conns = matched
    if len(conns) > 1 and request.remote:
        matched = [c for c in conns if c[2] == request.remote]
        if matched:
            conns = matched
    entries = []
    for ws, _token, _ip in conns:
        state = _uplink_session_state(ws)
        entries.append([ws, state, state["seq"]])
    return UplinkGauge(entries) if entries else None
paramrequestweb.Request

Returns

typing.Optional[selkies.stream_server.UplinkGauge]
func_stream_upload_body(request, leaf, dir_fd, append) -> int

Stream a request body into the file leaf names in the open directory dir_fd, opening, writing, and closing it on the executor.

Creates the file when append is False, refusing one that already exists, and appends when True. O_NOFOLLOW blocks a symlink planted over the leaf's name, and no component above it is re-walked by name: the dir_fd is the descriptor the path confinement opened, so a symlink swapped in over an intermediate component cannot redirect the write outside the root. Enforces the declared Content-Length.

Reads pace against two allowances: a paused read fills aiohttp's flow-control buffer, the TCP window closes, and the client's uplink is freed for the input/feedback traffic the stream depends on. The operator's static cap is the shared transfer_cap bucket; congestion control is upload_pacer fed one UplinkGauge verdict per read, so the transfer takes whatever the uplink has spare and backs off the moment the session's own delay inflates. A transfer below TRANSFER_MIN_GAUGED_BYTES is over before a queue could matter and skips the gauge, as does one with no gauge-able session socket; a gauge whose sockets all vanish mid-transfer stops pacing — the session it protected is gone. A gauged transfer's reads are sized to the current rate (_gauged_chunk_size), since the read size is what the client is handed back as receive window and sends at once.

Source Code
async def _stream_upload_body(
        self, request: web.Request, leaf: str, dir_fd: int, append: bool) -> int:
    """Stream a request body into the file `leaf` names in the open
    directory `dir_fd`, opening, writing, and closing it on the executor.

    Creates the file when ``append`` is False, refusing one that already
    exists, and appends when True. `O_NOFOLLOW` blocks a symlink planted
    over the leaf's name, and no component above it is re-walked by name:
    the `dir_fd` is the descriptor the path confinement opened, so a
    symlink swapped in over an intermediate component cannot redirect the
    write outside the root. Enforces the declared Content-Length.

    Reads pace against two allowances: a paused read fills aiohttp's
    flow-control buffer, the TCP window closes, and the client's uplink is
    freed for the input/feedback traffic the stream depends on. The
    operator's static cap is the shared `transfer_cap` bucket; congestion
    control is `upload_pacer` fed one `UplinkGauge` verdict per read, so
    the transfer takes whatever the uplink has spare and backs off the
    moment the session's own delay inflates. A transfer below
    `TRANSFER_MIN_GAUGED_BYTES` is over before a queue could matter and
    skips the gauge, as does one with no gauge-able session socket; a
    gauge whose sockets all vanish mid-transfer stops pacing — the
    session it protected is gone. A gauged transfer's reads are sized to
    the current rate (`_gauged_chunk_size`), since the read size is what
    the client is handed back as receive window and sends at once.

    Args:
        request: The upload request whose body is streamed.
        leaf: The staging file's name in the directory `dir_fd` opens.
        dir_fd: The held descriptor of the destination's directory.
        append: Append to the file instead of creating it.

    Returns:
        The byte count written.

    Raises:
        Exception: Propagated from the read/write path after the handle is
            closed; the caller owns removal of the target file.
    """
    declared = request.content_length
    loop = asyncio.get_running_loop()
    cap = self.transfer_cap if self.transfer_cap.active else None
    gauge = (
        self._session_gauge(request)
        if (declared or 0) >= TRANSFER_MIN_GAUGED_BYTES
        else None)
    flags = os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | (os.O_APPEND if append else os.O_EXCL)
    fh = await loop.run_in_executor(
        None, lambda: os.fdopen(os.open(leaf, flags, 0o644, dir_fd=dir_fd), "wb"))
    written = 0
    try:
        while True:
            read_size = (
                _gauged_chunk_size(self.upload_pacer) if gauge is not None
                else 1 << 20)
            chunk = await request.content.read(read_size)
            if not chunk:
                break
            if declared is not None and written + len(chunk) > declared:
                raise ValueError("body exceeds declared Content-Length")
            if cap is not None:
                await cap.pace(len(chunk))
            if gauge is not None:
                verdict = await gauge.sample()
                if gauge.alive:
                    await self.upload_pacer.pace_verdict(len(chunk), verdict, gauge.inflation_us)
                else:
                    gauge = None
            await loop.run_in_executor(None, fh.write, chunk)
            written += len(chunk)
        await loop.run_in_executor(None, fh.close)
    except Exception:
        try:
            await loop.run_in_executor(None, fh.close)
        except Exception:
            pass
        raise
    return written
paramrequestweb.Request

The upload request whose body is streamed.

paramleafstr

The staging file's name in the directory dir_fd opens.

paramdir_fdint

The held descriptor of the destination's directory.

paramappendbool

Append to the file instead of creating it.

Returns

int

The byte count written.

func_discard_chunked_upload(dest, part_path) -> None

Drop a chunked transfer's tracking entry and its on-disk .part file.

The entry and the file it names go together with nothing awaited between, so a restart on the same path cannot slip its own entry in between and lose it; the removal blocks the loop the once a discard costs, which a transfer's slices do not.

Source Code
def _discard_chunked_upload(self, dest: str, part_path: str) -> None:
    """Drop a chunked transfer's tracking entry and its on-disk .part file.

    The entry and the file it names go together with nothing awaited
    between, so a restart on the same path cannot slip its own entry in
    between and lose it; the removal blocks the loop the once a discard
    costs, which a transfer's slices do not.
    """
    self._chunked_uploads.pop(dest, None)
    _remove_quietly(part_path)
paramdeststr
parampart_pathstr

Returns

None
func_expire_stale_chunked_uploads() -> None

Reap transfers idle past UPLOAD_PART_TTL_SECONDS (entry + .part file).

Source Code
def _expire_stale_chunked_uploads(self) -> None:
    """Reap transfers idle past UPLOAD_PART_TTL_SECONDS (entry + .part file)."""
    now = time.monotonic()
    for key in [k for k, s in self._chunked_uploads.items()
                if now - s["ts"] > UPLOAD_PART_TTL_SECONDS and not s["busy"]]:
        self._discard_chunked_upload(key, self._chunked_uploads[key]["part"])
        logger.debug(f"Expired stale chunked upload: {key}")

Returns

None
funchandle_upload(request) -> web.Response

Stream a client file upload to the file-manager directory over HTTP.

Available in every streaming mode and not bounded by the data-channel / WebSocket per-message size, so it saturates the link where the per-chunk SCTP path cannot. The destination path (relative to the file-manager root) arrives URL-encoded in the X-Upload-Path header. The body streams to disk on the executor, and so do the directory creation, the staging open, and the plain path's rename onto the destination, since a network filesystem takes tens of milliseconds over each and a directory of small files pays it once per file, which the event loop would otherwise take from the stream it serves. A chunked transfer is tracked by its destination, so its finalize and its discards stay synchronous to keep the rename and the entry's removal atomic against a restart on the same path; each is a once-per-transfer cost, not a per-slice one. Path safety mirrors the data-channel path: no traversal outside the root, and the write lands in a directory descriptor the confinement walked and verified, with O_NOFOLLOW on the staging leaf itself.

Two request shapes share the endpoint:

  • Plain: one POST carrying the whole file, no chunk headers — staged in a hidden sibling of the destination and renamed onto it when the body is complete.
  • Chunked (the client slices files above its 64 MiB threshold so no single request body exceeds a fronting proxy's per-request cap, e.g. Cloudflare's 100 MB): sequential POSTs for the same X-Upload-Path, each also carrying X-Upload-Id: opaque client-chosen transfer id X-Upload-Offset: absolute byte offset of this slice X-Upload-Total: final file size in bytes X-Upload-Final: "1" on the last slice Slices accumulate in a staging sibling that offset 0 creates (_upload_staging_path) and the transfer's tracking entry names; offset 0 also removes the one an abandoned transfer to the same path left, and non-zero offsets must exactly continue the tracked transfer (same id, offset equal to the bytes already banked, matching staged size) or the transfer is discarded with 409. The final slice validates the accumulated size against X-Upload-Total and renames the staged file onto the destination atomically. Transfers idle past UPLOAD_PART_TTL_SECONDS are expired on the next chunked request.

Both shapes carry the mode of a file of this user's they replace onto the replacement (_carry_destination_mode) and are refused for view-only credentials (the view-only password, a viewer-role session token).

Source Code
async def handle_upload(self, request: web.Request) -> web.Response:
    """Stream a client file upload to the file-manager directory over HTTP.

    Available in every streaming mode and not bounded by the data-channel /
    WebSocket per-message size, so it saturates the link where the per-chunk
    SCTP path cannot. The destination path (relative to the file-manager
    root) arrives URL-encoded in the X-Upload-Path header. The body streams
    to disk on the executor, and so do the directory creation, the staging
    open, and the plain path's rename onto the destination, since a network
    filesystem takes tens of milliseconds over each and a directory of
    small files pays it once per file, which the event loop would otherwise
    take from the stream it serves. A chunked transfer is tracked by its
    destination, so its finalize and its discards stay synchronous to keep
    the rename and the entry's removal atomic against a restart on the same
    path; each is a once-per-transfer cost, not a per-slice one. Path safety
    mirrors the data-channel path: no traversal outside the root, and the
    write lands in a directory descriptor the confinement walked and
    verified, with `O_NOFOLLOW` on the staging leaf itself.

    Two request shapes share the endpoint:

    - Plain: one POST carrying the whole file, no chunk headers — staged in a
      hidden sibling of the destination and renamed onto it when the body is
      complete.
    - Chunked (the client slices files above its 64 MiB threshold so no
      single request body exceeds a fronting proxy's per-request cap, e.g.
      Cloudflare's 100 MB): sequential POSTs for the same X-Upload-Path,
      each also carrying
        X-Upload-Id:     opaque client-chosen transfer id
        X-Upload-Offset: absolute byte offset of this slice
        X-Upload-Total:  final file size in bytes
        X-Upload-Final:  "1" on the last slice
      Slices accumulate in a staging sibling that offset 0 creates
      (_upload_staging_path) and the transfer's tracking entry names; offset
      0 also removes the one an abandoned transfer to the same path left,
      and non-zero offsets must exactly continue the tracked transfer (same
      id, offset equal to the bytes already banked, matching staged size) or
      the transfer is discarded with 409. The final slice validates the
      accumulated size against X-Upload-Total and renames the staged file
      onto the destination atomically. Transfers idle past
      UPLOAD_PART_TTL_SECONDS are expired on the next chunked request.

    Both shapes carry the mode of a file of this user's they replace onto
    the replacement (`_carry_destination_mode`) and are refused for
    view-only credentials (the view-only password, a viewer-role session
    token).
    """
    settings = request.app["settings"]
    rel = urllib.parse.unquote(request.headers.get("X-Upload-Path", "") or "")

    def failed(message: str, status: int = 400) -> web.Response:
        """Refuse the transfer, recording the attempt and why it was refused."""
        audit.emit("file.upload.error", filename=rel, error=message)
        return web.json_response({"status": "error", "message": message}, status=status)

    if self._viewer_ceiling(request):
        return failed("View-only credentials cannot upload files", 403)
    if "upload" not in settings.file_transfers:
        return failed("uploads disabled", 403)
    root = getattr(settings, "file_manager_path", "") or ""
    if not root:
        return failed("uploads disabled", 403)
    root = os.path.normpath(os.path.expanduser(root))
    parts = self._file_parts(rel)
    if not parts:
        return failed("invalid upload path")
    dest = os.path.normpath(os.path.join(root, *parts))
    if not dest.startswith(os.path.join(root, "")):
        return failed("invalid upload path")
    # The destination's name in its directory, which the rename below acts
    # on through the directory's held descriptor.
    leaf = os.path.basename(dest)
    name = "/".join(parts)

    upload_id = request.headers.get("X-Upload-Id")
    offset_header = request.headers.get("X-Upload-Offset")
    if (upload_id is None) != (offset_header is None):
        return failed("X-Upload-Id and X-Upload-Offset must be sent together")

    total = -1
    if upload_id is not None:
        try:
            offset = int(offset_header or "")
            total = int(request.headers["X-Upload-Total"]) if "X-Upload-Total" in request.headers else -1
        except ValueError:
            return failed("malformed chunk headers")
        if offset < 0 or ("X-Upload-Total" in request.headers and total < 0):
            return failed("malformed chunk headers")

    # The write lands in a directory held open by the confinement
    # (`_open_dir_fd`): every component down to it was opened relative to
    # the one before and verified from the descriptor it opened to, so a
    # symlink planted over an intermediate name after the request's path
    # was validated cannot redirect the body or the final rename outside
    # the root, and `O_NOFOLLOW` on the staging leaf does the same for the
    # file itself.
    try:
        parent_fd = await asyncio.to_thread(self._open_dir_fd, parts[:-1], True)
    except FileRootEscape:
        return failed("path escape rejected")
    except OSError as e:
        return failed(f"mkdir failed: {e.strerror}", 500)
    try:
        if upload_id is None:
            staging = _staging_leaf_name()
            try:
                written = await self._stream_upload_body(
                    request, staging, parent_fd, append=False)
            except Exception as e:
                await asyncio.to_thread(_remove_quietly, staging, parent_fd)
                return failed(str(e))
            try:
                await asyncio.to_thread(_finalize_upload, staging, leaf, parent_fd)
            except OSError as e:
                await asyncio.to_thread(_remove_quietly, staging, parent_fd)
                return failed(f"finalize failed: {e.strerror}", 500)
            logger.info(f"HTTP upload finished: {dest} ({written} bytes)")
            audit.emit("file.upload.end", filename=name, size_bytes=written)
            return web.json_response({"status": "success", "bytes": written})

        self._expire_stale_chunked_uploads()
        final = request.headers.get("X-Upload-Final") == "1"
        state = self._chunked_uploads.get(dest)
        if offset == 0:
            if state is not None and state["busy"]:
                return web.json_response(
                    {"status": "error", "message": "another chunk for this path is in flight"},
                    status=409,
                )
            if state is not None:
                self._discard_chunked_upload(dest, state["part"])
            state = {"id": upload_id, "offset": 0, "ts": time.monotonic(),
                     "part": _upload_staging_path(dest), "busy": False}
            self._chunked_uploads[dest] = state
        else:
            if state is not None and state["busy"]:
                # Never discard here: the in-flight writer owns the .part.
                return web.json_response(
                    {"status": "error", "message": "another chunk for this path is in flight"},
                    status=409,
                )
            try:
                part_size = os.path.getsize(state["part"]) if state is not None else -1
            except OSError:
                part_size = -1
            if (state is None or state["id"] != upload_id
                    or state["offset"] != offset or part_size != offset):
                if state is not None:
                    self._discard_chunked_upload(dest, state["part"])
                return failed(f"chunk sequence mismatch at offset {offset}; transfer discarded", 409)
        part_path = state["part"]
        part_leaf = os.path.basename(part_path)

        # Claimed before the body's awaits so a slice for this path that arrives
        # meanwhile is refused, not interleaved onto the shared .part.
        state["busy"] = True
        try:
            written = await self._stream_upload_body(
                request, part_leaf, parent_fd, append=offset > 0)
        except Exception as e:
            self._discard_chunked_upload(dest, part_path)
            return failed(str(e))
        state["busy"] = False
        state["offset"] = offset + written
        state["ts"] = time.monotonic()

        if not final:
            return web.json_response({"status": "success", "bytes": state["offset"], "complete": False})

        received = state["offset"]
        if total >= 0 and received != total:
            self._discard_chunked_upload(dest, part_path)
            return failed(f"size mismatch: received {received}, expected {total}")
        # Synchronous, like the removal: a transfer is tracked by its destination,
        # so a restart on the same path cannot run between the rename and the entry's removal.
        try:
            _finalize_upload(part_leaf, leaf, parent_fd)
        except OSError as e:
            self._discard_chunked_upload(dest, part_path)
            return failed(f"finalize failed: {e.strerror}", 500)
        self._chunked_uploads.pop(dest, None)
        logger.info(f"HTTP chunked upload finished: {dest} ({received} bytes)")
        audit.emit("file.upload.end", filename=name, size_bytes=received)
        return web.json_response({"status": "success", "bytes": received, "complete": True})
    finally:
        try:
            os.close(parent_fd)
        except OSError:
            pass
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
func_file_parts(rel) -> Optional[List[str]]

Split a root-relative path into its name components, or None when it escapes the root. Empty and . components are dropped, and a .. surviving the normalization is the escape; a path that normalizes to nothing is no components, which is the root itself. A NUL byte names nothing on the filesystem and is refused with the escapes, rather than raising out of the calls that would see it.

Source Code
@staticmethod
def _file_parts(rel: str) -> Optional[List[str]]:
    """Split a root-relative path into its name components, or None when
    it escapes the root. Empty and ``.`` components are dropped, and a
    ``..`` surviving the normalization is the escape; a path that
    normalizes to nothing is no components, which is the root itself. A
    NUL byte names nothing on the filesystem and is refused with the
    escapes, rather than raising out of the calls that would see it."""
    if "\x00" in rel:
        return None
    rel = rel.strip("/\\")
    parts = [c for c in os.path.normpath(rel).split(os.sep) if c and c != "."]
    if rel and ".." in parts:
        return None
    return parts
paramrelstr

Returns

typing.Optional[typing.List[str]]
func_resolve_file_path(rel) -> Optional[str]

Resolve a root-relative path to a filesystem path at or under the file-manager directory. Returns None when the path escapes the root. An empty path resolves to the root itself, which browse may name and mkdir refuses through _at_file_root.

The confinement is the realpath-then-prefix comparison the static path checker recognizes, the prefix suffixed with the separator and the root itself allowed, so a sibling whose name extends the root's is not read as inside it. The endpoints that act rather than list do not act on the returned string: they open the directory through _open_dir_fd and _open_resolved_fd, whose descriptor holds the object this check validated.

Source Code
def _resolve_file_path(self, rel: str) -> Optional[str]:
    """Resolve a root-relative path to a filesystem path at or under the
    file-manager directory. Returns None when the path escapes the root. An
    empty path resolves to the root itself, which browse may name and mkdir
    refuses through `_at_file_root`.

    The confinement is the `realpath`-then-prefix comparison the static
    path checker recognizes, the prefix suffixed with the separator and
    the root itself allowed, so a sibling whose name extends the root's is
    not read as inside it. The endpoints that act rather than list do not
    act on the returned string: they open the directory through
    `_open_dir_fd` and `_open_resolved_fd`, whose descriptor holds the
    object this check validated."""
    parts = self._file_parts(rel)
    if parts is None:
        return None
    base = str(self.upload_dir)
    if not parts:
        return base
    parent = os.path.realpath(os.path.join(base, *parts[:-1]))
    full = os.path.join(parent, parts[-1])
    # After following in-directory symlinks the entry must still sit at or
    # under the root; a leaf symlink out of the tree is refused too.
    root = os.path.join(base, "")
    for resolved in (parent, os.path.realpath(full)):
        if resolved != base and not resolved.startswith(root):
            return None
    return full
paramrelstr

Returns

typing.Optional[str]
func_open_dir_fd(parts, create=False) -> int

Open the directory parts names under the file-manager root as a file descriptor, descending one component per os.open, each named relative to the descriptor before it and verified from the descriptor it opened to rather than by the name it went in under.

A symlinked component is followed the way the resolved string path followed it, and what makes that safe where the string was not is the verification: /proc/self/fd names where each open really landed, the kernel's own record of the object, which a name swapped after the open cannot change. A component that resolves outside the root — planted before the walk or in the instant of it — ends the walk with FileRootEscape instead of handing the caller an object to act outside with. With create, a missing component is made in place first, the way os.makedirs made the parents of an upload or a new folder; what the walk itself created is inside the descriptor it came from, so it needs no verification. POSIX only: dir_fd does not exist on Windows, and neither does this server.

Source Code
def _open_dir_fd(self, parts: List[str], create: bool = False) -> int:
    """Open the directory `parts` names under the file-manager root as a
    file descriptor, descending one component per `os.open`, each named
    relative to the descriptor before it and verified from the descriptor
    it opened to rather than by the name it went in under.

    A symlinked component is followed the way the resolved string path
    followed it, and what makes that safe where the string was not is the
    verification: `/proc/self/fd` names where each open really landed, the
    kernel's own record of the object, which a name swapped after the open
    cannot change. A component that resolves outside the root — planted
    before the walk or in the instant of it — ends the walk with
    `FileRootEscape` instead of handing the caller an object to act
    outside with. With `create`, a missing component is made in place
    first, the way `os.makedirs` made the parents of an upload or a new
    folder; what the walk itself created is inside the descriptor it came
    from, so it needs no verification. POSIX only: `dir_fd` does not exist
    on Windows, and neither does this server.

    Args:
        parts: Name components under the root; empty names the root.
        create: Make a missing intermediate directory.

    Returns:
        A descriptor for the directory, the caller's to close.

    Raises:
        FileRootEscape: A component resolved outside the root.
        OSError: A component is missing or is not a directory.
    """
    base = str(self.upload_dir)
    root = os.path.join(base, "")
    fd = os.open(base, os.O_RDONLY | os.O_DIRECTORY)
    try:
        for part in parts:
            if part in ("", ".", "..") or os.sep in part or (
                    os.altsep and os.altsep in part):
                raise ValueError(f"not a name component: {part!r}")
            try:
                opened = os.open(part, os.O_RDONLY | os.O_DIRECTORY, dir_fd=fd)
            except FileNotFoundError:
                if not create:
                    raise
                # A concurrent request may make it first; either way it is there now.
                with contextlib.suppress(FileExistsError):
                    os.mkdir(part, dir_fd=fd)
                opened = os.open(part, os.O_RDONLY | os.O_DIRECTORY, dir_fd=fd)
            try:
                resolved = os.readlink(f"/proc/self/fd/{opened}")
            except OSError:
                resolved = ""
            if resolved != base and not resolved.startswith(root):
                os.close(opened)
                raise FileRootEscape(part)
            os.close(fd)
            fd = opened
        return fd
    except BaseException:
        os.close(fd)
        raise
parampartsList[str]

Name components under the root; empty names the root.

paramcreatebool
= False

Make a missing intermediate directory.

Returns

int

A descriptor for the directory, the caller's to close.

func_open_resolved_fd(parts) -> Optional[Tuple[int, str]]

Open the directory parts names under the file-manager root as a file descriptor, and return it with the canonical path it opened to, or None when that sits outside the root.

The walk to the leaf verifies every intermediate component from the descriptor it opened (_open_dir_fd); the leaf itself is opened following symlinks and verified the same way, so an in-tree link to another directory in the tree stays listable the way the resolved string path allowed it while one resolving outside is refused. The verification reads /proc/self/fd, the kernel's own record of where the open landed, which a name swapped after the open cannot change: the caller acts on the verified object through the descriptor, never on a name that could have been swapped since.

Source Code
def _open_resolved_fd(self, parts: List[str]) -> Optional[Tuple[int, str]]:
    """Open the directory `parts` names under the file-manager root as a
    file descriptor, and return it with the canonical path it opened to,
    or None when that sits outside the root.

    The walk to the leaf verifies every intermediate component from the
    descriptor it opened (`_open_dir_fd`); the leaf itself is opened
    following symlinks and verified the same way, so an in-tree link to
    another directory in the tree stays listable the way the resolved
    string path allowed it while one resolving outside is refused. The
    verification reads `/proc/self/fd`, the kernel's own record of where
    the open landed, which a name swapped after the open cannot change:
    the caller acts on the verified object through the descriptor, never
    on a name that could have been swapped since.

    Args:
        parts: Name components under the root; empty names the root.

    Returns:
        The descriptor, the caller's to close, and its canonical path, or
        None when the entry resolved outside the root.

    Raises:
        NotADirectoryError: The leaf is not a directory. `O_DIRECTORY`
            refuses it before anything is read, so a FIFO cannot hold the
            open (and the worker thread making it) waiting for a writer.
        OSError: The leaf is missing or cannot be opened.
    """
    try:
        fd = self._open_dir_fd(parts[:-1])
    except FileRootEscape:
        return None
    try:
        if parts:
            opened = os.open(parts[-1], os.O_RDONLY | os.O_DIRECTORY, dir_fd=fd)
            os.close(fd)
            fd = opened
    except BaseException:
        os.close(fd)
        raise
    try:
        resolved = os.readlink(f"/proc/self/fd/{fd}")
    except OSError:
        os.close(fd)
        return None
    base = str(self.upload_dir)
    if resolved != base and not resolved.startswith(os.path.join(base, "")):
        os.close(fd)
        return None
    return fd, resolved
parampartsList[str]

Name components under the root; empty names the root.

Returns

typing.Optional

The descriptor, the caller's to close, and its canonical path, or

func_at_file_root(target) -> bool

Whether target is the file-manager directory itself rather than an entry within it.

An empty request path, and the . and a/.. spellings that normalize to one, all resolve to the root, so an endpoint that acts rather than lists refuses it: a directory is only ever created inside another, and creating the one the manager is rooted at is not an operation a client can mean. Browse may still name the root, which is the one directory the manager lists.

Source Code
def _at_file_root(self, target: str) -> bool:
    """Whether `target` is the file-manager directory itself rather than an entry within it.

    An empty request path, and the `.` and `a/..` spellings that normalize to
    one, all resolve to the root, so an endpoint that acts rather than lists
    refuses it: a directory is only ever created inside another, and creating
    the one the manager is rooted at is not an operation a client can mean.
    Browse may still name the root, which is the one directory the manager
    lists.
    """
    base = os.path.normcase(os.path.abspath(str(self.upload_dir)))
    return os.path.normcase(os.path.abspath(target)) == base
paramtargetstr

Returns

bool
func_viewer_or_disabled_files(request) -> Optional[str]

The message for a refused file-modification request, or None when the session is allowed to modify files. Uploads double as the writable toggle: a deployment that disables uploads serves the directory as read-only, and view-only credentials never modify files.

Source Code
def _viewer_or_disabled_files(self, request: web.Request) -> Optional[str]:
    """The message for a refused file-modification request, or None when the
    session is allowed to modify files. Uploads double as the writable
    toggle: a deployment that disables uploads serves the directory as
    read-only, and view-only credentials never modify files."""
    if self._viewer_ceiling(request):
        return "View-only credentials cannot modify files"
    if "upload" not in self.settings.file_transfers:
        return "File modifications disabled"
    if not str(self.upload_dir):
        return "File modifications disabled"
    return None
paramrequestweb.Request

Returns

typing.Optional[str]
func_json_body(request) -> Optional[Dict[str, Any]]
Source Code
async def _json_body(self, request: web.Request) -> Optional[Dict[str, Any]]:
    try:
        data = await request.json()
    except Exception:
        return None
    return data if isinstance(data, dict) else None
paramrequestweb.Request

Returns

typing.Optional[typing.Dict[str, typing.Any]]
func_browse_dir(parts, rel) -> Optional[List[Dict[str, Any]]]

Open the directory the request names and stat it into JSON entries.

Returns None when the entry resolved outside the root at open time, the race-only spelling of the refusal _resolve_file_path gives statically; a non-directory raises NotADirectoryError, which the handler answers like the missing case. The listing reads the pinned directory through its /proc/self/fd path — the descriptor the confinement opened, not a name that could have been swapped since the path was validated. Runs on the executor so a huge directory cannot stall the event loop that is streaming frames.

Source Code
def _browse_dir(self, parts: List[str], rel: str) -> Optional[List[Dict[str, Any]]]:
    """Open the directory the request names and stat it into JSON entries.

    Returns None when the entry resolved outside the root at open time,
    the race-only spelling of the refusal `_resolve_file_path` gives
    statically; a non-directory raises `NotADirectoryError`, which the
    handler answers like the missing case. The listing reads the pinned
    directory through its `/proc/self/fd` path — the descriptor the
    confinement opened, not a name that could have been swapped since the
    path was validated. Runs on the executor so a huge directory cannot
    stall the event loop that is streaming frames.

    Args:
        parts: Name components under the root; empty names the root.
        rel: The root-relative spelling the request used, for the entries'
            `path` fields.

    Returns:
        The entries, or None when the open landed outside the root.
    """
    opened = self._open_resolved_fd(parts)
    if opened is None:
        return None
    dir_fd, _ = opened
    try:
        if not stat.S_ISDIR(os.fstat(dir_fd).st_mode):
            raise NotADirectoryError(errno.ENOTDIR)
        entries: List[Dict[str, Any]] = []
        root = str(self.upload_dir)
        with os.scandir(f"/proc/self/fd/{dir_fd}") as it:
            for entry in it:
                if len(entries) >= FILE_LISTING_MAX_ENTRIES:
                    raise ListingTooLarge(rel)
                try:
                    st, is_dir = _listing_status(entry, root)
                except OSError:
                    continue
                rel_name = f"{rel}/{entry.name}".strip("/") if rel else entry.name
                entries.append({
                    "name": entry.name,
                    "path": rel_name,
                    "is_dir": is_dir,
                    "size": None if is_dir else st.st_size,
                    "mtime": datetime.fromtimestamp(st.st_mtime).isoformat(),
                })
    finally:
        os.close(dir_fd)
    entries.sort(key=lambda e: (not e["is_dir"], e["name"].lower()))
    return entries
parampartsList[str]

Name components under the root; empty names the root.

paramrelstr

The root-relative spelling the request used, for the entries' path fields.

Returns

typing.Optional

The entries, or None when the open landed outside the root.

funchandle_files_browse(request) -> web.Response

JSON directory listing for the shared file manager. The listing is read-only, but it carries both gates of the HTML index: the download permission, and the viewer ceiling, since a viewer watches the desktop and never reaches the files on it.

The listing reads the descriptor the confinement opened (_open_resolved_fd), never a path re-walked after the check, so a symlink swapped in after the path was validated cannot turn the listing into one of a directory outside the root.

Source Code
async def handle_files_browse(self, request: web.Request) -> web.Response:
    """JSON directory listing for the shared file manager. The listing is
    read-only, but it carries both gates of the HTML index: the download
    permission, and the viewer ceiling, since a viewer watches the desktop
    and never reaches the files on it.

    The listing reads the descriptor the confinement opened
    (`_open_resolved_fd`), never a path re-walked after the check, so a
    symlink swapped in after the path was validated cannot turn the
    listing into one of a directory outside the root."""
    if self._viewer_ceiling(request):
        return web.json_response({"status": "error", "message": "View-only credentials cannot list files"}, status=403)
    if "download" not in self.settings.file_transfers:
        return web.json_response({"status": "error", "message": "downloads disabled"}, status=403)
    rel_query = request.query.get("path", "")
    parts = self._file_parts(rel_query)
    if parts is None or self._resolve_file_path(rel_query) is None:
        return web.json_response({"status": "error", "message": "invalid path"}, status=400)
    rel = "/".join(parts)
    try:
        entries = await asyncio.to_thread(self._browse_dir, parts, rel)
    except (FileNotFoundError, NotADirectoryError):
        return web.json_response({"status": "error", "message": "not a directory"}, status=404)
    except PermissionError:
        return web.json_response({"status": "error", "message": "permission denied"}, status=403)
    except ListingTooLarge:
        return web.json_response(
            {"status": "error", "message": "the folder holds too many entries to list"}, status=413)
    if entries is None:
        return web.json_response({"status": "error", "message": "invalid path"}, status=400)
    return web.json_response({"status": "ok", "path": rel, "entries": entries})
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_files_mkdir(request) -> web.Response

POST /api/file-manager/mkdir: create a directory in the shared file manager. Refused for view-only credentials and where uploads are disabled, for a path that escapes the root, and for every spelling of the root itself, which nothing the manager offers has a use for: a directory is only ever created inside another. Refused to another site's page too (_fetched_cross_site): a form posting text/plain needs no preflight, request.json reads its body all the same, and the browser attaches a cached Basic login to it.

The directory is made inside a descriptor of its parent that the confinement walked (_open_dir_fd), so a symlink planted over an intermediate name after the path was validated cannot move the creation outside the root. A name already taken is the mkdir's own FileExistsError, not a check ahead of it that a concurrent request could invalidate.

Source Code
async def handle_files_mkdir(self, request: web.Request) -> web.Response:
    """POST /api/file-manager/mkdir: create a directory in the shared file
    manager. Refused for view-only credentials and where uploads are
    disabled, for a path that escapes the root, and for every spelling of
    the root itself, which nothing the manager offers has a use for: a
    directory is only ever created inside another. Refused to another
    site's page too (`_fetched_cross_site`): a form posting `text/plain`
    needs no preflight, `request.json` reads its body all the same, and the
    browser attaches a cached Basic login to it.

    The directory is made inside a descriptor of its parent that the
    confinement walked (`_open_dir_fd`), so a symlink planted over an
    intermediate name after the path was validated cannot move the
    creation outside the root. A name already taken is the `mkdir`'s own
    `FileExistsError`, not a check ahead of it that a concurrent request
    could invalidate."""
    refused = self._viewer_or_disabled_files(request)
    if refused:
        return web.json_response({"status": "error", "message": refused}, status=403)
    if self._fetched_cross_site(request, request.app["settings"]):
        logger.warning("Refused a new folder to another site's page (Origin %r)",
                       request.headers.get("Origin", ""))
        return web.json_response({"status": "error", "message": "Forbidden origin"}, status=403)
    data = await self._json_body(request)
    rel = (data or {}).get("path", "")
    if not isinstance(rel, str) or not rel.strip("/\\"):
        return web.json_response({"status": "error", "message": "invalid path"}, status=400)
    target = self._resolve_file_path(rel)
    if target is None or self._at_file_root(target):
        return web.json_response({"status": "error", "message": "invalid path"}, status=400)
    parts = self._file_parts(rel) or []

    def make() -> None:
        parent_fd = self._open_dir_fd(parts[:-1], create=True)
        try:
            os.mkdir(parts[-1], dir_fd=parent_fd)
        finally:
            os.close(parent_fd)

    try:
        await asyncio.to_thread(make)
    except FileRootEscape:
        return web.json_response({"status": "error", "message": "invalid path"}, status=400)
    except FileExistsError:
        return web.json_response({"status": "error", "message": "already exists"}, status=409)
    except NotADirectoryError:
        return web.json_response({"status": "error", "message": "a file is in the way"}, status=409)
    except OSError as e:
        return web.json_response({"status": "error", "message": e.strerror or "failed"}, status=500)
    audit.emit("file.mkdir", filename=rel)
    return web.json_response({"status": "ok"})
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_print_document(request) -> web.StreamResponse

GET /api/print/<name>: hand a printed document to the page that prints it, and take it out of the spool once it went out whole. Refused to view-only credentials: printing is the session owner's act. Refused to another site's page too (_fetched_cross_site), which would take the document out of the spool with the session's cached login.

Source Code
async def handle_print_document(self, request: web.Request) -> web.StreamResponse:
    """GET /api/print/<name>: hand a printed document to the page that
    prints it, and take it out of the spool once it went out whole.
    Refused to view-only credentials: printing is the session owner's act.
    Refused to another site's page too (`_fetched_cross_site`), which would
    take the document out of the spool with the session's cached login."""
    if not self.settings.printing_enabled[0]:
        return web.Response(status=403, text="Forbidden: printing disabled")
    if self._fetched_cross_site(request, self.settings):
        logger.warning("Refused a printed document to another site's page (Origin %r)",
                       request.headers.get("Origin", ""))
        return web.Response(status=403, text="Forbidden origin")
    if self._viewer_ceiling(request):
        return web.Response(status=403, text="View-only credentials cannot take printed documents")
    name = printing.document_name(request.match_info.get("name", ""))
    base = str(self.print_spool)
    full = os.path.normpath(os.path.join(base, name)) if name else ""
    if not full.startswith(base + os.sep) or not os.path.isfile(full):
        return web.Response(status=404, text="No such document")
    return _AuditedFileResponse(pathlib.Path(full), name, event="print.document", remove=True,
                                headers={"Content-Disposition": "inline"})
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.StreamResponse
funchandle_apps_catalog(request) -> web.StreamResponse

GET /api/apps/metadata.yml: the catalog of the proot-apps local repository the session installs from (apps_catalog). Nothing is served where no repository is configured, since the page then reads the remote catalog; a configured directory that is missing is the same answer, and the panel reports the catalog as failed to load rather than offering installs the runner cannot make.

Source Code
async def handle_apps_catalog(self, request: web.Request) -> web.StreamResponse:
    """GET /api/apps/metadata.yml: the catalog of the proot-apps local
    repository the session installs from (`apps_catalog`). Nothing is
    served where no repository is configured, since the page then reads
    the remote catalog; a configured directory that is missing is the
    same answer, and the panel reports the catalog as failed to load
    rather than offering installs the runner cannot make."""
    text = apps_catalog.catalog_text(apps_repo_folder())
    if text is None:
        return web.Response(status=404, text="No local apps repository")
    return web.Response(text=text, content_type="text/yaml",
                        headers={"Cache-Control": "no-cache"})
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.StreamResponse
funchandle_apps_icon(request) -> web.StreamResponse

GET /api/apps/img/<name>: one icon of the local repository's catalog, served from its metadata/img directory and nowhere else.

Source Code
async def handle_apps_icon(self, request: web.Request) -> web.StreamResponse:
    """GET /api/apps/img/<name>: one icon of the local repository's
    catalog, served from its `metadata/img` directory and nowhere else."""
    path = apps_catalog.icon_path(apps_repo_folder(), request.match_info.get("name", ""))
    if path is None:
        return web.Response(status=404, text="No such icon")
    return web.FileResponse(pathlib.Path(path))
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.StreamResponse
func_active_service() -> Optional[BaseStreamingService]
Source Code
def _active_service(self) -> Optional[BaseStreamingService]:
    return self.services.get(self.current_mode) if self.current_mode else None

Returns

typing.Optional[selkies.stream_server.BaseStreamingService]
funchandle_sessions(request) -> web.Response

GET /api/sessions: the pages connected to the active transport.

Source Code
async def handle_sessions(self, request: web.Request) -> web.Response:
    """GET /api/sessions: the pages connected to the active transport."""
    service = self._active_service()
    return web.json_response({"sessions": await service.sessions() if service else []})
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_session_delete(request) -> web.Response

DELETE /api/sessions/<id>: close that page's connection. Refused to view-only credentials, like every change to the session.

Source Code
async def handle_session_delete(self, request: web.Request) -> web.Response:
    """DELETE /api/sessions/<id>: close that page's connection. Refused to
    view-only credentials, like every change to the session."""
    if self._viewer_ceiling(request):
        return web.Response(status=403, text="View-only credentials cannot disconnect a session")
    service = self._active_service()
    if service is None or not await service.disconnect_session(request.match_info["id"]):
        return web.Response(status=404, text="No such session")
    return web.Response(status=204)
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_recording(request) -> web.Response

GET, POST, and DELETE /api/recording: the MP4 recording pixelflux makes of the session, H.264 with the session's audio as an Opus track when audio is on and pcmflux is installed. POST starts one into the file-manager directory unless the body names a path, DELETE stops it and GET reports on the current or last one. One recording at a time; starting a second or stopping none is a conflict.

Source Code
async def handle_recording(self, request: web.Request) -> web.Response:
    """GET, POST, and DELETE /api/recording: the MP4 recording pixelflux
    makes of the session, H.264 with the session's audio as an Opus track
    when audio is on and pcmflux is installed. POST starts one into the
    file-manager directory unless the body names a path, DELETE stops it
    and GET reports on the current or last one. One recording at a time;
    starting a second or stopping none is a conflict."""
    try:
        import pixelflux
    except ImportError:
        return web.Response(status=501, text="pixelflux is not installed")
    if request.method == "GET":
        return web.json_response(pixelflux.recording_status() or {"active": False})
    if self._viewer_ceiling(request):
        return web.Response(status=403, text="View-only credentials cannot record")
    try:
        if request.method == "POST":
            body = await request.json() if request.can_read_body else {}
            path = str((body or {}).get("path") or "")
            if not path:
                path = "recording-" + datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + ".mp4"
            target = self._recording_target(path)
            if target is None:
                logger.warning(f"Refused a recording outside the file-manager directory: {path!r}")
                return web.Response(status=400, text="Recording path leaves the file-manager directory")
            path = target
            if (pixelflux.recording_status() or {}).get("active"):
                raise RuntimeError("a recording is already active")
            audio_socket = await self._start_recording_audio(pixelflux)
            args = (path, None, audio_socket) if audio_socket else (path,)
            try:
                status = await asyncio.to_thread(pixelflux.start_recording, *args)
            except RuntimeError:
                await self._stop_recording_audio()
                raise
            audit.emit("recording.start", filename=status.get("path", path))
        else:
            status = await asyncio.to_thread(pixelflux.stop_recording)
            await self._stop_recording_audio()
            audit.emit("recording.stop", filename=status.get("path", ""),
                       size_bytes=status.get("bytes", 0), duration_s=status.get("duration_s", 0.0),
                       frames=status.get("frames", 0))
    except json.JSONDecodeError:
        return web.Response(status=400, text="Body is not JSON")
    except RuntimeError as exc:
        logger.warning(f"Recording request refused: {exc}")
        return web.Response(status=409, text="Recording request refused; the server log names the reason")
    return web.json_response(status)
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
func_recording_target(name) -> Optional[str]

Where a requested recording name writes, or None when it leaves the file-manager directory. The name is a path relative to that directory: an absolute path, a traversal segment, and a symlinked parent pointing outside are all refused, since the session token that reaches this endpoint carries no authority over the rest of the filesystem.

Source Code
def _recording_target(self, name: str) -> Optional[str]:
    """Where a requested recording name writes, or `None` when it leaves the
    file-manager directory. The name is a path relative to that directory:
    an absolute path, a traversal segment, and a symlinked parent pointing
    outside are all refused, since the session token that reaches this
    endpoint carries no authority over the rest of the filesystem."""
    base = os.path.realpath(self.upload_dir)
    if os.path.isabs(name):
        return None
    target = os.path.realpath(os.path.join(base, name))
    if not target.startswith(base + os.sep):
        return None
    return target
paramnamestr

Returns

typing.Optional[str]
func_start_recording_audio(pixelflux) -> str

The Ogg Opus socket a recording's audio track is read from, served by a pcmflux capture of the session's sink that runs for the recorder alone with no Python callback, so no frame passes through Python. Unlike the live streams' captures it encodes silence rather than gating it, so the track spans the recording instead of stopping at every silence. Empty for a video-only recording: audio off, a pixelflux or pcmflux without the socket, or a capture that does not start.

Source Code
async def _start_recording_audio(self, pixelflux: Any) -> str:
    """The Ogg Opus socket a recording's audio track is read from, served
    by a pcmflux capture of the session's sink that runs for the recorder
    alone with no Python callback, so no frame passes through Python.
    Unlike the live streams' captures it encodes silence rather than
    gating it, so the track spans the recording instead of stopping at
    every silence. Empty for a video-only recording: audio off, a
    pixelflux or pcmflux without the socket, or a capture that does not
    start."""
    try:
        if not self.settings.audio_enabled[0] or \
                "audio_socket" not in inspect.signature(pixelflux.start_recording).parameters:
            return ""
        from pcmflux import AudioCapture
        from .audio_control import ensure_capture_sink, opus_capture_settings
        capture_settings = opus_capture_settings(self.settings.audio_device_name, self.settings.audio_channels,
                                                 int(self.settings.audio_bitrate), 20.0)
        capture_settings.use_silence_gate = False
        runtime = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
        capture_settings.output_socket = os.path.join(runtime, f"selkies-record-audio-{os.getpid()}.sock")
    except (ImportError, AttributeError, ValueError):
        return ""
    capture = AudioCapture()
    try:
        await ensure_capture_sink(self.settings.audio_device_name)
        await asyncio.to_thread(capture.start_capture, capture_settings)
        if capture.state == "failed" or capture.last_error:
            raise RuntimeError(f"capture {capture.state}: {capture.last_error}")
    except (RuntimeError, OSError) as exc:
        logger.warning(f"Recording without audio: {exc}")
        await asyncio.to_thread(capture.stop_capture)
        return ""
    self._recording_audio = capture
    return capture_settings.output_socket
parampixelfluxAny

Returns

str
func_stop_recording_audio() -> None
Source Code
async def _stop_recording_audio(self) -> None:
    capture, self._recording_audio = self._recording_audio, None
    if capture is not None:
        await asyncio.to_thread(capture.stop_capture)

Returns

None
funchandle_screenshot(request) -> web.Response

GET /api/screenshot?display=<name>: a PNG of that display, by pixelflux. On X11 the root, which holds every display, with the cursor drawn in; on Wayland the output, with the cursor where the stream draws it into the picture. A display pixelflux does not know is not found; a screenshot it is too busy for or that timed out is a 503 to retry. A screenshot handed out is on the audit trail with the role that took it.

Source Code
async def handle_screenshot(self, request: web.Request) -> web.Response:
    """GET /api/screenshot?display=<name>: a PNG of that display, by
    pixelflux. On X11 the root, which holds every display, with the cursor
    drawn in; on Wayland the output, with the cursor where the stream draws
    it into the picture. A display pixelflux does not know is not found; a
    screenshot it is too busy for or that timed out is a 503 to retry. A
    screenshot handed out is on the audit trail with the role that took it."""
    try:
        import pixelflux
        screenshot = pixelflux.screenshot_png
    except (ImportError, AttributeError):
        return web.Response(status=501, text="pixelflux without screenshots")
    display = request.query.get("display", "primary")
    output = 0
    if self.settings.wayland[0] and display != "primary":
        from .display_utils import wayland_output_id
        output = wayland_output_id(display)
    try:
        png = await asyncio.to_thread(screenshot, output)
    except RuntimeError as exc:
        logger.warning(f"Screenshot of display {display!r} refused: {exc}")
        reason = str(exc)
        if reason.startswith("Unknown display") or reason == "Screenshot output was removed":
            return web.Response(status=404, text="No such display")
        if reason.startswith("Screenshot busy") or reason == "Screenshot timed out":
            return web.Response(status=503, text="Screenshot busy", headers={"Retry-After": "1"})
        return web.Response(status=500, text="Screenshot failed")
    audit.emit("screenshot", display=display, size_bytes=len(png),
               role="viewer" if self._viewer_ceiling(request) else "controller")
    return web.Response(body=png, content_type="image/png")
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
funcpending_print_documents() -> List[Tuple[str, int]]

The documents no page has taken yet, for a page that connects now.

Source Code
def pending_print_documents(self) -> List[Tuple[str, int]]:
    """The documents no page has taken yet, for a page that connects now."""
    return printing.pending(str(self.print_spool)) if self.print_watcher else []

Returns

typing.List[typing.Tuple[str, int]]
func_announce_print_document(name, size) -> None
Source Code
async def _announce_print_document(self, name: str, size: int) -> None:
    service = self.services.get(self.current_mode) if self.current_mode else None
    if service is not None:
        await service.announce_print_document(name, size)
paramnamestr
paramsizeint

Returns

None
funchandle_status(_) -> web.Response

GET /api/status: current mode, available modes, dual-mode flag.

Source Code
async def handle_status(self, _: web.Request) -> web.Response:
    """GET /api/status: current mode, available modes, dual-mode flag."""
    status = self._get_status()
    return web.json_response(status)
param_web.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_health(_) -> web.Response

GET /api/health: liveness probe, always 200.

Source Code
async def handle_health(self, _: web.Request) -> web.Response:
    """GET /api/health: liveness probe, always 200."""
    return web.Response(text="OK")
param_web.Request

Returns

aiohttp.web.aiohttp.web.Response
funchandle_metrics(request) -> web.Response

Prometheus exposition of the process-global registry.

Source Code
async def handle_metrics(self, request: web.Request) -> web.Response:
    """Prometheus exposition of the process-global registry."""
    data = await asyncio.to_thread(generate_latest)
    return web.Response(
        body=data,
        content_type='text/plain; version=1.0.0',
        headers={
            'Cache-Control': 'no-cache, no-store, must-revalidate',
            'Pragma': 'no-cache',
            'Expires': '0'
        }
    )
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.Response
func_get_static_content_path() -> str

Resolve the directory the static frontend is served from.

A configured web_root wins when it holds an index.html; otherwise the packaged web files are extracted to a temporary directory (importlib traversables are not guaranteed to be real files, and aiohttp static routes need a filesystem path).

Source Code
async def _get_static_content_path(self) -> str:
    """Resolve the directory the static frontend is served from.

    A configured ``web_root`` wins when it holds an ``index.html``;
    otherwise the packaged web files are extracted to a temporary directory
    (importlib traversables are not guaranteed to be real files, and aiohttp
    static routes need a filesystem path).

    Returns:
        The directory path, or an empty string when no usable content exists.
    """
    web_path = ""

    web_root = getattr(self.settings, "web_root", "")
    if web_root:
        web_path = os.path.expanduser(web_root)
        if os.path.isdir(web_path) and os.path.isfile(os.path.join(web_path, "index.html")):
            logger.info(f"Using custom web_root directory: {web_path}")
            return web_path
        logger.warning(f"web_root directory {web_path} not found or missing index.html")

    logger.debug("Defaulting to packaged web files.")
    try:
        package_path = importlib_resources.files(self.STATIC_CONTENT_PATH)
        self.web_files_ctx = tempfile.TemporaryDirectory(prefix="selkies_web")
        temp_path = pathlib.Path(self.web_files_ctx.name)
        await asyncio.to_thread(self._copy_traversable, package_path, temp_path)

        if (temp_path / "index.html").exists():
            logger.debug(f"Using extracted package path from temp dir: {temp_path}")
            return str(temp_path)
        else:
            logger.warning("Packaged web content missing index.html")
            self.web_files_ctx.cleanup()
    except Exception as e:
        logger.error(f"Failed to extract packaged web files: {e}")
        # Unset when extraction failed before its assignment.
        if self.web_files_ctx is not None:
            try:
                self.web_files_ctx.cleanup()
            except Exception:
                pass

    return ""

Returns

str

The directory path, or an empty string when no usable content exists.

func_copy_traversable(src, dst) -> None

Recursively copy a Traversable (file or directory) to a filesystem path.

Source Code
def _copy_traversable(self, src: Any, dst: pathlib.Path) -> None:
    """Recursively copy a Traversable (file or directory) to a filesystem path."""
    if src.is_file():
        with src.open('rb') as f_src, open(dst, 'wb') as f_dst:
            shutil.copyfileobj(f_src, f_dst)
    elif src.is_dir():
        dst.mkdir(exist_ok=True)
        for child in src.iterdir():
            self._copy_traversable(child, dst / child.name)
paramsrcAny
paramdstpathlib.Path

Returns

None
func_serve_paced_download(request, path, *, audit_name, download_name=None, allow_fallback=True) -> web.StreamResponse

Stream path to the page as an attachment with the download pacing, or answer with _AuditedFileResponse when nothing is paced and allow_fallback allows it.

download_name is the name the attachment is offered under and audit_name the one the audit trail records, distinct because a generated archive is built under a temporary name and offered under the folder's. allow_fallback is False for a file that exists only for the length of the transfer: FileResponse opens it after the handler returns, by which time the temporary is gone. Such a file is never handed to FileResponse, so a request the streaming path cannot answer (a HEAD, a Range resume, a conditional revalidation) is refused with 405.

The operator's static cap is the shared transfer_cap bucket, and congestion control is download_pacer fed one UplinkGauge verdict per chunk over the downloader's own session socket, so the transfer takes whatever the downlink has spare and backs off the moment the session's delay inflates. The gauge is end to end, so a reverse proxy or a fat modem buffer absorbing this host's writes still shows as the round trip it costs the session. Chunks are sized to the current rate (_gauged_chunk_size): each lands on the link as a burst, and a fixed 256 KiB would read as congestion on any link that cannot swallow it inside the gauge's delay budget. A file below TRANSFER_MIN_GAUGED_BYTES skips the gauge, as does a request with no gauge-able session socket; with no cap either, nothing is paced.

Pacing applies to plain GETs only: HEAD must answer instantly (StreamResponse.write is not empty-body-aware, so it would read and pace the whole file), a Range request is a resume or a seek that pacing a partial stream buys nothing for while FileResponse negotiates the 206, and a conditional request exists to be answered with 304/412 from validators only FileResponse computes.

A client that cancels a download closes its connection mid-stream, which surfaces as a connection error from the write; that is the transfer's normal end, logged as such and never a handler failure.

Source Code
async def _serve_paced_download(
    self,
    request: web.Request,
    path: pathlib.Path,
    *,
    audit_name: str,
    download_name: Optional[str] = None,
    allow_fallback: bool = True,
) -> web.StreamResponse:
    """Stream `path` to the page as an attachment with the download pacing,
    or answer with `_AuditedFileResponse` when nothing is paced and
    `allow_fallback` allows it.

    `download_name` is the name the attachment is offered under and
    `audit_name` the one the audit trail records, distinct because a
    generated archive is built under a temporary name and offered under the
    folder's. `allow_fallback` is False for a file that exists only for the
    length of the transfer: FileResponse opens it after the handler returns,
    by which time the temporary is gone. Such a file is never handed to
    FileResponse, so a request the streaming path cannot answer (a HEAD, a
    `Range` resume, a conditional revalidation) is refused with 405.

    The operator's static cap is the shared `transfer_cap` bucket, and
    congestion control is `download_pacer` fed one `UplinkGauge` verdict per
    chunk over the downloader's own session socket, so the transfer takes
    whatever the downlink has spare and backs off the moment the session's
    delay inflates. The gauge is end to end, so a reverse proxy or a fat
    modem buffer absorbing this host's writes still shows as the round trip
    it costs the session. Chunks are sized to the current rate
    (`_gauged_chunk_size`): each lands on the link as a burst, and a fixed
    256 KiB would read as congestion on any link that cannot swallow it
    inside the gauge's delay budget. A file below
    `TRANSFER_MIN_GAUGED_BYTES` skips the gauge, as does a request with no
    gauge-able session socket; with no cap either, nothing is paced.

    Pacing applies to plain GETs only: HEAD must answer instantly
    (StreamResponse.write is not empty-body-aware, so it would read and
    pace the whole file), a Range request is a resume or a seek that
    pacing a partial stream buys nothing for while FileResponse
    negotiates the 206, and a conditional request exists to be answered
    with 304/412 from validators only FileResponse computes.

    A client that cancels a download closes its connection mid-stream,
    which surfaces as a connection error from the write; that is the
    transfer's normal end, logged as such and never a handler failure.
    """
    name = download_name or path.name
    # The quoted-string fallback takes printable ASCII alone: a control
    # character (a newline is a legal Linux filename byte) would break the
    # header, and a quote or backslash would end or escape the string.
    filename = "".join(c if " " <= c <= "~" and c not in '"\\' else "_" for c in name)
    quoted = urllib.parse.quote(name.encode("utf-8", "surrogateescape"))
    conditional = any(
        name in request.headers
        for name in ("If-Modified-Since", "If-None-Match",
                     "If-Range", "If-Unmodified-Since")
    )
    size = (await asyncio.to_thread(path.stat)).st_size
    cap = self.transfer_cap if self.transfer_cap.active else None
    gauge = (
        self._session_gauge(request)
        if size >= TRANSFER_MIN_GAUGED_BYTES else None)
    if not ((cap is not None or gauge is not None or not allow_fallback)
            and request.method == "GET"
            and "Range" not in request.headers and not conditional):
        if not allow_fallback:
            return web.Response(status=405, headers={"Allow": "GET"})
        return _AuditedFileResponse(
            path, audit_name,
            headers={
                "Content-Disposition":
                    f'attachment; filename="{filename}"; filename*=UTF-8\'\'{quoted}'
            },
        )
    logger.info(
        f"Download '{filename}' ({size} bytes): "
        f"{'gauged' if gauge is not None else 'ungauged'}"
        + (f", capped at {cap.static_bps / 125000:.1f} Mbit/s"
           if cap is not None else ""))
    content_type = (
        mimetypes.guess_type(download_name or path.name)[0]
        or "application/octet-stream"
    )
    response = web.StreamResponse(
        status=200,
        headers={
            "Content-Type": content_type,
            "Content-Length": str(size),
            "Accept-Ranges": "bytes",
            "Content-Disposition":
                f'attachment; filename="{filename}"; filename*=UTF-8\'\'{quoted}',
        },
    )
    await response.prepare(request)
    fh = await asyncio.to_thread(open, path, "rb")
    sent = 0
    try:
        while True:
            chunk_size = (
                _gauged_chunk_size(self.download_pacer)
                if gauge is not None else 1 << 20)
            chunk = await asyncio.to_thread(fh.read, chunk_size)
            if not chunk:
                break
            if cap is not None:
                await cap.pace(len(chunk))
            if gauge is not None:
                verdict = await gauge.sample()
                if gauge.alive:
                    await self.download_pacer.pace_verdict(len(chunk), verdict, gauge.inflation_us)
                else:
                    gauge = None
            await response.write(chunk)
            sent += len(chunk)
        await response.write_eof()
        audit.emit("file.download", filename=audit_name, size_bytes=size, partial=False)
    except ConnectionError as e:
        logger.info(
            f"Download '{filename}' stopped by the client "
            f"after {sent} of {size} bytes: {e}")
    finally:
        await asyncio.to_thread(fh.close)
    return response
paramrequestweb.Request
parampathpathlib.Path
paramaudit_namestr
paramdownload_nameOptional[str]
= None
paramallow_fallbackbool
= True

Returns

aiohttp.web.aiohttp.web.StreamResponse
func_archive_source_bounds(dir_fd) -> Tuple[int, int]

Walk the directory dir_fd holds without reading file contents and return the total size and count of the regular files an archive of it would carry, stopping as soon as either is past its bound.

The walk is the one _archive_files makes (os.fwalk: a symlinked directory is never descended, and only regular files count), by lstat rather than an open; an entry that vanishes or turns unreadable mid-walk is skipped rather than failing the request.

Source Code
@staticmethod
def _archive_source_bounds(dir_fd: int) -> Tuple[int, int]:
    """Walk the directory `dir_fd` holds without reading file contents and
    return the total size and count of the regular files an archive of it
    would carry, stopping as soon as either is past its bound.

    The walk is the one `_archive_files` makes (`os.fwalk`: a symlinked
    directory is never descended, and only regular files count), by
    `lstat` rather than an open; an entry that vanishes or turns
    unreadable mid-walk is skipped rather than failing the request.
    """
    total = 0
    count = 0
    for _current, _directories, filenames, current_fd in os.fwalk(".", dir_fd=dir_fd):
        for name in filenames:
            try:
                st = os.stat(name, dir_fd=current_fd, follow_symlinks=False)
            except OSError:
                continue
            if not stat.S_ISREG(st.st_mode):
                continue
            total += st.st_size
            count += 1
            if total > ARCHIVE_MAX_SOURCE_BYTES or count > ARCHIVE_MAX_ENTRIES:
                return total, count
    return total, count
paramdir_fdint

Returns

typing.Tuple[int, int]
func_archive_files(dir_fd) -> Iterator[Tuple[str, int, os.stat_result]]

Yield each regular file under the directory dir_fd holds: its name relative to that directory, an open descriptor of it (the caller's to close), and its status.

os.fwalk never descends a symlinked directory, and each file is opened by name relative to its own directory's descriptor with O_NOFOLLOW, so no symlink, planted before the walk or swapped in during it, is followed out of the tree. O_NONBLOCK keeps a FIFO from holding the open, and anything the descriptor does not show to be a regular file is left out: a FIFO or a socket would wedge the read, and a device would inflate the archive without carrying the tree's content. An entry that vanishes or cannot be read is skipped.

Source Code
@staticmethod
def _archive_files(dir_fd: int) -> Iterator[Tuple[str, int, os.stat_result]]:
    """Yield each regular file under the directory `dir_fd` holds: its
    name relative to that directory, an open descriptor of it (the
    caller's to close), and its status.

    `os.fwalk` never descends a symlinked directory, and each file is
    opened by name relative to its own directory's descriptor with
    `O_NOFOLLOW`, so no symlink, planted before the walk or swapped in
    during it, is followed out of the tree. `O_NONBLOCK` keeps a FIFO from
    holding the open, and anything the descriptor does not show to be a
    regular file is left out: a FIFO or a socket would wedge the read, and
    a device would inflate the archive without carrying the tree's
    content. An entry that vanishes or cannot be read is skipped.
    """
    for current, _directories, filenames, current_fd in os.fwalk(".", dir_fd=dir_fd):
        for name in filenames:
            try:
                fd = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=current_fd)
            except OSError:
                continue
            try:
                st = os.fstat(fd)
            except OSError:
                os.close(fd)
                continue
            if not stat.S_ISREG(st.st_mode):
                os.close(fd)
                continue
            yield os.path.normpath(os.path.join(current, name)), fd, st
paramdir_fdint

Returns

typing.Iterator[typing.Tuple[str, int, os.os.stat_result]]
func_build_directory_archive(dir_fd) -> str

Write the regular files under the directory dir_fd holds to a zip archive in the system temporary directory, and return its path.

Every file is read through the descriptor _archive_files opened, so a link inside the tree pointing out of it cannot put content outside the root into an archive the user asked for the tree's contents of, and neither can one swapped in while the pack runs. The pack holds to the bounds _archive_source_bounds checked as it reads, so a file that grows between the two cannot carry the archive past them. A modification time a zip entry cannot hold (before 1980 or after 2107) is clamped rather than failing the pack.

Source Code
@classmethod
def _build_directory_archive(cls, dir_fd: int) -> str:
    """Write the regular files under the directory `dir_fd` holds to a zip
    archive in the system temporary directory, and return its path.

    Every file is read through the descriptor `_archive_files` opened, so
    a link inside the tree pointing out of it cannot put content outside
    the root into an archive the user asked for the tree's contents of,
    and neither can one swapped in while the pack runs. The pack holds to
    the bounds `_archive_source_bounds` checked as it reads, so a file that
    grows between the two cannot carry the archive past them. A
    modification time a zip entry cannot hold (before 1980 or after 2107)
    is clamped rather than failing the pack.

    Raises:
        ArchiveTooLarge: The files read past `ARCHIVE_MAX_SOURCE_BYTES` or
            `ARCHIVE_MAX_ENTRIES`.
    """
    fd, staged = tempfile.mkstemp(prefix="selkies-files-", suffix=".zip")
    os.close(fd)
    carried = 0
    entries = 0
    try:
        with zipfile.ZipFile(staged, "w", zipfile.ZIP_DEFLATED) as archive:
            for name, file_fd, st in cls._archive_files(dir_fd):
                with os.fdopen(file_fd, "rb") as source:
                    entries += 1
                    if entries > ARCHIVE_MAX_ENTRIES:
                        raise ArchiveTooLarge(name)
                    # A name that is not UTF-8 is carried with its stray
                    # bytes replaced, which zipfile could not encode at all.
                    info = zipfile.ZipInfo(os.fsencode(name).decode("utf-8", "replace"),
                                           _zip_date_time(st.st_mtime))
                    info.compress_type = zipfile.ZIP_DEFLATED
                    # Permission bits only: no set-ID or sticky bit rides
                    # into an archive that another machine may extract.
                    info.external_attr = (stat.S_IFREG | (stat.S_IMODE(st.st_mode) & 0o777)) << 16
                    info.file_size = st.st_size
                    with archive.open(info, "w", force_zip64=st.st_size >= ARCHIVE_ZIP64_FROM) as sink:
                        while True:
                            chunk = source.read(1 << 20)
                            if not chunk:
                                break
                            carried += len(chunk)
                            if carried > ARCHIVE_MAX_SOURCE_BYTES:
                                raise ArchiveTooLarge(name)
                            sink.write(chunk)
    except BaseException:
        with contextlib.suppress(OSError):
            os.remove(staged)
        raise
    return staged
paramdir_fdint

Returns

str
func_pack_directory(parts) -> Optional[str]

Open the directory parts names under the file-manager root, bound it, and pack it (_build_directory_archive); return the archive's path, or None when the directory resolved outside the root.

Runs whole on one ARCHIVE_PACK_POOL worker with the directory's descriptor: the descriptor is opened, walked, and closed by the same call, so a request abandoned while the pack runs cannot close it, or have its number reused, under the walk. The bound comes first, so a folder past ARCHIVE_MAX_SOURCE_BYTES or ARCHIVE_MAX_ENTRIES is refused before a byte is staged.

Source Code
def _pack_directory(self, parts: List[str]) -> Optional[str]:
    """Open the directory `parts` names under the file-manager root, bound
    it, and pack it (`_build_directory_archive`); return the archive's
    path, or None when the directory resolved outside the root.

    Runs whole on one `ARCHIVE_PACK_POOL` worker with the directory's
    descriptor: the descriptor is opened, walked, and closed by the same
    call, so a request abandoned while the pack runs cannot close it, or
    have its number reused, under the walk. The bound comes first, so a
    folder past `ARCHIVE_MAX_SOURCE_BYTES` or `ARCHIVE_MAX_ENTRIES` is
    refused before a byte is staged.

    Raises:
        ArchiveTooLarge: The folder holds more than one archive may carry.
        OSError: The directory is missing, is not one, or cannot be opened.
    """
    opened = self._open_resolved_fd(parts)
    if opened is None:
        return None
    dir_fd, _ = opened
    try:
        total, count = self._archive_source_bounds(dir_fd)
        if total > ARCHIVE_MAX_SOURCE_BYTES or count > ARCHIVE_MAX_ENTRIES:
            raise ArchiveTooLarge("/".join(parts))
        return self._build_directory_archive(dir_fd)
    finally:
        os.close(dir_fd)
parampartsList[str]

Returns

typing.Optional[str]
funchandle_files_zip(request) -> web.StreamResponse

GET /api/file-manager/zip?path=<dir>: hand a directory to the page as one zip archive.

A folder has no single file to fetch, so the archive is what a folder's Download button asks for. zipfile cannot stream, so the folder is packed to a temporary file first and the response starts once the whole archive exists; the wait before the first byte is that packing, and the temporary is unlinked once the transfer ends however it ends. Symlinks are not followed (_archive_files). The root is refused like every endpoint that acts rather than lists, so a folder download is always a folder within the manager and never the whole served tree in one request. View-only credentials are refused the archive like the listing and the bytes (_viewer_ceiling): a viewer watches the desktop, it does not take the files on it. Another site's page is refused too (_fetched_cross_site): an image or a link there would spend the pack on the session's cached login, and the page's own Download button is a same-origin navigation.

The folder is bounded before anything is staged, and a folder past ARCHIVE_MAX_SOURCE_BYTES or ARCHIVE_MAX_ENTRIES is refused with 413, because the pack runs to completion ahead of the first byte and the pacing below only throttles the network side. The pack runs on a pool of two (ARCHIVE_PACK_POOL), so concurrent requests queue for it rather than each staging a full copy. The folder is opened through the confinement's descriptor and walked from it (_pack_directory), so the pack reads the object the check opened rather than a name a symlink planted between the two could have swapped.

The archive rides the same pacing the file download does (_serve_paced_download), so a folder the size of a video directory still backs off with the session's delay instead of taking whatever the downlink accepts. It is streamed rather than handed to FileResponse, since the temporary does not outlive the handler, and the route is GET alone for the same reason: a HEAD, a Range, or a conditional request would want FileResponse to answer from a file that is gone, and each is refused before the folder is packed rather than after it.

Source Code
async def handle_files_zip(self, request: web.Request) -> web.StreamResponse:
    """GET /api/file-manager/zip?path=<dir>: hand a directory to the page
    as one zip archive.

    A folder has no single file to fetch, so the archive is what a folder's
    Download button asks for. `zipfile` cannot stream, so the folder is
    packed to a temporary file first and the response starts once the whole
    archive exists; the wait before the first byte is that packing, and the
    temporary is unlinked once the transfer ends however it ends. Symlinks
    are not followed (`_archive_files`). The root is refused like every
    endpoint that acts rather than lists, so a folder download is always a
    folder within the manager and never the whole served tree in one
    request. View-only credentials are refused the archive like the listing
    and the bytes (`_viewer_ceiling`): a viewer watches the desktop, it
    does not take the files on it. Another site's page is refused too
    (`_fetched_cross_site`): an image or a link there would spend the pack
    on the session's cached login, and the page's own Download button is
    a same-origin navigation.

    The folder is bounded before anything is staged, and a folder past
    `ARCHIVE_MAX_SOURCE_BYTES` or `ARCHIVE_MAX_ENTRIES` is refused with
    413, because the pack runs to completion ahead of the first byte and
    the pacing below only throttles the network side. The pack runs on a
    pool of two (`ARCHIVE_PACK_POOL`), so concurrent requests queue for it
    rather than each staging a full copy. The folder is opened through the
    confinement's descriptor and walked from it (`_pack_directory`), so
    the pack reads the object the check opened rather than a name a
    symlink planted between the two could have swapped.

    The archive rides the same pacing the file download does
    (`_serve_paced_download`), so a folder the size of a video directory
    still backs off with the session's delay instead of taking whatever the
    downlink accepts. It is streamed rather than handed to FileResponse,
    since the temporary does not outlive the handler, and the route is GET
    alone for the same reason: a HEAD, a Range, or a conditional request
    would want FileResponse to answer from a file that is gone, and each is
    refused before the folder is packed rather than after it.
    """
    if self._viewer_ceiling(request):
        return web.Response(status=403, text="View-only credentials cannot download files")
    if "download" not in self.settings.file_transfers:
        return web.Response(status=403, text="Forbidden: downloads disabled")
    if self._fetched_cross_site(request, request.app["settings"]):
        logger.warning("Refused a folder archive to another site's page (Origin %r)",
                       request.headers.get("Origin", ""))
        return web.Response(status=403, text="Forbidden origin")
    rel = str(request.query.get("path", ""))
    target = self._resolve_file_path(rel)
    if target is None or self._at_file_root(target):
        return web.json_response({"status": "error", "message": "not found"}, status=404)
    if any(name in request.headers for name in (
            "Range", "If-Modified-Since", "If-None-Match", "If-Range", "If-Unmodified-Since")):
        return web.Response(status=405, headers={"Allow": "GET"})
    staged: Optional[str] = None
    try:
        try:
            staged = await asyncio.get_running_loop().run_in_executor(
                ARCHIVE_PACK_POOL, self._pack_directory, self._file_parts(rel) or [])
        except ArchiveTooLarge:
            return web.json_response(
                {"status": "error",
                 "message": "the folder holds too much to pack as one archive"},
                status=413)
        except OSError:
            staged = None
        if staged is None:
            return web.json_response({"status": "error", "message": "not found"}, status=404)
        name = os.path.basename(os.path.normpath(rel))
        return await self._serve_paced_download(
            request, pathlib.Path(staged),
            audit_name=rel, download_name=f"{name}.zip",
            allow_fallback=False)
    finally:
        if staged is not None:
            await asyncio.to_thread(os.remove, staged)
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.StreamResponse
funcfancy_index_handler(request) -> web.StreamResponse

GET /api/files/...: serve the file-manager tree for download.

Files are served with an attachment disposition, since inline types (text, images, PDF) would otherwise render inside the dashboard's file-browser iframe instead of downloading; directories render a styled HTML listing, redirected to their slash-terminated URL so the relative links resolve one level down. The index exists solely to download files, so the listing itself is gated behind the same "download" transfer permission as the bytes. Path validation rejects any traversal before touching the filesystem and re-checks the symlink-resolved path against the root.

A download paces like an upload, mirrored: _serve_paced_download carries the mechanism and its conditions.

View-only credentials are refused the listing and the bytes alike: a viewer watches the desktop, it does not take the files on it.

Source Code
async def fancy_index_handler(self, request: web.Request) -> web.StreamResponse:
    """GET /api/files/...: serve the file-manager tree for download.

    Files are served with an attachment disposition, since inline types
    (text, images, PDF) would otherwise render inside the dashboard's
    file-browser iframe instead of downloading; directories render a
    styled HTML listing, redirected to their slash-terminated URL so the
    relative links resolve one level down. The index exists solely to
    download files, so the listing itself is gated behind the same
    "download" transfer permission as the bytes. Path validation rejects
    any traversal before touching the filesystem and re-checks the
    symlink-resolved path against the root.

    A download paces like an upload, mirrored: `_serve_paced_download`
    carries the mechanism and its conditions.

    View-only credentials are refused the listing and the bytes alike: a
    viewer watches the desktop, it does not take the files on it.
    """
    if self._viewer_ceiling(request):
        return web.Response(status=403, text="View-only credentials cannot list or download files")
    if "download" not in self.settings.file_transfers:
        return web.Response(status=403, text="Forbidden: downloads disabled")
    rel_path = request.match_info.get("path", "").lstrip("/")
    base = str(self.upload_dir)
    parts = self._file_parts(rel_path)
    if parts is None:
        return web.Response(
            status=403, text="Forbidden: Directory Traversal detected"
        )
    resolved = os.path.realpath(os.path.join(base, *parts))
    if resolved != base and not resolved.startswith(os.path.join(base, "")):
        return web.Response(
            status=403, text="Forbidden: Directory Traversal detected"
        )
    full_path = pathlib.Path(resolved)

    if not full_path.exists():
        return web.Response(status=404, text="Not Found")

    if full_path.is_file():
        return await self._serve_paced_download(
            request, full_path, audit_name="/".join(parts))

    if not request.path.endswith("/"):
        # Exactly one leading slash, so the location is never protocol-relative.
        location = "/" + request.path.lstrip("/") + "/"
        if request.query_string:
            location += "?" + request.query_string
        raise web.HTTPMovedPermanently(location)

    try:
        items = await asyncio.to_thread(
            _scan_directory, full_path, full_path != self.upload_dir, str(self.upload_dir))
    except PermissionError:
        return web.Response(status=403, text="Permission Denied")

    items.sort(key=lambda x: (not x["is_dir"], x["name"].lower()))

    rows = ""
    for item in items:
        # A name that is not UTF-8 comes back from scandir with surrogates,
        # which neither quote() nor the response's encoding takes.
        raw_name = item["name"].encode("utf-8", "surrogateescape")
        escaped_name = html.escape(raw_name.decode("utf-8", "replace"))
        escaped_mtime = html.escape(item["mtime"])
        escaped_size = html.escape(item["size"])
        rows += f"""
        <tr>
            <td><a href="{urllib.parse.quote(raw_name)}">{escaped_name}</a></td>
            <td>{escaped_mtime}</td>
            <td>{escaped_size}</td>
        </tr>"""

    # The header template leaves the H1 open; the current path closes it here.
    escaped_rel_path = html.escape(rel_path)
    current_display_path = f"/api/files/{escaped_rel_path}"

    # json.dumps yields a safely quoted/escaped JavaScript string literal.
    js_safe_upload_dir = json.dumps(str(self.upload_dir))
    path_injection = f"<script>window.__SELKIES_INJECTED_PATH_PREFIX__ = {js_safe_upload_dir};</script>"

    html_content = f"""
    {FILE_INDEX_HEADER}
    {current_display_path}</h1>
    {path_injection}
    <table id="list">
        <thead>
            <tr>
                <th>File Name</th>
                <th>Date</th>
                <th>Size</th>
            </tr>
        </thead>
        <tbody>
            {rows}
        </tbody>
    </table>
    {FILE_INDEX_FOOTER}
    """

    return web.Response(text=html_content, content_type="text/html")
paramrequestweb.Request

Returns

aiohttp.web.aiohttp.web.StreamResponse
funcinitialize_app() -> web.Application

Build the aiohttp application: auth middleware, API routes, service routes, and the static frontend.

Every control-plane endpoint lives under /api so a fronting proxy routes them, present and future, with one rule. The file-browser API serves the file-manager directory independently of the static content, so a deployment whose frontend is served elsewhere (nginx, web_root unset) still gets downloads instead of a 404; the apps catalog of a proot-apps local repository is served the same way; and the Prometheus registry is process-global, so one mode-agnostic endpoint serves both streaming modes.

Source Code
async def initialize_app(self) -> web.Application:
    """Build the aiohttp application: auth middleware, API routes, service
    routes, and the static frontend.

    Every control-plane endpoint lives under `/api` so a fronting proxy
    routes them, present and future, with one rule. The file-browser API
    serves the file-manager directory independently of the static content,
    so a deployment whose frontend is served elsewhere (nginx, `web_root`
    unset) still gets downloads instead of a 404; the apps catalog of a
    proot-apps local repository is served the same way; and the
    Prometheus registry is process-global, so one mode-agnostic endpoint
    serves both streaming modes.

    Returns:
        The configured application (also stored on ``self.app``).
    """
    self._require_configured_credentials()
    self._require_one_listen_setting()

    self.app = web.Application(middlewares=[self._auth_middleware])
    self.app["supervisor"] = self
    self.app["settings"] = self.settings
    frame_policy = _frame_ancestors_directive(self.settings.frame_ancestors)
    if frame_policy:
        async def _send_frame_policy(request: web.Request, response: web.StreamResponse) -> None:
            response.headers["Content-Security-Policy"] = frame_policy
        self.app.on_response_prepare.append(_send_frame_policy)

    api_prefix = self.settings.subfolder
    if api_prefix:
        logger.debug(f"Prepending api prefix: {api_prefix!r} to router handlers")

    routes = [
        web.get(f"{api_prefix}/api/status", self.handle_status),
        web.get(f"{api_prefix}/api/health", self.handle_health),
        web.post(f"{api_prefix}/api/switch", self.handle_switch),
        web.post(f"{api_prefix}/api/upload", self.handle_upload),
        # The file manager's JSON API, apart from /api/files/ so that none
        # of its names shadows a file or folder of the same name there.
        web.get(f"{api_prefix}/api/file-manager/browse", self.handle_files_browse),
        web.post(f"{api_prefix}/api/file-manager/mkdir", self.handle_files_mkdir),
        web.get(f"{api_prefix}/api/file-manager/zip", self.handle_files_zip,
                allow_head=False),
        web.get(f"{api_prefix}/api/files/{{path:.*}}", self.fancy_index_handler),
        web.get(f"{api_prefix}/api/print/{{name}}", self.handle_print_document),
        web.get(f"{api_prefix}/api/apps/metadata.yml", self.handle_apps_catalog),
        web.get(f"{api_prefix}/api/apps/img/{{name}}", self.handle_apps_icon),
        web.get(f"{api_prefix}/api/sessions", self.handle_sessions),
        web.delete(f"{api_prefix}/api/sessions/{{id}}", self.handle_session_delete),
        web.get(f"{api_prefix}/api/recording", self.handle_recording),
        web.post(f"{api_prefix}/api/recording", self.handle_recording),
        web.delete(f"{api_prefix}/api/recording", self.handle_recording),
        web.get(f"{api_prefix}/api/screenshot", self.handle_screenshot),
    ]
    if self.settings.enable_metrics_http[0]:
        routes.append(web.get(f"{api_prefix}/api/metrics", self.handle_metrics))
    self.app.add_routes(routes)

    for service in self.services.values():
        service.register_routes(api_prefix, self.app.router)

    self.static_fs_path = await self._get_static_content_path()
    if self.static_fs_path:
        async def index_handler(request: web.Request) -> web.FileResponse:
            moved = _ipv6_loopback_redirect(request, f"{api_prefix}/")
            if moved:
                raise web.HTTPFound(moved)
            return web.FileResponse(os.path.join(self.static_fs_path, "index.html"))

        index = self.app.router.add_get(f"{api_prefix}/", index_handler)
        static = self.app.router.add_static(
            f"{api_prefix}/", self.static_fs_path, name="static"
        )

        async def _revalidate_fixed_names(request: web.Request, response: web.StreamResponse) -> None:
            """Have a browser revalidate, on every load, the entry page and
            every other file of the web client's code whose name does not
            change with its content.

            With validators alone a browser may reuse such a file by
            heuristic freshness, so after an upgrade or a change of web
            root it loads the old page, or the old core under a new page;
            the validators keep each revalidation a 304. What the build
            names by content hash, under `assets/`, is left to cache, and
            so are icons and the manifest, which no page's behavior hangs
            on and which Firefox would revalidate on every load.
            """
            resource = request.match_info.route.resource
            name = request.match_info.get("filename", "")
            if resource is index.resource or (
                    resource is static and not name.startswith("assets/") and name.endswith(WEB_CODE)):
                response.headers.setdefault("Cache-Control", "no-cache")
        self.app.on_response_prepare.append(_revalidate_fixed_names)
    else:
        logger.warning("Unable to find web content, skipping web routers handlers")
    return self.app

Returns

aiohttp.web.Application

The configured application (also stored on self.app).

func_unix_socket_path() -> str
Source Code
def _unix_socket_path(self) -> str:
    return str(getattr(self.settings, "unix_socket", "") or "").strip()

Returns

str
func_clear_stale_unix_socket(sock_path) -> None

Remove a leftover socket file so the bind cannot fail with EADDRINUSE.

Only a socket inode that nothing accepts on is removed. Unlinking a live one would leave the instance that owns it serving an inode no client can reach, so a path still in use — or occupied by anything that is not a socket — aborts the start instead.

Source Code
def _clear_stale_unix_socket(self, sock_path: str) -> None:
    """Remove a leftover socket file so the bind cannot fail with EADDRINUSE.

    Only a socket inode that nothing accepts on is removed. Unlinking a live
    one would leave the instance that owns it serving an inode no client can
    reach, so a path still in use — or occupied by anything that is not a
    socket — aborts the start instead."""
    try:
        mode = os.stat(sock_path).st_mode
    except FileNotFoundError:
        return
    except OSError as exc:
        raise RuntimeError(
            f"Cannot inspect unix socket path '{sock_path}': {exc}"
        ) from exc
    if not stat.S_ISSOCK(mode):
        raise RuntimeError(
            f"Unix socket path '{sock_path}' exists and is not a socket; "
            "refusing to remove it."
        )
    if _unix_socket_is_live(sock_path):
        raise RuntimeError(
            f"Another server is already listening on '{sock_path}'."
        )
    try:
        os.unlink(sock_path)
    except FileNotFoundError:
        pass
    except OSError as exc:
        raise RuntimeError(
            f"Cannot remove stale unix socket '{sock_path}': {exc}"
        ) from exc
paramsock_pathstr

Returns

None
func_remove_own_unix_socket() -> None

Drop this listener's socket file on shutdown so nothing is left behind for the next start to clear; the runtime does not unlink it on every supported Python version. A path something is accepting on again belongs to another instance and is left alone.

Source Code
def _remove_own_unix_socket(self) -> None:
    """Drop this listener's socket file on shutdown so nothing is left behind
    for the next start to clear; the runtime does not unlink it on every
    supported Python version. A path something is accepting on again belongs
    to another instance and is left alone."""
    sock_path = self._unix_socket_path()
    if not sock_path:
        return
    try:
        if not stat.S_ISSOCK(os.stat(sock_path).st_mode):
            return
        if _unix_socket_is_live(sock_path):
            return
        os.unlink(sock_path)
    except OSError:
        pass

Returns

None
func_build_sites(ssl_context=None) -> List[web.BaseSite]

The aiohttp sites of the configured listener: one on a Unix domain socket when unix_socket is set, otherwise one per address addr binds on port, bound here so a listen address the host lacks can be skipped rather than failing the start.

Source Code
async def _build_sites(self, ssl_context: Optional[ssl.SSLContext] = None) -> List[web.BaseSite]:
    """The aiohttp sites of the configured listener: one on a Unix domain
    socket when ``unix_socket`` is set, otherwise one per address ``addr``
    binds on ``port``, bound here so a listen address the host lacks can
    be skipped rather than failing the start."""
    sock_path = self._unix_socket_path()
    if sock_path:
        parent = os.path.dirname(sock_path)
        if parent:
            os.makedirs(parent, exist_ok=True)
        self._clear_stale_unix_socket(sock_path)
        return [web.UnixSite(self.runner, path=sock_path, ssl_context=ssl_context)]
    socks = await _bind_listen_sockets(self.settings.addr, self.settings.port)
    return [web.SockSite(self.runner, sock, ssl_context=ssl_context) for sock in socks]
paramssl_contextOptional[ssl.SSLContext]
= None

Returns

typing.List[aiohttp.web.aiohttp.web.BaseSite]
func_start_sites(ssl_context=None) -> List[web.BaseSite]

Bind and start the configured listener, publishing it as self.sites.

A start that fails part-way stops what came up so nothing is left listening under a site the server does not track.

Source Code
async def _start_sites(self, ssl_context: Optional[ssl.SSLContext] = None) -> List[web.BaseSite]:
    """Bind and start the configured listener, publishing it as ``self.sites``.

    A start that fails part-way stops what came up so nothing is left
    listening under a site the server does not track.

    Raises:
        OSError: When the listener cannot be bound.
    """
    sites = await self._build_sites(ssl_context)
    started: List[web.BaseSite] = []
    try:
        for site in sites:
            await site.start()
            started.append(site)
    except BaseException:
        for site in started:
            await site.stop()
        for site in sites[len(started):]:
            sock = getattr(site, "_sock", None)
            if sock is not None:
                sock.close()
        raise
    self.sites = sites
    return sites
paramssl_contextOptional[ssl.SSLContext]
= None

Returns

typing.List[aiohttp.web.aiohttp.web.BaseSite]
func_site_kind() -> str
Source Code
def _site_kind(self) -> str:
    return "Unix socket listener" if self._unix_socket_path() else "TCP listener"

Returns

str
func_site_endpoint() -> str

Where the server is reached: every bound address once it listens, the configured one before that.

Source Code
def _site_endpoint(self) -> str:
    """Where the server is reached: every bound address once it listens,
    the configured one before that."""
    sock_path = self._unix_socket_path()
    if sock_path:
        return f"unix://{sock_path}"
    if self.sites:
        return ", ".join(site.name for site in self.sites)
    return f"{self.settings.addr} port {self.settings.port}"

Returns

str
funcstart_server() -> None

Start the HTTP/HTTPS server, the tokens-file watcher when one is set, and, under HTTPS, the cert-reload watcher.

Source Code
async def start_server(self) -> None:
    """Start the HTTP/HTTPS server, the tokens-file watcher when one is set,
    and, under HTTPS, the cert-reload watcher."""
    if not self.app:
        await self.initialize_app()

    https = getattr(self.settings, "enable_https", (False,))[0]
    if https:
        try:
            self.ssl_context = self._create_ssl_context()
        except Exception as exc:
            logger.error("Failed to create SSL context at startup: %s", exc)
            raise

    tokens = self.services.get("websockets")
    if getattr(self.settings, "tokens_file", "") and tokens is not None:
        self.tokens_watcher = asyncio.create_task(tokens.watch_tokens_file())

    self.runner = web.AppRunner(self.app, access_log_class=PathOnlyAccessLogger,
                                keepalive_timeout=IDLE_CONNECTION_TIMEOUT_SECONDS)
    await self.runner.setup()

    try:
        await self._start_sites(self.ssl_context)
    except Exception as exc:
        logger.error("Cannot bind %s: %s", self._site_endpoint(), exc)
        raise
    logger.info("Selkies server running on %s", self._site_endpoint())

    if https:
        self.cert_watcher = asyncio.create_task(self._watch_and_reload_certs())
    if self.settings.printing_enabled[0]:
        self.print_watcher = printing.SpoolWatcher(
            str(self.print_spool), asyncio.get_running_loop(), self._announce_print_document)
        try:
            self.print_watcher.start()
        except OSError as exc:
            # Watching the spool is one inotify instance, which a host can run out of.
            logger.warning("Printing is off: the spool cannot be watched (%s).", exc)
            self.print_watcher = None
        else:
            self.print_queue = printing.PrintQueue(str(self.print_spool))
            await self.print_queue.start()

Returns

None
funcstop_server() -> None

Stop the server gracefully: the cert and tokens-file watchers, active service, listener, extracted web files, and the runner, in that order.

Source Code
async def stop_server(self) -> None:
    """Stop the server gracefully: the cert and tokens-file watchers, active
    service, listener, extracted web files, and the runner, in that order."""
    for watcher in (self.cert_watcher, self.tokens_watcher):
        if watcher and not watcher.done():
            watcher.cancel()
            try:
                await watcher
            except asyncio.CancelledError:
                pass
    if self.print_queue:
        await self.print_queue.stop()
        self.print_queue = None
    if self.print_watcher:
        self.print_watcher.stop()
        self.print_watcher = None

    await self._stop_service()

    if self.web_files_ctx:
            self.web_files_ctx.cleanup()
    for site in self.sites:
        await site.stop()
    if self.sites:
        self._remove_own_unix_socket()
    self.sites = []
    if self.runner:
        await self.runner.cleanup()
        logger.debug("Server cleanup complete.")

Returns

None
funcrun() -> None

Start the server and serve until canceled, then clean up.

Source Code
async def run(self) -> None:
    """Start the server and serve until canceled, then clean up."""
    try:
        await self.start_server()
        await asyncio.Future()
    except asyncio.CancelledError:
        logger.info("Shutdown signal received...")
    finally:
        await self.stop_server()

Returns

None
funcregister_service(name, service) -> None

Register a streaming service under name for later activation.

Source Code
def register_service(self, name: str, service: BaseStreamingService) -> None:
    """Register a streaming service under ``name`` for later activation."""
    self.services[name] = service
paramnamestr
paramserviceBaseStreamingService

Returns

None