261021.1 · 7c63c8b4f6af0ce6fa07e2fe3a94da12d54c1434
The program is called www
Two other tools are called mdserve, so an install of that name may get another program. NAME now holds www, and every path, variable, URL and journal tag follows: WWW_ADDR, WWW_SHARE, ~/.local/state/www, .www-follow and /_www/. Old names stop working.
The name test skips web hosts and the form media type, which hold www without naming the program.
reading declarations
NAME
@@ -1 +1 @@-mdserve+www
@@ -1,4 +1,4 @@-# mdserve+# www Browse trees of Markdown docs in the browser, rendered on each request, with their git history. @@ -7,27 +7,27 @@ It is fast. A page from a running server takes one to three milliseconds, a serv ## Usage ```-mdserve [-fnh] [-a ip[:port]] [dir]-mdserve [-fnh] --share[=host[:port]] [--secret[=token]] [dir]-mdserve --restart [-a ip[:port] | --share[=host[:port]]]-mdserve -l [-a ip[:port] | --share[=host[:port]]] [dir]-mdserve --kill [-a ip[:port] | --share[=host[:port]]]+www [-fnh] [-a ip[:port]] [dir]+www [-fnh] --share[=host[:port]] [--secret[=token]] [dir]+www --restart [-a ip[:port] | --share[=host[:port]]]+www -l [-a ip[:port] | --share[=host[:port]]] [dir]+www --kill [-a ip[:port] | --share[=host[:port]]] ``` -Run `mdserve` in a directory. It prints the tree's URL, opens the browser and writes a lockfile named after the endpoint, such as `127.31.1.1-8080.lock`.+Run `www` in a directory. It prints the tree's URL, opens the browser and writes a lockfile named after the endpoint, such as `127.31.1.1-8080.lock`. `cat` the lockfile to see the URL, root, PID and start time. Delete the lockfile to stop serving the tree. The Delete lock button on the tree's pages does the same.-The URL is all it prints, so `url=$(mdserve -n)` works. Running `mdserve` again in a served directory prints the same URL and starts nothing.+The URL is all it prints, so `url=$(www -n)` works. Running `www` again in a served directory prints the same URL and starts nothing. -Trees on one address share one server. The first `mdserve` starts it in the background, and later ones hand their tree to it and return.-When something other than your mdserve on that address holds the port, `mdserve` fails at once with `E98` and names the listeners there.+Trees on one address share one server. The first `www` starts it in the background, and later ones hand their tree to it and return.+When something other than your www on that address holds the port, `www` fails at once with `E98` and names the listeners there. The server exits when its last lock is gone. SIGTERM stops it at once and removes every lock, as does the Kill server button on the index at `/`.-A running server keeps the binary it started with. `mdserve --restart` moves every server's trees to the current binary, and `make install` and `make install-deb` run it for you; open tabs reload onto the new binary. A binary without the capability for port 80, such as the one from `make install`, leaves a port-80 server on its old binary and says so.+A running server keeps the binary it started with. `www --restart` moves every server's trees to the current binary, and `make install` and `make install-deb` run it for you; open tabs reload onto the new binary. A binary without the capability for port 80, such as the one from `make install`, leaves a port-80 server on its old binary and says so. The index at `/` lists the server's locks: each tree with the time its lock was written and a Delete lock button, and the lock's path on hover. Trees on other addresses follow as plain links, since only their own server may stop them. The header shows when the server started, and a warning appears when the binary on disk has changed since.-To reach the server by name, add a line such as `127.31.1.1 mdserve` to `/etc/hosts`. Type the name with its slash, eg `mdserve/`, or the browser searches for it.+To reach the server by name, add a line such as `127.31.1.1 www` to `/etc/hosts`. Type the name with its slash, eg `www/`, or the browser searches for it. -- `-a`, `--addr ip[:port]`: serve on another loopback address; the port defaults to 80 where mdserve may bind it, else 8080+- `-a`, `--addr ip[:port]`: serve on another loopback address; the port defaults to 80 where www may bind it, else 8080 - `--share[=host[:port]]`: share the tree on the default route's address, or on the address or name given beyond loopback, such as `192.168.1.5`, `0.0.0.0` or `mate.peer.home`; see [sharing](doc/sharing.md) - `--secret[=token]`: serve a shared tree under a random token, or the one given, so only those holding the link reach it - `-f`, `--foreground`: when no server runs on the address yet, serve in the foreground until Ctrl-C@@ -39,16 +39,16 @@ To reach the server by name, add a line such as `127.31.1.1 mdserve` to `/etc/ho - `--kill`: stop every server and print the trees it served, as `--list` does; with `-a` or `--share`, only the server on that address - `-h`, `--help`: print the options -Options follow GNU style. Short ones combine, as in `-fn`, and options may follow the directory. Optional values go joined, as in `--share=192.168.1.5`; with a space the address would be read as the directory, and mdserve says so.+Options follow GNU style. Short ones combine, as in `-fn`, and options may follow the directory. Optional values go joined, as in `--share=192.168.1.5`; with a space the address would be read as the directory, and www says so. Two variables set the default addresses. `--help` shows each value and where it comes from, and an error from a bad value names its variable. -- `MDSERVE_SHARE`: the address or name for a bare `--share`, eg `mate.peer.home`, in a start and in `--kill`, `--restart` and `--list`. Without it a bare `--share` takes the default route's address-- `MDSERVE_ADDR`: the loopback address for a start without `-a`. `--list`, `--kill` and `--restart` without an address still cover every server+- `WWW_SHARE`: the address or name for a bare `--share`, eg `mate.peer.home`, in a start and in `--kill`, `--restart` and `--list`. Without it a bare `--share` takes the default route's address+- `WWW_ADDR`: the loopback address for a start without `-a`. `--list`, `--kill` and `--restart` without an address still cover every server ## Sharing -`mdserve --share` serves the tree to other machines on the LAN from a server of its own. It serves only what other users could read, hides dot entries such as `.git` and shows nothing of the machine around the tree. `--secret` puts the tree under a random token. See [doc/sharing.md](doc/sharing.md).+`www --share` serves the tree to other machines on the LAN from a server of its own. It serves only what other users could read, hides dot entries such as `.git` and shows nothing of the machine around the tree. `--secret` puts the tree under a random token. See [doc/sharing.md](doc/sharing.md). ## History @@ -56,20 +56,20 @@ Add `?log` to a path for its git history, newest first: one line per commit, whi ## Log -mdserve logs every run, server event and page request to the systemd journal: `journalctl --user -t mdserve`. Without a journal it writes `~/.local/state/mdserve/log`. See [doc/log.md](doc/log.md).+www logs every run, server event and page request to the systemd journal: `journalctl --user -t www`. Without a journal it writes `~/.local/state/www/log`. See [doc/log.md](doc/log.md). ## Install -mdserve needs git.-`make install` links `build/mdserve` into `~/.local/bin`.-`make deb` builds a Debian package in `build/`, such as `mdserve_0~git20261015223930.20f02fd_amd64.deb`, versioned by the last commit's UTC time and hash. It depends on git. For another architecture, pass `ARCH`, eg `make deb ARCH=arm64`.+www needs git.+`make install` links `build/www` into `~/.local/bin`.+`make deb` builds a Debian package in `build/`, such as `www_0~git20261015223930.20f02fd_amd64.deb`, versioned by the last commit's UTC time and hash. It depends on git. For another architecture, pass `ARCH`, eg `make deb ARCH=arm64`. `make install-deb` builds the package while sudo asks for the password, then installs it with `sudo apt install`, which pulls in git. It first removes the `~/.local/bin` link from `make install`, which would otherwise shadow the package's binary. Run it again to replace an installed build, even of the same commit. `make uninstall-deb` removes the package. -To try a change without installing it, `make run` builds `build/mdserve` and serves this directory in the foreground on port 8080 with `--stamp`, beside an installed server on port 80. Stop it with Ctrl-C before the next run; a run that finds a server on 8080 hands its tree to that server, which keeps the old code.+To try a change without installing it, `make run` builds `build/www` and serves this directory in the foreground on port 8080 with `--stamp`, beside an installed server on port 80. Stop it with Ctrl-C before the next run; a run that finds a server on 8080 hands its tree to that server, which keeps the old code. The program's name lives in the file `NAME`. The binary, its paths, environment variables, journal tag and URLs, the Makefile and the Debian package all take it from there, and a test fails on any other copy outside prose and `go.mod`. -The package grants `/usr/bin/mdserve` one capability, `cap_net_bind_service`, so it can bind port 80 while running as the user. `getcap /usr/bin/mdserve` shows it. With it mdserve serves on port 80 and URLs carry no port; without it, as after `make install`, the port is 8080. mdserve also takes port 80 where the machine lets every user bind low ports.+The package grants `/usr/bin/www` one capability, `cap_net_bind_service`, so it can bind port 80 while running as the user. `getcap /usr/bin/www` shows it. With it www serves on port 80 and URLs carry no port; without it, as after `make install`, the port is 8080. www also takes port 80 where the machine lets every user bind low ports. ## Notes @@ -82,26 +82,26 @@ A code page anchors lines as GitHub does, so `main.go#L42` lands on line 42 and A server keeps git's answers so most pages run no git at all: which repo each directory lies in, and the histories pages show. It reads HEAD from the repo's files, and a history is kept under the commit HEAD names, so a new commit misses the cache and nothing shown goes stale. A `.git` that appears or goes makes the server ask git again. `--cache` sets the memory for it. The cache holds only what someone has visited, with one exception. When a tree joins, the server asks git in the background for what its listing and its README show, since those open first. The log says how long that took.-In a large repo git can take a second for a history, because it reads every commit back to the ones that touched the path. When it takes over 100 ms, the history page says so and names `git commit-graph write --reachable --changed-paths`, which builds git's index of the paths each commit changed and makes such histories several times faster. mdserve never writes to the repo itself.+In a large repo git can take a second for a history, because it reads every commit back to the ones that touched the path. When it takes over 100 ms, the history page says so and names `git commit-graph write --reachable --changed-paths`, which builds git's index of the paths each commit changed and makes such histories several times faster. www never writes to the repo itself. Open pages reload when what they show changes. A doc or code page reloads when its file is written, a listing when a name in it changes, any page in a repo when its HEAD moves, and the index when a tree joins or leaves. A page whose file or tree is gone waits two seconds for it to return, then reloads to say so. A page whose server stops reloads after three seconds without it, so the browser says the server is gone; a restart takes about 60 ms, and its new server reloads the page first. Each page holds a server-sent events stream while its tab is visible; a hidden tab lets go, since a browser opens at most six connections to one host, and checks again when shown. The server watches the directory of each open page and its repo's git directory through inotify, since editors save by renaming a new file over the old one, and polls every second where inotify fails. Docs follow CommonMark with GitHub's extensions, rendered by goldmark: tables, strikethrough, task lists, bare URLs as links, footnotes and definition lists. Lines join into a paragraph, and a blank line starts the next. A line that starts with a dash begins a list even right after text. Straight quotes and dashes turn typographic. Code in a fenced block with a known language is highlighted by chroma, as are code pages, whose language follows the file name. Go is lexed by Go's own scanner from the standard library instead, about 150 times faster, with the same colours. Every gap between blocks is the same, whether the block is a paragraph, list, table or code. Headings sit close to their text. -mdserve refuses to serve the home directory. A tree exposes everything below it, and home holds `~/.ssh`, `~/.gnupg` and every token file.-Symlinks inside the tree are followed, absolute ones included. A link that leads out of the tree is followed only into a directory that holds a file named `.mdserve-follow`, or below one, and only when other users could read the target. A file needs the others-read bit and a directory others-read and others-execute, checked from the marker's directory down. So nobody reads through mdserve what `cat` would not show them.+www refuses to serve the home directory. A tree exposes everything below it, and home holds `~/.ssh`, `~/.gnupg` and every token file.+Symlinks inside the tree are followed, absolute ones included. A link that leads out of the tree is followed only into a directory that holds a file named `.www-follow`, or below one, and only when other users could read the target. A file needs the others-read bit and a directory others-read and others-execute, checked from the marker's directory down. So nobody reads through www what `cat` would not show them. A store of private files linked into repos, such as lord's `~/.local/share/lord`, opens with one marker in its root. Secrets kept there at mode 600 stay refused. Opening a refused link shows the link, its target and the reason, and the listing marks it refused with the same on hover.-A loopback server answers only to loopback addresses, `localhost` and names that `/etc/hosts` maps to loopback. Its Content-Security-Policy forbids every script but mdserve's own, which reloads pages and opens history folds, and which it allows by hash. Raw HTML in a doc passes through, so a README in a cloned repo could otherwise run code on this origin.-mdserve runs no code a tree supplies. It reads files and runs git with every option that would start a program named in the repo's config turned off: the pager, textconv, external diff, signature checks, fsmonitor and fetching.+A loopback server answers only to loopback addresses, `localhost` and names that `/etc/hosts` maps to loopback. Its Content-Security-Policy forbids every script but www's own, which reloads pages and opens history folds, and which it allows by hash. Raw HTML in a doc passes through, so a README in a cloned repo could otherwise run code on this origin.+www runs no code a tree supplies. It reads files and runs git with every option that would start a program named in the repo's config turned off: the pager, textconv, external diff, signature checks, fsmonitor and fetching. -Each endpoint has a registry directory such as `~/.local/state/mdserve/127.31.1.1-8080/` with a symlink to each lock its server holds. A joining `mdserve` adds a link, and the server sees it through inotify, writes the lock and serves the tree. Where inotify fails the server polls every second. An flock on the directory keeps a tree from joining a server on its way out. `$XDG_STATE_HOME` replaces `~/.local/state` when set.+Each endpoint has a registry directory such as `~/.local/state/www/127.31.1.1-8080/` with a symlink to each lock its server holds. A joining `www` adds a link, and the server sees it through inotify, writes the lock and serves the tree. Where inotify fails the server polls every second. An flock on the directory keeps a tree from joining a server on its way out. `$XDG_STATE_HOME` replaces `~/.local/state` when set. A lock left by a crash is detected on the next start through its PID and the kernel boot ID, then removed. A new server also clears what a crashed server left in the registry. -The stylesheet is mdserve's own. It began as md2html-default's, whose light palette it keeps, and the page colours sit as variables on `:root`. The browser loads it once: its URL names a hash of its content, eg `/_mdserve/style.7b94a08e7731.css`, so it is kept for a year, and a changed stylesheet comes under a new URL.-Pages load no fonts from the network. STIX Two Text, the serif, comes from the machine when installed and otherwise from the copy built into mdserve, which the browser caches for a year. The mono face is Iosevka where installed, else the system monospace.-With `--stamp`, each page says when its binary was built and how many seconds ago, the commit it was built from and how long the page took to render, eg `built 12:49, 108 s ago, from 1aa282c+modified, rendered in 3.3 ms`. It sits small and faint above the header, so a glance tells which build served the page. `+modified` marks a build from uncommitted code. mdserve's script counts the seconds up while the page is open. Shares leave the stamp out.-The header names a tree by its base name and shows the full path on hover. Every tree page's breadcrumbs start at `index`, which links `/`. The header, listings and mdserve's own messages are mono; documents keep the serif.+The stylesheet is www's own. It began as md2html-default's, whose light palette it keeps, and the page colours sit as variables on `:root`. The browser loads it once: its URL names a hash of its content, eg `/_www/style.7b94a08e7731.css`, so it is kept for a year, and a changed stylesheet comes under a new URL.+Pages load no fonts from the network. STIX Two Text, the serif, comes from the machine when installed and otherwise from the copy built into www, which the browser caches for a year. The mono face is Iosevka where installed, else the system monospace.+With `--stamp`, each page says when its binary was built and how many seconds ago, the commit it was built from and how long the page took to render, eg `built 12:49, 108 s ago, from 1aa282c+modified, rendered in 3.3 ms`. It sits small and faint above the header, so a glance tells which build served the page. `+modified` marks a build from uncommitted code. www's script counts the seconds up while the page is open. Shares leave the stamp out.+The header names a tree by its base name and shows the full path on hover. Every tree page's breadcrumbs start at `index`, which links `/`. The header, listings and www's own messages are mono; documents keep the serif. Every page keeps one width, 58ch of the body font, and keeps space for a scrollbar, so the header holds still from page to page. Code is half the size of the body text, never under 11px, so about 115 columns fit; longer lines scroll. Add `*.*.*.*-*.lock` to `~/.config/git/ignore` so locks stay out of `git status`.
@@ -10,11 +10,11 @@ import ( "time" ) -// pageBudget is the time mdserve promises for any page from a warm server.+// pageBudget is the time www promises for any page from a warm server. const pageBudget = 5 * time.Millisecond // Every kind of page renders within pageBudget once its git answers are-// cached: a doc and a code page the size of mdserve's own README and+// cached: a doc and a code page the size of www's own README and // render.go, a listing, histories and a commit's diff. Each takes the // median of 15 runs, so one slow run does not fail it. func TestPageBudget(t *testing.T) {
@@ -3,9 +3,9 @@ Add `?log` to a path for its git history: the commits that touched it, newest first, one line each with hash, date and subject. A line opens to the commit's full message and its diff, limited to the path; the diff loads when the line first opens. A commit with no message shows its hash and date alone. A directory's history covers everything below it. A listing whose directory has commits opens its history from its first entry, `<(git log)`. A doc or code page with commits opens with a line that leads there and names its last commit. A page outside a repo shows git's complaint. A history lists the 500 newest commits. Bodies wrapped by hand join into paragraphs; a list item or an indented line keeps its own line. A commit's hash, in a history or at a page's top, opens the commit on a page of its own: its message and its diff, limited to the file or directory the history was for, with a link to all the files it changed in the tree. Added lines sit on a green tint and removed ones on a red. The diff stops at 1 MiB.-mdserve runs `git log`, `git show` and `git rev-parse` in the directory of what the page shows, after links resolve. A cloned repo's `.git/config` can name programs for git to run, so mdserve turns off external diff, textconv, signature checks and fetches, and drops `GIT_` variables from its environment. git's own `safe.directory` check refuses repos that other users own.+www runs `git log`, `git show` and `git rev-parse` in the directory of what the page shows, after links resolve. A cloned repo's `.git/config` can name programs for git to run, so www turns off external diff, textconv, signature checks and fetches, and drops `GIT_` variables from its environment. git's own `safe.directory` check refuses repos that other users own. ## On shares -A shared server shows history and diffs as loopback does, with four differences. History appears only when other users could read the repo's git directory, its `HEAD`, `objects` and `refs`; nobody reads through mdserve what git run as another user would not show them. Each message loses its trailer block, which carries emails and session links in lines such as `Signed-off-by:` and `Claude-Session:`. As in git, the last paragraph is a trailer block when a quarter of its lines are `Key: value` lines, so a cherry-pick note or a wrapped trailer does not keep it. A page in a repo the share keeps private never reloads on a commit. Diffs leave out files whose names begin with a dot, as listings do. A share never shows git's errors, which name paths on the machine, and says only `No git history here.`+A shared server shows history and diffs as loopback does, with four differences. History appears only when other users could read the repo's git directory, its `HEAD`, `objects` and `refs`; nobody reads through www what git run as another user would not show them. Each message loses its trailer block, which carries emails and session links in lines such as `Signed-off-by:` and `Claude-Session:`. As in git, the last paragraph is a trailer block when a quarter of its lines are `Key: value` lines, so a cherry-pick note or a wrapped trailer does not keep it. A page in a repo the share keeps private never reloads on a commit. Diffs leave out files whose names begin with a dot, as listings do. A share never shows git's errors, which name paths on the machine, and says only `No git history here.` No history page shows authors, on a share or on loopback.
@@ -1,22 +1,22 @@ # Log -mdserve logs every action, always. There is no flag and no level to set, because a filter set before a mistake drops the entry that would explain it.+www logs every action, always. There is no flag and no level to set, because a filter set before a mistake drops the entry that would explain it. -Entries go to the systemd journal, tagged `mdserve`:+Entries go to the systemd journal, tagged `www`: ```-journalctl --user -t mdserve --since -1h-journalctl --user -t mdserve MDSERVE_ENDPOINT=127.31.1.1-80-journalctl --user -t mdserve -p err+journalctl --user -t www --since -1h+journalctl --user -t www WWW_ENDPOINT=127.31.1.1-80+journalctl --user -t www -p err ``` -Each entry carries the endpoint it concerns in `MDSERVE_ENDPOINT` and its priority in `PRIORITY`: 3 for faults, 6 for the rest. The journal also holds what explains a vanished server, such as a suspend, an OOM kill or a logout.-Where no journal socket exists, entries go to `~/.local/state/mdserve/log` instead, or below `$XDG_STATE_HOME` when set. Its lines hold time, PID, endpoint, priority and the message. The file is mode 600 and rotates once to `log.1` at 1 MB. `mdserve --help` names the place in use.+Each entry carries the endpoint it concerns in `WWW_ENDPOINT` and its priority in `PRIORITY`: 3 for faults, 6 for the rest. The journal also holds what explains a vanished server, such as a suspend, an OOM kill or a logout.+Where no journal socket exists, entries go to `~/.local/state/www/log` instead, or below `$XDG_STATE_HOME` when set. Its lines hold time, PID, endpoint, priority and the message. The file is mode 600 and rotates once to `log.1` at 1 MB. `www --help` names the place in use. ## What is logged -Every run of `mdserve`, with its arguments, working directory and binary, and its outcome when it fails. A run that joins a server or finds its tree already served says so.-Each server's start with its binary and commit, eg `/usr/bin/mdserve 54cca2a`, and `+modified` for a build from uncommitted code. A stale binary is the first suspect when a page looks wrong.+Every run of `www`, with its arguments, working directory and binary, and its outcome when it fails. A run that joins a server or finds its tree already served says so.+Each server's start with its binary and commit, eg `/usr/bin/www 54cca2a`, and `+modified` for a build from uncommitted code. A stale binary is the first suspect when a page looks wrong. Each tree served and withdrawn, with the cause: a deleted lock, a Delete lock or Kill server button, or a signal. Then the server's stop and exit. Stale locks and registry links cleared after a crash, and the SIGTERM that `--kill` and `--restart` send. Every page request, with method, path, status and time taken. Live reload streams, the stylesheet, fonts and the favicon come with every page and are left out. A shared server adds the visitor's address.
@@ -1,22 +1,22 @@ # Sharing -`mdserve --share [dir]` serves a tree on the address of the default route, so other machines on the LAN can read it. mdserve asks the kernel which source address it would use to leave the machine, and the printed URL shows it. Over a full-tunnel VPN that is the tunnel's address.+`www --share [dir]` serves a tree on the address of the default route, so other machines on the LAN can read it. www asks the kernel which source address it would use to leave the machine, and the printed URL shows it. Over a full-tunnel VPN that is the tunnel's address. -`MDSERVE_SHARE`, eg `MDSERVE_SHARE=mate.peer.home`, gives a bare `--share` another address, for `--kill --share` and `--restart --share` as well.+`WWW_SHARE`, eg `WWW_SHARE=mate.peer.home`, gives a bare `--share` another address, for `--kill --share` and `--restart --share` as well. -`--share=ip[:port]` names the address instead, joined with `=`: `--share 192.168.1.5` would read the address as the directory. Any IPv4 address the machine lets mdserve bind will do, `0.0.0.0` included. A name will do too, such as `--share=mate.peer.home`: mdserve binds the address it resolves to, which an interface here must hold.+`--share=ip[:port]` names the address instead, joined with `=`: `--share 192.168.1.5` would read the address as the directory. Any IPv4 address the machine lets www bind will do, `0.0.0.0` included. A name will do too, such as `--share=mate.peer.home`: www binds the address it resolves to, which an interface here must hold. -The printed URL carries the address's name when its reverse lookup resolves back to it, so a share on a WireGuard address with its own DNS prints `http://mate.peer.home/notes/`. Otherwise it carries the address. The port defaults to 80 where mdserve may bind it, else 8080, as on loopback. `-a` takes only loopback addresses, so a typo there cannot share a tree.+The printed URL carries the address's name when its reverse lookup resolves back to it, so a share on a WireGuard address with its own DNS prints `http://mate.peer.home/notes/`. Otherwise it carries the address. The port defaults to 80 where www may bind it, else 8080, as on loopback. `-a` takes only loopback addresses, so a typo there cannot share a tree. -A shared address gets a server of its own, apart from the loopback one. More trees join it with the same `--share` address. The lock is named after the address, such as `192.168.1.5-80.lock`, and deleting it stops the share. `mdserve --kill --share` stops every tree on that address. `mdserve --restart --share` moves the trees to a new binary. `mdserve -l` lists shared trees with the rest.+A shared address gets a server of its own, apart from the loopback one. More trees join it with the same `--share` address. The lock is named after the address, such as `192.168.1.5-80.lock`, and deleting it stops the share. `www --kill --share` stops every tree on that address. `www --restart --share` moves the trees to a new binary. `www -l` lists shared trees with the rest. -On `0.0.0.0` the printed URL carries the machine's name, since the server answers on every address. `0.0.0.0` claims its port on every address, so it cannot bind a port that any listener holds, the loopback mdserve included. mdserve then fails with `E98` and names the listeners on that port. Share on one address, or give `0.0.0.0` another port.+On `0.0.0.0` the printed URL carries the machine's name, since the server answers on every address. `0.0.0.0` claims its port on every address, so it cannot bind a port that any listener holds, the loopback www included. www then fails with `E98` and names the listeners on that port. Share on one address, or give `0.0.0.0` another port. ## What a visitor sees A shared tree mounts at its base name, eg `http://192.168.1.69/notes/`. The URL does not reveal where the tree lives on the machine. -The server serves only what other users on the machine could read. A file needs the others-read bit, and every directory from the tree's root down to it needs others-read and others-execute. Links out of the tree follow the `.mdserve-follow` rule as on loopback. Names that begin with a dot are neither listed nor served, at any depth, nor are links that lead to them inside the tree: `.git` holds the history and every deleted file. History pages show commit messages alone, without trailers, and only when other users could read the repo. Everything else in the tree is served, so share a directory that holds only what you mean to share.+The server serves only what other users on the machine could read. A file needs the others-read bit, and every directory from the tree's root down to it needs others-read and others-execute. Links out of the tree follow the `.www-follow` rule as on loopback. Names that begin with a dot are neither listed nor served, at any depth, nor are links that lead to them inside the tree: `.git` holds the history and every deleted file. History pages show commit messages alone, without trailers, and only when other users could read the repo. Everything else in the tree is served, so share a directory that holds only what you mean to share. A refused entry shows as refused, without the reason, since the reason names paths outside the tree. The index at `/` lists the shared trees by name. It shows no lock paths, no other addresses and no buttons. No page has a Delete lock or Kill server button, so no visitor can stop the server. @@ -31,7 +31,7 @@ note: other machines connect to 192.168.1.69:80; a firewall here may drop them u Check with: sudo tcpdump -ni wlp0s20f3 tcp port 80 ``` -mdserve cannot find out by itself. A firewall drops the connection before any socket sees it, reading the ruleset needs root, and a test from this machine goes through loopback and passes.+www cannot find out by itself. A firewall drops the connection before any socket sees it, reading the ruleset needs root, and a test from this machine goes through loopback and passes. Run the capture and load the URL on the other machine:
@@ -50,7 +50,7 @@ type commit struct { // run, so the options that would start one are off, fsmonitor among them // whichever command runs, and git may not fetch a partial clone's missing // objects. Pathspecs are literal, and no GIT_-// variable from mdserve's own environment leaks in. git's safe.directory+// variable from www's own environment leaks in. git's safe.directory // check refuses repos that other users own. func git(ctx context.Context, dir string, args ...string) ([]byte, error) { args = append([]string{"-c", "protocol.allow=never", "-c", "core.fsmonitor=false", "-c", "core.quotePath=false", "--no-pager", "--literal-pathspecs"}, args...)@@ -353,7 +353,7 @@ func (t *tree) serveLog(w http.ResponseWriter, r *http.Request, name string, dir } // git walks every commit back to the ones that touched name, unless // the repo's commit-graph tells it which paths each commit changed.- // mdserve leaves the repo alone and names the command instead. A share+ // www leaves the repo alone and names the command instead. A share // says nothing, since its viewer cannot run it. if took > slowGit && !t.shared && t.lacksPathFilters(r.Context(), name) { fmt.Fprintf(&body, "<p class=\"own\">git took %d ms for this history. "+
@@ -1,4 +1,4 @@-module mdserve+module www go 1.25
@@ -48,7 +48,7 @@ var goneWait = 2 * time.Second // live tells open pages when what they show has changed. A page names its // version: the server instance that rendered it and what the page showed.-// It keeps a stream open on /_mdserve/live, which wakes on any change in+// It keeps a stream open on /_www/live, which wakes on any change in // the directories the page reads from and sends one event, reload, when // the version is no longer current. type live struct {
@@ -14,10 +14,10 @@ import ( "time" ) -// mdserve logs every action, so a later look can tell what the user and-// mdserve did: each run with its arguments, each server's start and stop+// www logs every action, so a later look can tell what the user and+// www did: each run with its arguments, each server's start and stop // with the cause, trees joining and leaving, and page requests. Entries go-// to the systemd journal, tagged mdserve, or to a file in the state+// to the systemd journal, tagged www, or to a file in the state // directory where no journal runs. Nothing is filtered when written; the // journal's PRIORITY filters when read. @@ -105,7 +105,7 @@ func journalEntry(prio int, ep, msg string) []byte { return b.Bytes() } -// logFile is the fallback log, eg ~/.local/state/mdserve/log.+// logFile is the fallback log, eg ~/.local/state/www/log. func logFile() (string, error) { base, err := stateDir() if err != nil {@@ -188,7 +188,7 @@ func logSink() *os.File { return f } -// build names the running binary and its commit, eg /usr/bin/mdserve+// build names the running binary and its commit, eg /usr/bin/www // 54cca2a, with +modified for a build from uncommitted code. A stale binary // is the first thing to rule out. func build() string {
@@ -1,4 +1,4 @@-// Command mdserve browses trees of Markdown docs in the browser. It renders+// Command www browses trees of Markdown docs in the browser. It renders // each page with goldmark when requested and mirrors the file tree in its // URLs. Trees on one address share a server, which lives while their locks // exist.@@ -315,9 +315,9 @@ var ( shareEnv = envPrefix + "SHARE" ) -// endpointOf returns where c serves. A bare --share takes MDSERVE_SHARE,-// and a start without -a takes MDSERVE_ADDR; --list, --kill and --restart-// without an address cover every server, so MDSERVE_ADDR leaves them be.+// endpointOf returns where c serves. A bare --share takes WWW_SHARE,+// and a start without -a takes WWW_ADDR; --list, --kill and --restart+// without an address cover every server, so WWW_ADDR leaves them be. // An error from a variable's value names the variable. func endpointOf(c config, uid int) (netip.AddrPort, error) { if c.sharing {@@ -504,7 +504,7 @@ func validToken(s string) bool { }) } -// errno is an error with the code mdserve exits with.+// errno is an error with the code www exits with. type errno struct { code syscall.Errno msg string@@ -570,7 +570,7 @@ func openBrowser(url string) { } } -// detach re-runs mdserve in a new session and relays its start report.+// detach re-runs www in a new session and relays its start report. // Go cannot fork safely, so a re-exec is the way to background itself. func detach(c config) int { r, w, err := os.Pipe()
@@ -14,7 +14,7 @@ var nameFile string var progName = strings.TrimSpace(nameFile) // envPrefix starts the name of every environment variable the program-// reads, eg MDSERVE_ADDR.+// reads, eg WWW_ADDR. var envPrefix = strings.ToUpper(progName) + "_" // ownPath is the URL path of the program's own resources, eg the
@@ -13,7 +13,8 @@ import ( // The name lives in NAME alone. Go code takes it from progName, the Makefile // and the package files from NAME, so a tracked file names the program // only in prose: comments, README.md, doc/ and go.mod, which Go needs as-// a literal.+// a literal. A name such as www also occurs inside words that belong to+// others, which foreign strips first. func TestNameHasOneSource(t *testing.T) { needGit(t) out, err := exec.Command("git", "ls-files").Output()@@ -38,16 +39,20 @@ func TestNameHasOneSource(t *testing.T) { if tok == token.EOF { break }- if (tok == token.STRING || tok == token.CHAR) && strings.Contains(strings.ToLower(lit), lower) {+ if (tok == token.STRING || tok == token.CHAR) && strings.Contains(foreign.Replace(strings.ToLower(lit)), lower) { t.Errorf("%s: %s", fset.Position(pos), lit) } } continue } for i, line := range strings.Split(string(src), "\n") {- if !strings.HasPrefix(strings.TrimSpace(line), "#") && strings.Contains(strings.ToLower(line), lower) {+ if !strings.HasPrefix(strings.TrimSpace(line), "#") && strings.Contains(foreign.Replace(strings.ToLower(line)), lower) { t.Errorf("%s:%d: %s", f, i+1, line) } } } }++// foreign removes the words that hold www without naming the program: web+// hosts and the form media type.+var foreign = strings.NewReplacer("://www.", "://", "x-www-form-urlencoded", "")
@@ -11,8 +11,8 @@ import ( ) // A registry directory joins trees to the server of one endpoint, eg-// ~/.local/state/mdserve/127.31.1.1-8080/, with one symlink per lock. A-// joining mdserve adds a link; the server sees it through inotify, writes+// ~/.local/state/www/127.31.1.1-8080/, with one symlink per lock. A+// joining www adds a link; the server sees it through inotify, writes // the lock and serves the tree. An flock on the directory orders a join // against the server's start and exit, so no tree joins a dying server. type registry struct {
@@ -164,7 +164,7 @@ func originHost(host string, port uint16) string { // report ends a start with the tree's URL alone, the same whether this run // started the tree or found it served. A share adds a note on stderr: a-// firewall drops other machines before mdserve sees them, and a test from+// firewall drops other machines before www sees them, and a test from // this machine passes through loopback, so only a capture tells. func report(rep *reporter, ep netip.AddrPort, url string) { rep.send("url", url)@@ -331,7 +331,7 @@ func (h *hub) dropLeftovers() { } // adopt serves the tree whose lock is lockPath and writes the lock. link is-// the registry entry a joining mdserve made, or "" to make one. token puts+// the registry entry a joining www made, or "" to make one. token puts // a shared tree under a secret path. func (h *hub) adopt(lockPath, link, token string) (*tree, error) { if !filepath.IsAbs(lockPath) || filepath.Base(lockPath) != lockName(h.ep) {@@ -487,7 +487,7 @@ func (h *hub) shutdown(why string) { } // halt closes the listener while the registry is locked, so a joining-// mdserve finds the address free and starts a new server instead of+// www finds the address free and starts a new server instead of // linking into this one. Open connections drain in the background. The // caller holds the lock. func (h *hub) halt(why string) {
@@ -86,7 +86,7 @@ func testEndpoint(t *testing.T) netip.AddrPort { return ep } -// start runs mdserve for dir as a detached child would and returns its+// start runs www for dir as a detached child would and returns its // report lines once it finished reporting, and a channel with its exit code. func start(t *testing.T, c config) ([]string, chan int) { t.Helper()
@@ -398,7 +398,7 @@ func guard(next http.Handler, anyHost bool) http.Handler { var hostsFile = "/etc/hosts" // localHost accepts a loopback address, localhost, or a name that the hosts-// file maps to loopback, eg an mdserve instance named there. DNS is never+// file maps to loopback, eg an www instance named there. DNS is never // asked: a DNS name that resolves to loopback is the rebinding attack itself. func localHost(host string) bool { if strings.EqualFold(host, "localhost") {@@ -845,7 +845,7 @@ type crumb struct { } // indexCrumb leads every tree page's breadcrumbs to the index, the parent-// of all trees. Its label is not mdserve, which a tree may be called.+// of all trees. Its label is not www, which a tree may be called. var indexCrumb = crumb{Label: "index", Href: "/"} type page struct {
@@ -242,7 +242,7 @@ func TestIndexDeleteReturns(t *testing.T) { } } -// The fonts come from mdserve, cached for a year; nothing else leaves the+// The fonts come from www, cached for a year; nothing else leaves the // font directory. func TestFonts(t *testing.T) { s, _, _ := serveTree(t)
@@ -285,7 +285,7 @@ func TestSpacedShareHint(t *testing.T) { } } -// MDSERVE_SHARE fills a bare --share and MDSERVE_ADDR a start without -a.+// WWW_SHARE fills a bare --share and WWW_ADDR a start without -a. // Flags win, --list, --kill and --restart keep covering every server, and // a bad value names its variable. func TestEnvironmentDefaults(t *testing.T) {
@@ -9,9 +9,9 @@ import ( "strings" ) -// taken explains why ep cannot be bound when no mdserve of this user serves+// taken explains why ep cannot be bound when no www of this user serves // there: it names the listeners that hold the port, marking the ones that-// are mdserve servers on other addresses.+// are www servers on other addresses. func taken(ep netip.AddrPort) string { var names []string for _, l := range holders(ep) {