Serving It Safely
ToolGUI has no login, no tokens and no permission model. Whoever reaches the app runs it, with everything the tool itself can reach: the files it opens, the databases it queries, the commands it shells out to. Treat "can open the page" as "can do anything the tool does", and decide who that should be.
Bind to loopback
StartService(":3000") listens on every interface, so the tool is on the
network the moment the machine is. The examples in these docs bind to
127.0.0.1 instead, which keeps it on the machine it runs on:
e := tgexec.NewWebExecutor(app)
e.StartService("127.0.0.1:3000")
That is the right default for a tool you run for yourself. It is also the
only thing standing between the tool and the rest of the network, so widen it
on purpose, not by copying a :3000 out of a README.
Origin check
The update websocket carries every event and every render of the app, and the
same-origin policy doesn't cover websockets: without a check, any page open in
the same browser could connect to ws://localhost:3000/api/update/..., press
the app's buttons and read back what they render.
So the handshake takes only an Origin whose host matches the one the request
asked for, and answers anything else with 403. Nothing has to be configured for
this; it is how the socket behaves.
Limits
Nothing authenticates a connection, so what one can ask for is capped. The
service holds 1024 states at most, one per open page, and a connection that
finds no room is refused rather than handed one anyway. One upload is 1 GiB at
most, and it is stored under a component the page actually drew, so a caller
cannot keep a file per name it invents. An event is held to the same rule: it
writes the state under an id the last finished run drew, so a client cannot
fill a session with keys no component owns and no run would ever read or
release. One message on the update socket is 1 MiB at most, and a form event
nests 32 levels at most: every level of a form has its subtree read again, so
the two together are what stop one message from buying far more work than it
took to send. StartService also puts deadlines on sending a request's
headers, on sitting idle between requests, and on naming a state once the
websocket handshake is done.
A download goes the other way and is capped by nothing, because there is
nothing to cap: DownloadFile serves a file one of the page's own runs offered,
named by a token that is unguessable, that is looked up in the state which
offered it and nowhere else, and that is only accepted alongside the state id of
the connection asking. A token is therefore a bearer of nothing on its own, and
neither it nor the state id is ever put in a URL, where a link, a log line or a
Referer would carry it further than the fetch that needs it.
The caps are worth lowering on anything reachable by more than the person running it:
e.SetMaxStateCount(64)
e.SetMaxUploadSize(16 * 1024 * 1024)
e.SetMaxMessageSize(64 * 1024)
Raise the message cap instead for an app whose iframe or plugin components send values of their own that are larger than 1 MiB. A message over the cap is refused by its header, without being read into memory, and the page keeps its connection.
These bound what one visitor costs. They are not a substitute for deciding who reaches the page.
Behind a reverse proxy
To serve the tool to more than the local machine, put it behind a proxy that authenticates — an identity-aware proxy, an SSO gateway, whatever the team already runs — and let the proxy reach ToolGUI over loopback.
A proxy that passes the browser's Host through needs nothing else. One that
rewrites it has to name the public origin, or the handshake will refuse the
browser it is proxying for:
e.SetAllowedOrigins([]string{"https://tools.example.com"})
Each entry is a full origin, scheme and all, the way a browser sends it. The app's own origin is always allowed on top of these.