The gap between a protocol sketch and a useful client.
The earlier interface could illustrate a Gopher menu, but it could not prove the important parts: how a selector becomes bytes on a socket, how text framing ends or what happens when a remote server stalls, lies about its type or returns binary data.
Version 3.2.0 keeps the bounded Node.js path for the CLI and gateway, then adds a native Android transport for direct mobile use. The browser edition is installable and offline-safe, but its public Pages build stays fixture-only because browser JavaScript cannot create the raw TCP socket Gopher requires.
Rules the implementation cannot blur.
The protocol is simple; the trust boundary is not:
- RFC 4266 address parsing must preserve selector and search-query semantics, while RFC 1436 framing, text terminators, dot-stuffing and binary types keep distinct wire and output rules.
- Every network operation needs a total deadline, idle timeout, request cap, response cap and menu-entry ceiling.
- Hosted fetches must fail closed on private, loopback or mixed public/private DNS answers and must connect to the validated address rather than resolving again.
- Untrusted text cannot control the terminal, and binary responses cannot be decoded or printed as if they were text.
A local gateway, not an open proxy.
Putting an internet-wide HTTP proxy behind the explorer would make the page convenient and create an SSRF service at the same time. DIG instead keeps the gateway beside the user, accepts API calls only from its own browser origin and emits no CORS access for other sites.
Hosted mode is explicit rather than inferred. It requires a token, blocks private destinations and pins the validated DNS result into the TCP connection. Local private access exists only behind an explicit flag and a visible warning.
Two bounded transports, four honest surfaces.
The CLI and local explorer share the Node.js destination policy, pinned TCP client and parser. Android applies the same public-destination and response limits in its native transport, then hands typed bytes to the packaged explorer. The PWA and Pages presentation stop at a verified offline fixture unless an authenticated gateway is available.
Why the live protocol boundary sits outside the browser.
DIG keeps transport, destination policy and representation explicit instead of hiding Gopher behind a generic web request.
A Node.js TCP client and gateway perform live Gopher requests.
- Why
- Node can write the selector and CRLF to a raw socket, enforce byte and time limits and reuse the same parser and policy as the CLI.
- What I ruled out
- Browser JavaScript cannot open raw TCP sockets, while a generic forwarding proxy would hide Gopher-specific selectors, item types and response limits behind an unrelated abstraction.
- What it cost
- Live browser exploration needs a local process and a second HTTP boundary; the public static site can offer only verified fixtures.
The gateway is same-origin, bounded and destination-aware.
- Why
- DNS validation, IP pinning, response limits and the absence of CORS keep each request inside the documented local or authenticated hosted policy.
- What I ruled out
- An anonymous public proxy would be simpler to visit, but it would create an abuse and server-side request-forgery surface for arbitrary destinations.
- What it cost
- Operators must configure authentication and network policy, and private destinations require an explicit local override rather than working by default.
Transport paths preserve response bytes before interpretation.
- Why
- Gopher serves menus, text and binary item types. Exact bytes, byte counts and SHA-256 digests let the CLI save binary data without corrupting it and make parser decisions inspectable.
- What I ruled out
- Decoding every response as text would be convenient for display, but it would damage binary payloads and conceal invalid or unsupported encodings.
- What it cost
- The implementation must branch deliberately by item type, keep text encoding support narrow and carry additional metadata through each surface.
Android uses a native Capacitor transport, not a remote web wrapper.
- Why
- The packaged explorer can make direct, bounded TCP requests on mobile while keeping the interface assets inside the APK. The native layer applies public-destination checks and has no private-network override.
- What I ruled out
- Embedding the hosted site would still depend on an HTTP gateway and connectivity, while a browser PWA cannot gain raw-socket access simply by being installed.
- What it cost
- The project owns an Android/Kotlin boundary, SDK and signing workflow, and parity tests between the Node.js and native policies.
Decisions that make the boundary visible.
Each surface says clearly what it can reach and what it stores.
Resolve once, then connect to what was checked
The hosted policy rejects the whole hostname if any DNS answer is non-public. A successful lookup returns the exact address used by the TCP client, closing the usual validation-to-connection gap.
The trade-offStrict mixed-answer rejection can block unusual but legitimate DNS setups; it is safer than guessing which answer an attacker intended.
Label fixture-only mode honestly
The public Pages explorer states that it is backed only by deterministic fixtures. Live browsing is offered only by the local or deliberately hosted gateway, so the interface never implies that a static browser opened a Gopher connection.
The trade-offVisitors cannot point the public demo at arbitrary servers, but they can inspect the interface without turning Pages into a misleading proxy.
Commit downloads only when complete
The CLI stages output and publishes the requested path only after the bounded fetch succeeds. On failure it removes the temporary artifact instead of leaving a plausible but partial file.
The trade-offAtomic output needs staging, cleanup and sufficient local space, but callers never have to guess whether a visible target is complete.
How v3.2.0 is checked.
The 102-test Node.js suite covers RFC parsing, TCP fixtures, network policy, the HTTP contract, atomic CLI output, PWA state, Android bundle rules and release contracts. Fifteen passing Playwright flows cover Chromium and mobile WebKit, including live navigation and offline recovery.
Android CI builds and lints the native project, runs its unit tests and verifies the packaged assets. Release validation also checks deterministic archives, dependency audit, container smoke behaviour and the signed APK contract.
What works today.
From the terminal or Android app, a user can fetch real menus, text, searches and common binary types. The local browser explorer adds history, bookmarks, search forms, raw inspection, JSON export and downloads through its same-origin gateway; the installed PWA keeps the fixture usable offline.
DIG does not turn Gopher into HTTP. Traffic to a Gopher server remains cleartext, Pages does not fetch live resources, and Gopher+, TLS, Telnet sessions and recursive crawling stay outside the supported contract.
Evidence ledger.
The v3.2.0 claims are tied to executable checks and visible boundaries:
- Verification
- The checked source passes 102 Node.js tests and 15 browser flows across Chromium and mobile WebKit, with one intentionally platform-specific skip.
- Android
- The Capacitor 8 application supports Android 7/API 24 and newer, targets API 36 and uses a native direct-TCP transport rather than loading the hosted site.
- Network policy
- Hosted mode requires an access token, rejects a hostname when any DNS answer is non-public and connects only to the address it already validated.
- Output integrity
- The CLI writes through a same-directory temporary file and exposes the final path atomically; binary bytes are never printed to an interactive terminal.
- Offline boundary
- The installable PWA caches the static shell and verified fixture, never API responses. Returning online resumes a previously live gateway session without redirecting fixture-only visitors into network calls.
Verified release v3.2.0 Verified on
This case study describes the checked v3.2.0 implementation: RFC 1436 menu and text framing, RFC 4266 URLs and search, common binary items, bounded Node.js and Android TCP transports, a same-origin gateway and an offline-safe fixture PWA. UTF-8 is the supported URL-field encoding; Gopher traffic remains unauthenticated and unencrypted.