Skip to content

Repository files navigation

Guapy

CI PyPI Python License: MIT Code style: ruff

Embed browser-based remote desktops (RDP, VNC, SSH, Telnet) into your Python web application. Guapy is a framework-free asyncio library for the Guacamole protocol and its guacd daemon — no Java webapp, no Node.js sidecar.

It speaks the Guacamole wire protocol end to end: immutable instructions, an incremental bounded parser, version negotiation, the full guacd handshake, an asyncio TCP transport, and a mountable ASGI WebSocket tunnel for FastAPI, Starlette, and Django Channels.

Why Guapy?

Apache Guacamole renders remote desktops in the browser, driven by a small C daemon called guacd. To use it from a Python application you traditionally either run Guacamole's full Java webapp (its own auth model, its own database) or hand-write the protocol glue. Guapy is the missing Python layer:

Guapy guacamole-lite (Node) Apache Guacamole (Java)
Language Python Node.js Java
Async IO asyncio event loop servlet threads
Runs inside your app ✅ ASGI mount ✅ ❌ separate webapp
Per-connection authorization ✅ resolver you control encrypted token its own DB
Runtime dependencies zero ws, deep-extend servlet container
Typed exceptions with HTTP/WS mappings ✅ ❌ ✅

Guapy deliberately ships no authentication, token crypto, session registry, or database. Your app authorizes each tunnel through a one-method resolver; Guapy handles everything below that.

Quick start

Mount the tunnel in FastAPI and decide per request which remote host a user gets — the resolver sees the query string, headers (cookies!), and client address:

from fastapi import FastAPI
from guapy import (
    GuacamoleConfiguration,
    GuacamoleUnauthorizedError,
    GuacdEndpoint,
    QueryWhitelistResolver,
    ResolvedConnection,
    TunnelRequest,
)
from guapy.server import GuacamoleASGIApp

app = FastAPI()


class DashboardResolver:
    """Authorize tunnels with your own session machinery."""

    async def resolve(self, request: TunnelRequest) -> ResolvedConnection:
        session_id = request.headers.get("cookie", "")
        if not is_logged_in(session_id):  # your application logic
            raise GuacamoleUnauthorizedError("sign in first")
        vm = lookup_vm_for_user(session_id)  # your application logic
        return ResolvedConnection(
            GuacamoleConfiguration(
                protocol="rdp",
                parameters={
                    "hostname": vm.host,
                    "port": "3389",
                    "username": vm.username,
                    "password": vm.password,
                },
            )
        )


tunnel = GuacamoleASGIApp(
    endpoint=GuacdEndpoint(host="guacd.internal", port=4822),
    resolver=DashboardResolver(),
)
app.mount("/guacamole", tunnel)

Point guacamole-common-js in the browser at /guacamole/webSocket and the remote desktop renders.

Prefer the safe default? QueryWhitelistResolver wraps a fixed connection and lets clients override only what you whitelist (display size, color scheme…):

from guapy import QueryWhitelistResolver

resolver = QueryWhitelistResolver(
    GuacamoleConfiguration(
        protocol="ssh", parameters={"hostname": "bastion.internal", "port": "22"}
    ),
    allowed=("width", "height", "dpi", "color-scheme"),
)

Using the client directly

Guapy also works as a plain guacd client, no web layer involved:

import asyncio

from guapy import GuacamoleClient, GuacamoleConfiguration, GuacdEndpoint, Instruction


async def main() -> None:
    configuration = GuacamoleConfiguration(
        protocol="ssh",
        parameters={
            "hostname": "ssh.example.internal",
            "port": "22",
            "username": "alice",
            "password": "provided-by-your-application",
        },
    )

    client = GuacamoleClient()
    async with await client.connect(GuacdEndpoint(), configuration) as session:
        await session.send(Instruction.create("sync", "0"))
        instruction = await session.receive()
        if instruction is not None:
            print(instruction)


asyncio.run(main())

Applications remain responsible for which configuration is authorized. Never send connection credentials to an untrusted client or log them.

Features

  • Immutable Guacamole instructions and a bounded incremental parser that survives arbitrary TCP fragmentation and split UTF-8 code points.
  • Complete guacd handshake with protocol version negotiation, client capabilities (screen, audio/video/image formats, timezone, name), and support for joining existing sessions by connection ID.
  • Pluggable transports — asyncio TCP adapter included; TLS to guacd is a constructor flag. Bring your own connector for proxies or recording.
  • Ordered instruction filter pipelines for inspecting, rewriting, or dropping instructions on read and write paths.
  • Typed status model — every error carries its Guacamole status code and the mapped HTTP status and WebSocket close code, so tunnels close exactly the way the official Java server closes them.
  • Production-shaped ASGI tunnel: per-connection resolvers, connection limits, inactivity timeouts, open/close hooks, GET /health, ASGI lifespan support, and graceful drain on shutdown.

Guapy requires Python 3.10+ and installs with zero dependencies.

Try it in Docker

The e2e fixture is a complete Guacamole deployment in one command — Guapy, official guacd, a containerized SSH server (demo/demo), and a browser client:

docker compose -f e2e/docker-compose.yml up --build

Open http://localhost:9090, select New Connection → SSH → Connect. The same stack backs the integration tests:

GUAPY_TEST_GUACD_HOST=127.0.0.1 GUAPY_TEST_SSH_HOST=sshd uv run pytest tests/integration

CI runs exactly these tests against real guacd and sshd containers on every push — the handshake is verified against the actual daemon, not mocks.

Logging

Guapy uses the standard-library logger named guapy and never configures handlers or levels. It logs lifecycle events (tunnel open/close, protocol disconnects, guacd EOF) and never logs instructions or credentials.

Development

uv sync --dev
uv run pytest                 # unit tests (integration tests skip without guacd)
uv run ruff check .
uv run mypy src

See Architecture.md for the design, layering, and the boundaries Guapy deliberately does not cross. Contributions welcome — start with CONTRIBUTING.md.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages