gopher

package
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 1, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package gopher serves AgentBBS content over the Gopher protocol (RFC 1436) and its SSH-authenticated sibling, "hedgehog".

Two surfaces, one engine

Every AgentBBS protocol service exposes a native port for external clients plus a zero-install `ssh <name>@` TUI for members (see internal/news). Gopher follows the same shape — but with a twist forced by the protocol itself:

  • Classic Gopher (RFC 1436) is a stateless, unauthenticated, one-shot TCP protocol on port 70: the client sends a single selector line and reads one response. There is no auth verb (no NNTP-style AUTHINFO), so SSH-key auth cannot be bolted onto a port-70 gopher server without breaking every gopher client. The public listener (Serve) therefore serves *public* content only.

  • "hedgehog" is the same gopher wire semantics carried over the SSH channel (RunBrowser), with the member's SSH key as the credential. Because the session is already authenticated, hedgehog additionally resolves members-only selectors (e.g. private newsgroups). It is the home-grown, gopher-like, SSH-authenticated surface — gopher where gopher can, our own thing where it can't.

Both surfaces share one Resolve engine (server.go); the only difference is the authed flag passed in. The engine is read-only: nothing is ever written to the BBS over gopher.

Index

Constants

View Source
const (
	TypeText   = '0' // a plain text file
	TypeMenu   = '1' // a Gopher directory (submenu)
	TypeError  = '3' // an error / "does not exist" line
	TypeBinary = '9' // a generic binary file
	TypeHTML   = 'h' // an HTML file / URL: link
	TypeImage  = 'I' // an image
	TypeInfo   = 'i' // an informational line (not selectable)
)

Gopher item types (RFC 1436 §3.8). Only the handful the BBS emits are named.

Variables

This section is empty.

Functions

func RunBrowser

func RunBrowser(s ssh.Session, srv *Server, member string) error

RunBrowser drives the hedgehog gopher browser over the SSH session: an in-process gopher client that resolves selectors with authed=true (so the member sees members-only content their SSH key entitles them to). This is the authenticated counterpart to the public RFC-1436 listener — same wire semantics, carried over SSH instead of port 70. member is the connecting member's name. Requires a PTY.

Types

type Content

type Content interface {
	ListUsers(limit int) ([]store.User, error)
	NewsGroups() ([]store.NewsGroup, error)
	NewsArticlesRange(group string, from, to int64) ([]store.NewsArticle, error)
	NewsArticleByNum(group string, num int64) (store.NewsArticle, bool, error)
}

Content is the read-only slice of the store the gopher engine needs. It is satisfied directly by store.Store, so no new store methods are required; tests supply a small fake.

type Item

type Item struct {
	Type     byte
	Display  string
	Selector string
	Host     string
	Port     string
}

An Item is one gopher directory row. Info rows (TypeInfo) are display-only; every other type is a selectable link the client may follow to Selector on Host:Port. Keeping menus as structured Items (rather than pre-rendered text) lets the hedgehog browser TUI navigate them while the native listener renders the exact same rows to the wire.

type Kind

type Kind int

Kind classifies a resolved response.

const (
	KindMenu   Kind = iota // a gopher directory (Menu set)
	KindText               // a text document (Text set)
	KindBinary             // a binary file (Data set)
	KindError              // selector unknown/refused (Text set to the message)
)
type Menu struct {
	Items []Item
	// contains filtered or unexported fields
}

A Menu accumulates gopher directory rows and renders them in the wire format: each row is "<type><display>\t<selector>\t<host>\t<port>\r\n" and the whole menu is terminated by a lone "." line (the RFC 1436 last-line dot).

func NewMenu

func NewMenu(host, port string) *Menu

NewMenu starts a menu whose links advertise host:port as the server to dial for each selector (mirrored back to clients so link-following works).

func (m *Menu) Info(text string) *Menu

Info appends one or more informational lines (type 'i'), split on newlines so callers can pass a multi-line block.

func (m *Menu) Len() int

Len reports how many rows have been added (informational + selectable).

func (m *Menu) Link(itemType byte, display, selector string) *Menu

Link appends a selectable row of the given item type pointing at selector on this server.

func (m *Menu) Render() string

Render returns the full menu including the terminating "." line, CRLF-framed.

func (m *Menu) URL(display, url string) *Menu

URL appends an "URL:" link (type 'h') to an external http(s) address — the widely-supported convention for linking off-gopher.

type Response

type Response struct {
	Kind Kind
	Menu *Menu  // KindMenu
	Text string // KindText, KindError
	Data []byte // KindBinary
}

Response is the result of resolving a selector. Wire() renders it to the exact bytes the native listener sends; the TUI reads Kind/Menu/Text directly.

func (Response) Wire

func (r Response) Wire() []byte

Wire renders the response to the gopher wire format: menus and text end with the RFC 1436 "." terminator (text is dot-stuffed); binaries are sent raw.

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server resolves gopher selectors into responses over BBS content. It is shared by the public listener (Serve) and the hedgehog SSH browser (RunBrowser); the authed flag on Resolve is the only behavioural difference between them.

func New

func New(c Content, dataDir, host, port string, pubGroups []string, aboutFn func() string) *Server

New builds a Server. host/port are echoed into menu rows so gopher clients can follow links. pubGroups is the allowlist of newsgroups visible without auth (the authenticated hedgehog surface sees them all). aboutFn supplies the /about page body and may be nil.

func (*Server) Resolve

func (s *Server) Resolve(selector string, authed bool, member string) Response

Resolve maps a gopher selector to a response. authed is true only on the hedgehog (SSH-authenticated) surface, where member is the connecting member's name; the public listener always passes authed=false. The engine never writes.

func (*Server) Serve

func (s *Server) Serve(ctx context.Context, addr string) error

Serve runs the public Gopher listener (RFC 1436) on addr until ctx is cancelled. Every connection is a single transaction: read one CRLF-terminated selector, resolve it with authed=false (public content only), write the response, close. It is read-only — nothing is ever written to the BBS here.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL