pkgproxy design: setup page
The setup page the daemon serves for remote clients, and the desktop launcher that opens it. Part of the pkgproxy design, whose index says which file holds each § number.
20. Setup page for remote clients
pkgproxyctl setup <tool> (§14) only helps on a machine that has pkgproxy
installed. For someone who wants to use an instance on their LAN, or a
public one, the daemon serves a page at http://host:3142/ that says how.
Without it that address would be a 404: the gateway sends the snap relay
only /v2, /api/v1 and /v1 (§11), after the Go, LXD and Docker sides,
and answers any other path itself.
20.1 Routes
The page wraps the plain listener’s handler (cmd/pkgproxy/setuppage.go).
It takes only GET and HEAD of origin-form requests for exactly /, anything
under /setup/ and, from a local visitor, exactly /pkgproxy/cache
(§20.7); forward-proxy requests (CONNECT, absolute
URIs) and every other path go through to internal/gateway unchanged. No
client protocol pkgproxy serves uses these paths: snapd’s API is under
/v2/ and /api/, Docker’s under /v2/, Go’s and LXD’s under their base
paths.
GET /is the landing page: HTML whenAcceptrankstext/htmlabovetext/plain(a browser), a plain-text rendering throughguide(§22) otherwise (curl sends*/*).GET /setup/<tool>is a shell script, and?undo=1its undo counterpart, forcurl -fsSL http://host:3142/setup/apt | sudo sh. It is#!/bin/shfollowed by exactly the linespkgproxyctl setup <tool>prints when piped: both call the same per-tool function, so they cannot drift apart. It is served astext/plain, nottext/x-shellscript, so that a browser shows it to be read rather than downloading it.
The page offers apt, snap, go, docker and lxd-remote, each only when its
side is enabled (docker unless -docker-upstream off, lxd-remote only
with remotes configured and the HTTPS listener on). setup lxd and
setup snapcraft are left out: their scripts call pkgproxy’s local admin
socket (§16.9) and ask the LXD of the machine they run on for its bridge, so
they cannot work from another machine (gitlab.com/zygoon/pkgproxy#51).
It is on by default (-setup-page=false turns it off): it reveals nothing
that an open forward proxy does not already.
The HTML page is self-contained, with no requests for anything but
itself: the visitor may not yet reach anything but this proxy. Its
styles and copy-button script are inline, and the application icon
(data/pkgproxy.svg, the snap’s icon, embedded by package data) is
inlined as a data: URI for both the favicon and the header.
20.2 Which address the output names
The commands must name an address the visitor can reach. -addr usually
cannot be that: its host is a wildcard, and proxyHost’s substitute, the
machine’s hostname, may not resolve elsewhere (mDNS often does not). The
request’s Host header is the address the visitor just reached pkgproxy
at, so it is used, in place of -addr, as input to the same URL helpers
the CLI uses — but only when isPlainHostPort accepts it: a DNS name of
letters, digits, dots and hyphens, an IPv4 address or a bracketed IPv6
address without a zone, and an optional decimal port. It ends up inside
shell commands, so anything else (including no Host, as over a UNIX
socket) falls back to -addr. -public-url http://host[:port] overrides
both, for an instance reached through a name or port that differs from
what clients send, such as behind NAT. The LXD remote URL keeps the port
of -lxd-tls-addr on whichever host was chosen.
20.3 Language
The prose follows Accept-Language, matched by l10n.AcceptLanguage
against the languages with a catalog. The locale is put only on a context
used to render the response, never on the request’s: a request context
carrying the client’s locale would put that request’s log lines in the
client’s language (gitlab.com/zygoon/pkgproxy#28).
20.4 Trust
The scripts travel over plain HTTP, so piping one to sh trusts the
network between the visitor and pkgproxy, exactly as using the proxy at
all does. The page says so, links each script so that it can be read
first, and lists the plain commands under each one-liner, for setup and
for undo alike, for anyone who would rather paste them, or has no
curl, as minimal Debian and Ubuntu images do not.
20.5 Desktop launcher
The snap has an applications menu entry that opens the setup page. Its
Exec is pkgproxy.open, an app of its own running pkgproxyctl open
with only the desktop plug: the pkgproxyctl app, often run with sudo,
gets no session-bus access. snapd’s desktop file sanitizer accepts an
Exec that names one of the snap’s apps and an Icon path under
${SNAP}; the translated Comment, GenericName and Keywords live in
the file itself, since a desktop file has no catalog.
pkgproxyctl open asks the daemon for /pkgproxy/v1/info over the stats socket
(§13.5), retrying briefly while the socket is not there yet (right
after install); a daemon that omits setup_page, as an older one during
a refresh does, counts as serving the page. It turns addr into a URL
for this machine (a wildcard host becomes loopback) and ignores
-public-url, which is how other machines reach pkgproxy and may not
work from this one (NAT without hairpinning). It then runs xdg-open,
which in a strict snap is snapctl user-open, handing the URL to the
desktop portal or snapd’s userd; the desktop interface allows both,
and no network plug is needed, as nothing is probed over TCP.
Failures go to stderr, worded “cannot …” with the fix, and exit 1. A
menu entry has no terminal to show them, so the help and the README say
to run pkgproxy.open in a terminal. A desktop notification is left out,
as it would need a D-Bus client.
20.6 Offline notice
While pkgproxy refuses upstream requests (the mode “offline”, or an open
or half-open breaker in “store-overloaded”, §21) the page shows a notice
(setupPage.Notice, its own template block), rendered at each request
without JavaScript: what fails and that the visitor may try later or undo
the setup. “store-overloaded” with every breaker closed shows none. It
names the hosts left alone and when each is tried again, nothing else of
the requests that opened them: they tell a visitor whether their tools are
affected, and a request through that host would say as much with a 503.
It names no command, since a visitor cannot change the mode.
20.7 Cache status
On the machine that runs pkgproxy, the cache page at /pkgproxy/cache
shows how the cache is doing: its size on disk, the free space of its file
system (statfs(2), Linux only), the downloads saved
(stats.Recorder.BytesSaved, content only:
§13.5), the metadata served besides in one
sentence when there is any (stats.Recorder.MetadataServed), a table per
kind with the downloads saved by each, and a treemap of what fills the
cache. The setup page is for setting up clients and stays light: for a
local visitor it has one line under the introduction, the cache’s size and
its disk’s free space, linking to the cache page (“See what is cached.”),
which is how someone who opened the setup page from the launcher (§20.5)
finds it. The plain-text renderings carry the same line, with the cache
page’s URL at the address the visitor used (not -public-url’s, which may
not work from this machine), and the cache page’s summary and table.
The cache page is under /pkgproxy/, pkgproxy’s own part of the URL space
(gitlab.com/zygoon/pkgproxy#75 moves its API there), rather than at a bare
/cache: no client protocol uses /pkgproxy/, but -go-base-path or
-lxd-base-path could well be set to /cache.
internal/cacheusage.Scan walks -cache-dir summing allocated blocks, as
du does, each object with its .meta.json, without following symlinks
or crossing file systems; files outside a store’s objects/ count as
other pkgproxy data. Counting on write and delete would still need a walk
at start, races on overwrites and misses what bypasses store.Store. A
walk is cheap only while the inodes are cached, so cacheusage.Monitor
walks lazily, in the background: a view of either page starts one when the
last is over a minute old and waits for it up to 1 s, otherwise it shows
the previous report with its age and says it is measuring again, or
“measuring” before the first. The page says the sizes are as of that
walk; the downloads saved are counted as they happen. Nothing is walked
while nobody looks.
Each kind is split into groups and leaves: apt by archive and source
package; snaps by name and revision, from the recorder’s hash
associations (§13.3);
Go by module and version; LXD by image and serial, labelled with the remote
and release (pkgproxy-ubuntu noble), the whole path in the tooltip;
Docker blobs by the repository they were served through most, as they are
keyed by digest (§19.4).
What a report keeps is folded within each kind and group, not as a share of
the whole cache, so that a small kind (a hundred Go modules beside gigabytes
of snaps) still shows its own largest members. Rectangles are shared out by
area: a kind gets MapBudget (300) times its share of the cache, at least
MinKindBudget (32); it keeps at most half that many groups, and at most
MaxGroups (48); a group gets the kind’s budget times its share of the
kind for its leaves and their rest, and is drawn whole below two. Within
those counts the largest are kept while they take at least an eighth of
their parent’s average rectangle, and the rest becomes one “N more items”
rectangle. A map thus has at most 1.5 × (300 + 6 × 32) + 12 = 750
rectangles, a page of about 125 KB; a real-like cache of about 980 items
draws about 240, a page of 48 KB. A long tail (hundreds of small source
packages in one archive) still ends up in its group’s rest.
internal/treemap.Squarify lays out each level and the page inlines it as
SVG without JavaScript: a <title> tooltip per rectangle, a <title> and
<desc> summary, and the table as its visible text alternative and legend,
in the Okabe–Ito palette in both themes. A kind’s label is its name and
size, or its name alone when both do not fit, never cut mid-word; the
tooltip and the table give the size.
What the cache holds is what the machine downloads, so only a local
visitor sees either the line or the page, one who could read the stats
socket anyway: over TCP, never a UNIX socket listener (VM guests and other
snaps use those), from a loopback or own address, without Forwarded,
X-Forwarded-* or X-Real-IP (a reverse proxy on the host makes everyone
look local), for a Host that is an IP address, localhost or the host
name, against DNS rebinding. Anyone else’s request for /pkgproxy/cache
goes to the gateway, which answers 404 as for any path it does not know, so
the page’s existence is not revealed either. Answers with the cache’s
figures are Cache-Control: no-store.