Neo6502 › Articles › Prophet
Building a Prophet server for the Neo6502
Prophet is the Neo6502 community's program repository: a client on the board, a server somewhere. We rewrote the server in Go, security first, and put prophet.3617.fr online — with our own programs in it.
On the Neo6502, programs are installed from the console: prophet.neo, the community client written in Mad Pascal by bocianu, talks to a server through a WiFi modem (AT commands) and downloads the file in chunks. The upstream server, prophet-server, is written in Python/Flask and published without a licence file; the current client cannot do TLS, so the public server runs over HTTP. We wanted a server of our own — to publish AsteroNeo, to learn the protocol before writing our own client, and to host it ourselves with our own operating rules. Neo6502Prophet is an independent Go reimplementation, protocol-compatible, without a single copied line. Four sprints in two days; here is what we did.
1. Record the protocol before writing a line
First step: read the client, not the server. prophet.pas sends GET with a ResponseFormat: cli header and parses raw text, \n\r line endings included: /cat for categories, paginated /list/<cat>, /search, /app/<id> for the sheet, /dirs and /files for the tree, then /files/<id>/<n> in Range chunks with 206 responses. Two control bytes (\x83, \x82) bracket the id in listings. /files/prophet/1 serves the client's own update. The data repository is a tree of category/package/desc.yaml folders, older versions under _N/. All of it is written down in docs/ANALYSE-AMONT.md — with thanks to bocianu, whose client, server and data repository make all of this possible — along with what is not verified: the public server may differ from the published code.
2. Our security choices
A program repository is a public HTTP service distributing files that boards will execute. Before writing, we listed the classic risks of such a service and decided to answer each one: never a debug mode, local listening by default; every action — refreshing the repository, which runs a git pull — authenticated; validated and bounded parameters, errors without tracebacks; served files confined to their folder, links ignored; filtered ids; a log with rotation; no state shared between requests; catalogue reloaded in memory without an intermediate file; a fingerprint for every file served. Nine points, written down in SECURITY.md and tested.
3. Ours: Go, read-only, security first
Standard library and yaml.v3, one static binary. No write API; /refresh requires a token of at least 16 characters compared in constant time, honours a minimum delay, runs git pull --ff-only without a shell and with a timeout — without a token the endpoint does not exist (404). Ids filtered by regular expression, bounded integers, whitelisted sort fields, limited search key, bounded request body; any invalid input yields a text 400 or 404, never a traceback. File paths are recomputed from the root and checked: inside the folder, regular file, not a link. The catalogue is scanned in memory and swapped atomically; every file exposes its SHA-256 in the JSON. The server listens on 127.0.0.1 by default.
The client's text format is reproduced byte for byte and tested as such. One addition: the Neo's console only displays ASCII, so titles and descriptions are transliterated for the client while the JSON stays UTF-8. And a dual listener: HTTP on 8998 for today's clients, native HTTPS (TLS ≥ 1.2) for the modems that will be able to — the Pico W of PicoWiFiModemUSB, the "picowifitls" story on the Neo6502drive side.
4. Do not serve just anything
A program repository distributes binaries people will run. Two safeguards. Static: neocheck reads every .neo (header, blocks, bounds, kernel area); an invalid file is not served, a "raw" file is flagged (neo = ok/raw/invalid in the JSON). Dynamic: neotrace replays every program in Phosphoneo with the API call trace (--api-log) and the effective address of every write, and reconstructs what the program did to the kernel. Replay of 16 September: 56 programs out of 56 (46 .neo, 10 .bas) run, no unresolved write, no malicious behaviour observed; one note, zc.neo writes into the kernel area (table based at $FC20) with no visible effect. The audit also found a Phosphoneo bug — a $FFFF graphics block ignored at load — since fixed.
5. A web interface, without breaking the client
Served at / to browsers only (Accept: text/html without ResponseFormat: cli): catalogue grid with cover, search and filter, sheet with description, version, files, size, .neo status, SHA-256 and download. Embedded HTML, CSS and JS, no dependency, strict CSP. Covers are screenshots taken in Phosphoneo by a script — 42 packages out of 42. A dedicated test checks that the text client receives exactly the same response as before.
6. In production: prophet.3617.fr
A dedicated unprivileged Debian 13 container (1 core, 512 MB), hardened systemd service (systemd-analyze security exposure 1.3), nftables accepting port 8998 from the reverse proxy only, automatic security updates. Caddy terminates TLS (https://prophet.3617.fr) and also relays plain HTTP on 8998 for ESP8266 clients, with Crowdsec; an hourly timer refreshes the mirror of the community repository through the local token. Several data folders are served side by side: bocianu's mirror and our own repository, where AsteroNeo 0.2.0 is the first published package (/app/asteroneo, cover included). As of 16 September: 37 games, 5 tools, 1 other.
7. What we do not claim
The server has never been queried from a real Neo6502 board: we do not have one, and the end-to-end test through the Pico W modem is waiting for Neo6502drive's TLS story. The upstream client does not check the SHA-256 we expose — only a modified client will. The dynamic audit says what the programs did in the emulator, not what they would do on a board with other inputs. The server's code has no public repository yet. Next is a native graphical client ("B") for the Neo6502, with pre-converted covers, TLS and fingerprint checking.