Metadata-Version: 2.5
Name: guapy
Version: 2.0.0
Summary: Framework-free asynchronous Guacamole protocol and guacd library
Project-URL: Homepage, https://github.com/Adithya1331/guapy
Project-URL: Documentation, https://guapy.readthedocs.io
Project-URL: Repository, https://github.com/Adithya1331/guapy.git
Project-URL: Issues, https://github.com/Adithya1331/guapy/issues
Project-URL: Changelog, https://github.com/Adithya1331/guapy/blob/main/CHANGELOG.md
Author-email: Adithya <adithyakokkirala@gmail.com>
Maintainer-email: Adithya <adithyakokkirala@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: asyncio,guacamole,rdp,remote-desktop,ssh,vnc
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications
Classifier: Topic :: Internet
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# Guapy

[![CI](https://github.com/Adithya1331/guapy/actions/workflows/ci.yml/badge.svg)](https://github.com/Adithya1331/guapy/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/guapy.svg)](https://pypi.org/project/guapy/)
[![Python](https://img.shields.io/pypi/pyversions/guapy.svg)](https://pypi.org/project/guapy/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Code style: ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/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:

```python
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](https://guacamole.apache.org/doc/gug/guacamole-common-js.html)
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…):

```python
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:

```python
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:

```bash
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:

```bash
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

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

See [Architecture.md](docs/architecture.md) for the design, layering, and the
boundaries Guapy deliberately does not cross. Contributions welcome — start
with [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT. See [LICENSE](LICENSE).
