ducoterra 7988da2b93 feat: decide who may connect at all, by source address
mirror has no accounts, so on a real network the address a connection arrives
from is not a nicety — it is the entire access control. Before this change the
answer was "everyone who can open a TCP connection", which is fine for a laptop
demo and alarming for a box someone leaves running.

`mirror serve --allow-cidr 10.2.0.0/22` (or MIRROR_ALLOW_CIDRS) now restricts
pages, the JSON API and the signaling socket to the networks named, and it is
strict where it needs to be: a malformed CIDR exits 2 with one line instead of
starting a server whose list silently failed to parse, because the two
outcomes of that typo are "nobody can connect" and "everybody can connect".
Empty remains the only permissive state, on purpose.

The rules that matter live in src/mirror/access.py and are the ones that get
either outcome:

  * X-Forwarded-For is believed only from MIRROR_TRUSTED_PROXIES (loopback by
    default), and only its left-most value. An untrusted peer that sends the
    header gets judged on its own address — there is a test for exactly that,
    because it is the shape of a bypass.
  * The socket is closed before it accepts, so a refused address never creates
    a roster entry to clean up behind it.
  * An address that cannot be parsed fails closed, and a v6 address is never
    "inside" a v4 network (comparing the two raises TypeError, which would be
    a 500 on a dual-stack host).
  * /healthz is exempt: the container health probe comes from loopback, which
    a production list otherwise excludes.

The refused client is told why, and which address it was judged as — "403" in
front of a proxy is undiagnosable otherwise. Browsers get a page, fetches get
JSON, and the page is self-contained because a refused address cannot load
/static/app.css any more than it can load a page. net.js keys its own notice on
the /api/config 403 and stops: without that the pill reads "offline,
reconnecting in 32s" forever for a decision that will never change.

The static files drop their only inline script and style attribute (the landing
page's counter is /static/home.js, its fine print is .fineprint) so the
deployment can send `script-src 'self'; style-src 'self'` with no
'unsafe-inline'. That policy was verified through a real nginx edge carrying the
exact header set — capture through SRTP to decoded, painted frames at
1280x720, eight distinct, zero securitypolicyviolation events — which also
caught the edge sending `Cache-Control: no-store, no-store` and needed
proxy_hide_header, since nginx appends rather than replaces.

24 tests added; 176 pass, 2 skipped, pyright and ruff clean.

LLM-Generated: true
2026-11-01 17:47:31 -05:00

mirror

Device-agnostic screen mirroring that lives entirely in the browser.

mirror runs a small HTTP server on your machine. Open it from any device on your network:

Page URL What it does
Share /mirror Picks a window, screen, or tab via the OS-native picker and mirrors it.
Watch /watch Shows every screen currently being mirrored; click a tile to take the stage.
Index / Signpost + live count of mirrors.

Any number of people can mirror at the same time and any number of people can watch. There is no authentication — this is built for a trusted local network, and that assumption is deliberate and documented in SECURITY.md.

Quickstart

uv sync --extra dev
uv run mirror serve            # http://0.0.0.0:8055

Then, from another machine on the LAN:

http://<this-machine>:8055/mirror    # to share
http://<this-machine>:8055/watch     # to watch

Linux is the primary target. On Linux the native picker is X11/Wayland's own (Chromium uses xdg-desktop-portal on Wayland/GNOME/KDE, and its built-in X11 picker on Xorg), so nothing has to be installed on the client side.

How it works

Media never touches the server. Each mirror is a WebRTC peer connection established directly between the sharer's browser and each watcher's browser (a "mesh" fan-out). The server is a signaling relay and roster only: it tracks who is publishing what, forwards SDP/ICE between peers, and pushes roster deltas to viewers.

 sharer browser ──ws──► ┌──────────────────────┐ ◄──ws── watcher browser
      │                 │  FastAPI             │                 ▲
      │   offer/ICE ───►│  /ws  (signalling)   │◄─── offer/ICE   │
      │                 │  registry (in-mem)   │                 │
      └────── WebRTC (SRTP, LAN, p2p) ─────────┴─────────────────┘

Configuration

Environment variables (all optional; a .env file is loaded if present):

Variable Default Meaning
MIRROR_HOST 0.0.0.0 Bind address for mirror serve.
MIRROR_PORT 8055 Bind port.
MIRROR_ICE_SERVERS [] JSON array of RTCIceServer objects, if peers cannot reach each other directly.
MIRROR_MAX_MIRRORS 0 Cap on concurrent mirrors (0 = unlimited).
DEBUGPY 0 1 opens a debugpy listener on DEBUGPY_PORT.
DEBUGPY_PORT 5678 debugpy port.

Platform notes

Screen capture needs a secure context. http://localhost is one; a plain http://192.168.x.x opened from another device is not, and the browser hides getDisplayMedia() completely. The /mirror page detects this and prints the three ways forward (share from localhost, serve HTTPS, or mark that one origin as secure on the sharing device). Watching never needs permission.

Linux. On X11/Xorg Chromium uses its native capturer — the picker appears and that is the end of it. On Wayland (GNOME/KDE) Chromium goes through xdg-desktop-portal, so the compositor shows its own permission dialog for each share, which mirror neither controls nor tries to bypass. Chrome's picker offers whole screens and individual windows; on Wayland what the portal will agree to depends on the compositor.

Development

uv run pytest                       # unit + API + browser E2E (~20s)
uv run pytest -m "not e2e"          # everything except the browser (~0.5s)
uv run ruff check . && uv run ruff format --check . && uv run pyright

The browser suite drives real capture: Chromium's fake media device yields a moving 1280×720 monitor stream, so the assertions are on decoded, changing pixels at the watcher — not on log lines.

There is a second, opt-in rung that exercises Chromium's real X11 capture backend on a private Xvfb display, isolated from your own session so it can never prompt you or mirror your desktop:

MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay

See AGENTS.md for the operating rules, .agents/PLAN.md for the locked architectural decisions, and SECURITY.md for why there is no login.

S
Description
No description provided
Readme MIT
707 KiB
Languages
Python 70.8%
JavaScript 20.8%
CSS 4.5%
HTML 3%
Shell 0.6%
Other 0.3%