Skip to content

lablink_allocator_service.routes.desktop

GET /desktop — cookie-gated noVNC viewer page.

Reads the signed lablink_session cookie minted by /api/request_vm, looks up the assigned VM by session_id, and renders the noVNC viewer configured from the persisted browser_ws_url and browser_credential.

If the cookie is missing, invalid, or the bound VM is no longer running, redirect to / so the student can submit their email again.

The participant desktop.

Auth: The signed lablink_session cookie.

Renders the noVNC viewer page. Reads the cookie minted by POST /api/request_vm, looks up the assigned VM by session_id, and configures the viewer from the persisted browser_ws_url and browser_credential. If the cookie is missing, invalid, or the bound VM is no longer running, it redirects to / so the participant can submit their email again.

Source code in packages/allocator/src/lablink_allocator_service/routes/desktop.py
@bp.route("/desktop")
def desktop():
    """The participant desktop.

    Auth: The signed `lablink_session` cookie.

    Renders the noVNC viewer page. Reads the cookie minted by
    `POST /api/request_vm`, looks up the assigned VM by `session_id`, and
    configures the viewer from the persisted `browser_ws_url` and
    `browser_credential`. If the cookie is missing, invalid, or the bound VM
    is no longer running, it redirects to `/` so the participant can submit
    their email again.
    """
    raw_cookie = request.cookies.get("lablink_session")
    if not raw_cookie:
        return redirect("/", code=302)

    pool = current_app.config["DB_POOL"]
    conn = pool.getconn()
    try:
        secret = get_or_create_cookie_secret(conn)
        try:
            payload = verify(raw_cookie, secret=secret)
        except InvalidSignature:
            return redirect("/", code=302)
        session_id, _, suffix = payload.partition(":")

        table = sql.Identifier(current_app.config["VM_TABLE_NAME"])
        with conn.cursor() as cur:
            cur.execute(
                sql.SQL(
                    "SELECT browser_ws_url, browser_credential, hostname "
                    "FROM {table} "
                    "WHERE sessionid = %s AND status = 'running'"
                ).format(table=table),
                (session_id,),
            )
            row = cur.fetchone()
    finally:
        pool.putconn(conn)

    if row is None or row[0] is None:
        return redirect("/", code=302)

    ws_url, credential, hostname = row
    view_only = suffix == "view_only"

    if suffix == "admin_session":
        return _render_admin_session_page(ws_url, hostname)

    # Proxied path (relative, server-side credential). reconnect=1 makes the
    # Kasm noVNC viewer retry after an unexpected drop (reconnect_delay ms
    # between attempts) instead of stranding the participant on a dead
    # "Disconnected" screen — transient WS kills are a fact of life on every
    # connectivity path (Cloudflare 524 churn, campus middleboxes, laptop
    # sleep). Deliberate disconnects don't loop: the viewer arms reconnect
    # only after a successful connect and disarms it on user Disconnect.
    if ws_url.startswith("proxy/"):
        vo_qs = "&view_only=1" if view_only else ""
        return redirect(
            f"/static/novnc/vnc.html?path={ws_url}"
            f"&autoconnect=1&resize=remote"
            f"&reconnect=1&reconnect_delay=2000{vo_qs}{VIEWER_TUNING_QS}",
            code=302,
        )

    # Direct ws(s):// target (manual / lan_direct connectivity):
    # the browser opens the WS straight at the client KasmVNC, with no
    # allocator proxy in the byte path. Modern Chromium/Firefox refuse
    # to attach an `Authorization: Basic` header to a WebSocket upgrade
    # (URL userinfo is stripped at the URL-parser level), so we drive
    # KasmVNC in RFB `VncAuth` mode on this connectivity and pass the
    # per-session 8-char credential through the bundled noVNC's
    # `?password=` URL param — it consumes it in-band during the VNC
    # auth handshake. The credential sits in the WS-viewer URL, but
    # `/static/novnc/*` has `access_log off` in lablink-nginx.conf and
    # we use `location.replace` to keep the URL out of session history.
    # DevTools visibility is unavoidable for any browser-direct auth
    # scheme; per-session rotation bounds the exposure window.
    parts = urlsplit(ws_url)
    host = parts.hostname or ""
    port = parts.port or 6080
    encrypt = "1" if parts.scheme == "wss" else "0"
    pw_qs = "" if credential is None else f"&password={quote(credential, safe='')}"
    vo_qs = "&view_only=1" if view_only else ""
    target = (
        f"/static/novnc/vnc.html?host={host}&port={port}&encrypt={encrypt}"
        f"&autoconnect=1&resize=remote"
        f"&reconnect=1&reconnect_delay=2000{pw_qs}{vo_qs}{VIEWER_TUNING_QS}"
    )
    return (
        "<!doctype html><meta charset=utf-8>"
        '<body style="margin:0">'
        f"<script>location.replace({json.dumps(target)});</script>",
        200,
    )