# pyfconfig A clone of [ifconfig.me](https://ifconfig.me/) built on the [kaya](https://gitea.woggioni.net/woggioni/kaya) framework (`kaya-core` + `kaya-rsgi`), served by [Granian](https://github.com/emmett-framework/granian) over the RSGI protocol. ## Stack - **kaya-core** — routing and HTTP request/response handling - **kaya-rsgi** — Granian (RSGI) adapter - **kaya-forwarded** — trusted-proxy handling of `Forwarded` / `X-Forwarded-*` headers - **granian** — application server - **rloop** — Rust event loop used by Granian instead of the stdlib asyncio loop - **httpx + pwo** — test client over kaya's ASGI transport No database, sessions, or authentication — the app is stateless. ## Endpoints | Method | Path | Response | |---|---|---| | GET | `/` | HTML page for browsers (`Accept: text/html`), plain-text IP otherwise | | GET | `/ip` | Client IP address | | GET | `/ua` | `User-Agent` header | | GET | `/lang` | `Accept-Language` header | | GET | `/encoding` | `Accept-Encoding` header | | GET | `/mime` | `Accept` header | | GET | `/charset` | `Accept-Charset` header | | GET | `/forwarded` | `X-Forwarded-For` header | | GET | `/all` | All connection fields as `key: value` lines (empty fields included; `remote_host` shown as `unavailable`) | | GET | `/all.json` | Same fields as a JSON object, empty fields omitted | | GET | `/api/health` | Liveness probe (`{"status":"ok"}`) | The `/all` field order mirrors the reference site: `ip_addr`, `remote_host`, `user_agent`, `port`, `language`, `referer`, `connection`, `keep_alive`, `method`, `encoding`, `mime`, `charset`, `via`, `forwarded`. The reported client IP/port honor the `Forwarded`, `X-Forwarded-For`, `X-Forwarded-Host` and `X-Forwarded-Port` proxy headers **only when the direct peer belongs to one of the `TRUSTED_PROXY_CIDRS`** (see [Configuration](#configuration)); the header chain is walked right-to-left skipping trusted proxies, so spoofed entries prepended by the client are never selected. Without trusted proxies configured, or when the peer is untrusted, the socket peer address is used. Example: ```console $ curl http://localhost:8080/ 203.0.113.7 $ curl http://localhost:8080/all.json {"ip_addr":"203.0.113.7","user_agent":"curl/8.21.0","port":"51342","method":"GET","mime":"*/*"} ``` ## Running ### With Docker Compose (recommended) ```bash docker compose up -d --build # App published at http://localhost:8080 ``` ### On the host ```bash cp .env.example .env # optional; all settings have defaults python -m venv .venv && . .venv/bin/activate pip install --index-url https://gitea.woggioni.net/api/packages/woggioni/pypi/simple \ --extra-index-url https://pypi.org/simple \ -e . granian --interface rsgi --loop rloop --host 0.0.0.0 --port 8000 pyfconfig.app:app ``` The `--loop rloop` flag (or `GRANIAN_LOOP=rloop`, already set in the Docker image) makes Granian run the app on the [rloop](https://github.com/gi0baro/rloop) Rust event loop instead of the stdlib asyncio loop. ### Configuration Environment variables (see `.env.example`): | Variable | Default | Description | |---|---|---| | `SITE_NAME` | `pyfconfig` | Public name used in the HTML page title and the curl examples (set to your domain, e.g. `ifconfig.example.com`) | | `TRUSTED_PROXY_CIDRS` | *(empty)* | Comma-separated CIDRs/IPs of trusted reverse proxies (e.g. `127.0.0.1,10.0.0.0/8`). Forwarded headers are honored only from these peers; empty means no proxy is trusted | The bind address is configured through Granian itself (`GRANIAN_HOST` / `GRANIAN_PORT` env vars or `--host` / `--port` CLI flags). ## Tests ```bash . .venv/bin/activate python -m unittest discover -s tests -t . ``` The tests drive the app in-process through kaya's ASGI interface using `httpx.ASGITransport`; no external services are needed. ## Project layout ``` src/pyfconfig/ ├── app.py # KayaApp assembly; imports route modules ├── config.py # env-driven Settings dataclass ├── page.py # HTML landing page builder (kaya has no templating) └── routes/ ├── health.py # GET /api/health ├── root.py # GET / with content negotiation └── connection.py # /ip /ua /lang /encoding /mime /charset /forwarded /all /all.json tests/ └── test_routes.py # unittest + httpx.ASGITransport ``` Route modules import the module-level `app` from `pyfconfig.app` and register handlers with `@app.GET(...)` decorators at import time — `pyfconfig.app` imports them last to complete the wiring. ## Deployment The `Dockerfile` is a multi-stage alpine build producing a minimal, non-root runtime image; pushing a `release/*` tag triggers the Gitea Actions workflow in `.gitea/workflows/build.yaml`, which builds and publishes the image to `gitea.woggioni.net/woggioni-opencode-agent/pyfconfig`.