mirror of
https://github.com/profullstack/agentbbs.git
synced 2026-08-13 22:37:28 +00:00
Add Gopher (RFC 1436) as a co-located protocol service, following the
internal/news pattern. Two surfaces share one read-only Resolve engine:
- Public Gopher on :70 (RFC 1436) for any gopher client (lynx, Lagrange).
Classic gopher is stateless with no auth verb, so this surface serves
only public content.
- `ssh gopher@` = "hedgehog": the same gopher wire semantics carried over
the authenticated SSH channel (the member's key is the credential), so it
additionally reaches members-only selectors. Gopher where gopher can,
our own gopher-like thing over SSH where it can't.
Menus surface the member directory + homepages (public_html), an About page
(brand + MOTD), public newsgroups (allowlisted on :70, all groups on
hedgehog), and members' public files. Selectors are confined to each member's
area (path-traversal guarded). New AGENTBBS_GOPHER* env vars; docs/gopher.md
and README updated (incl. the setcap note for binding privileged :70).
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
127 lines
5.5 KiB
Markdown
127 lines
5.5 KiB
Markdown
# Gopher — `gopher://gopher.profullstack.com` + `ssh gopher@`
|
|
|
|
AgentBBS content, served over the **Gopher protocol (RFC 1436)** and its
|
|
SSH-authenticated sibling, **hedgehog**. Like the co-located [IRC](irc.md) and
|
|
[News](news.md) networks, it's **free for every registered member**, and the
|
|
public surface is open to anyone with a gopher client (lynx, Lagrange,
|
|
Bombadillo, Gophie).
|
|
|
|
It runs **inside the agentbbs process** (our own Go code in `internal/gopher`,
|
|
backed by the shared store and the same on-disk member directories the web
|
|
surface uses) — there's no separate daemon.
|
|
|
|
## Why two surfaces (gopher *and* hedgehog)
|
|
|
|
Classic Gopher is a **stateless, unauthenticated, one-shot** TCP protocol: the
|
|
client opens a connection to port 70, sends a single selector line, reads one
|
|
response, and the connection closes. Crucially, **there is no auth verb** — no
|
|
NNTP-style `AUTHINFO`, no session. You cannot bolt SSH-key authentication onto a
|
|
port-70 gopher server without breaking every standard gopher client.
|
|
|
|
So AgentBBS does what it does for every other protocol — a **native port for the
|
|
world, an `ssh` front door for members** — but splits the content by what the
|
|
protocol can prove:
|
|
|
|
| Surface | Transport | Auth | Sees |
|
|
|---|---|---|---|
|
|
| **Gopher** | TCP `:70`, RFC 1436 | none (public) | public content only |
|
|
| **hedgehog** | the SSH channel (`ssh gopher@`) | your registered SSH key | public **+ members-only** selectors |
|
|
|
|
**hedgehog** is the same gopher wire semantics (item-type menus, selectors)
|
|
carried over the authenticated SSH session instead of port 70. It's gopher where
|
|
gopher can, and our own gopher-like thing where it can't. Both surfaces share one
|
|
resolver (`internal/gopher/server.go`); the only difference is an `authed` flag.
|
|
|
|
The engine is **read-only** — nothing is ever written to the BBS over gopher.
|
|
|
|
## Connect
|
|
|
|
| Path | Address | For |
|
|
|---|---|---|
|
|
| Public gopher | `gopher://gopher.profullstack.com` | anyone, any gopher client |
|
|
| hedgehog | `ssh gopher@bbs.profullstack.com` | members — zero-setup built-in browser |
|
|
|
|
```bash
|
|
lynx gopher://gopher.profullstack.com # or Lagrange, Bombadillo, Gophie
|
|
ssh gopher@bbs.profullstack.com # members: the hedgehog browser TUI
|
|
```
|
|
|
|
### `ssh gopher@` — the built-in browser
|
|
|
|
Drops a member straight into a terminal gopher browser (Bubble Tea, no client to
|
|
install). Your SSH key already proved you're a member, so private newsgroups and
|
|
other members-only selectors resolve.
|
|
|
|
- `↑`/`↓` (`j`/`k`) move · `enter` (`→`/`l`) open a link
|
|
- `backspace` (`←`/`h`/`esc`) go back · `q` quit
|
|
- in a text document: `↑`/`↓` scroll, `space` page down, `g` top
|
|
|
|
Non-members are refused with a pointer to `ssh join@` and the public gopher URL.
|
|
|
|
## Menu map
|
|
|
|
Both surfaces expose the same selector tree; hedgehog additionally resolves the
|
|
members-only parts.
|
|
|
|
```
|
|
/ root menu
|
|
/about brand + MOTD + how to connect (text)
|
|
/members directory → each member's homepage
|
|
/~<name>[/path] a member's homepage (their public_html — the same
|
|
pages served at https://bbs.profullstack.com/~name)
|
|
/news newsgroups (public groups only unless authenticated)
|
|
/news/<group> article list (newest first)
|
|
/news/<group>/<num> one article (text)
|
|
/files directory → each member's public files area
|
|
/files/~<name>[/path] a member's public files (their /public SFTP area)
|
|
```
|
|
|
|
Directories render as gopher menus; files are served by type (text vs binary vs
|
|
image). Path selectors are confined to the member's own area — any `..` that
|
|
would escape is neutralised and the resolved target is re-checked against the
|
|
base directory before anything is opened.
|
|
|
|
### News: public vs members-only
|
|
|
|
The news network is otherwise members-only (see [news.md](news.md)), so on the
|
|
**public** `:70` surface only an allowlist of groups is reachable
|
|
(`AGENTBBS_GOPHER_NEWS_GROUPS`, default `pfs.announce`). Every group is visible
|
|
on **hedgehog**, where you're authenticated. A public request for a non-allowed
|
|
group is refused with a hint to `ssh gopher@`.
|
|
|
|
## Configuration (env)
|
|
|
|
| Var | Default | Meaning |
|
|
|---|---|---|
|
|
| `AGENTBBS_GOPHER` | `1` | enable the gopher service (`0` disables both surfaces) |
|
|
| `AGENTBBS_GOPHER_ADDR` | `:70` | public RFC-1436 listener address |
|
|
| `AGENTBBS_GOPHER_HOST` | `gopher.<host w/o "bbs.">` | hostname advertised in menu selector rows |
|
|
| `AGENTBBS_GOPHER_NEWS_GROUPS` | `pfs.announce` | groups exposed on the public surface (all groups show on hedgehog) |
|
|
|
|
## Deploy note — binding port 70
|
|
|
|
`:70` is privileged (< 1024), just like the news `:563` NNTPS port, and **gopher
|
|
is not HTTP**, so Caddy's reverse proxy can't front it. Grant the binary the bind
|
|
capability:
|
|
|
|
```bash
|
|
sudo setcap 'cap_net_bind_service=+ep' /path/to/agentbbs
|
|
```
|
|
|
|
Or run the listener on a high port and redirect with the firewall:
|
|
|
|
```bash
|
|
AGENTBBS_GOPHER_ADDR=:7070 ./agentbbs
|
|
sudo iptables -t nat -A PREROUTING -p tcp --dport 70 -j REDIRECT --to-port 7070
|
|
```
|
|
|
|
hedgehog (`ssh gopher@`) needs none of this — it rides the existing SSH port.
|
|
|
|
## Why in-process
|
|
|
|
Same reasoning as [news](news.md): a full separate gopher daemon plus a
|
|
content-export pipeline would duplicate the member directory, homepage storage,
|
|
and store the BBS already owns. Resolving selectors directly against the live
|
|
store and the on-disk `public_html`/`public` areas keeps the two surfaces always
|
|
in sync with the rest of the BBS, for a protocol whose entire spec fits on a
|
|
few pages.
|