Skip to content

Development

This page is for people working on the BBS's code: how the source is laid out, how the screens are shared between terminals, and what the tests cover.

Layout

Path What it holds
cmd/bbs the command: serve, check, init, user and area administration
internal/server listeners, session headers, rlogin, SSH, telling 3270s from the rest
internal/telnet the server side of telnet: options, terminal type, window size, the handoff to go3270
internal/confedit bbs.conf edited as a sysop would by hand, comments kept; the catalog of keys read from bbs.conf.sample (embedded by the root package), and which take effect at a reload
internal/sockets ports bound as root and handed down: the BBS becomes its user, the same process
internal/backup backups: the configuration, a database snapshot and the files in one tar.gz; listing, pruning, restoring
internal/confui the configuration menus (sheets of internal/view), for bbs config and the sysop's tools: changes held until saved
internal/view what the screens show, described once: menus, tables, messages, forms, panels, drafts
internal/script the sysop's JavaScript (goja): screens of internal/view, the BBS as the caller, data kept, the time a script may compute
internal/guard deny lists, bans, the name filter, the hack log
internal/geoip country databases: MaxMind DB files and CSVs of ranges, read itself; a file kept up to date
internal/phone caller IDs, by libphonenumber's numbering plans and places (and NANPA's area codes (generated by internal/phone/gen: make phone-data)
internal/events the sysop's scheduled events
internal/app the screens after the login, for every front end: menus, areas, mail, files, who's online, the profile, validation, paging the sysop
internal/textui the text front end: the login, line editing, ANSI detection, paging, RIP menus; the screens as prompts and keys; chat, doors, transfers
internal/ui3270 the 3270 front end: the logon, the screens as fields and PF keys on go3270; chat, IND$FILE, its graphics
internal/sixel the sixel encoder
internal/petscii the Commodore's character set: translating ANSI and UTF-8 out, keys in
internal/rip RIPscrip: MegaNums, commands, menu scenes
internal/vt terminal identification (DA1, DA2, XTVERSION, CTerm, answerback, VT52); DEC graphics and VT52 output
internal/files file areas' storage: names, uploads, limits
internal/ftp the FTP server: the file areas as a file system
internal/news articles: parsing, checking, making, decoding; wildmats
internal/nntp the NNTP server and the peers' client
internal/ircd the IRC server: channels as chat rooms; ergo's irc-go, numerics and ISUPPORT list
internal/mrc Multi Relay Chat: the BBS as its own connector, MRC's rooms as chat rooms
internal/qwknet a QWK network's node: calling the hub by FTP, REPs up, packets down
internal/qwk QWK packets and REPs: MESSAGES.DAT's blocks, CONTROL.DAT, indexes (MBF floats), QWKE kludges, CP437
internal/ftn FidoNet's formats: addresses, Type 2+ packets, messages, kludges, character sets
internal/fido the BBS on FidoNet: tosser, scanner, netmail, the binkp mailer
internal/door doors: drop files, running a door on the caller's line
doors/dosemu2 the Docker image DOS doors run in: dosemu2, its FOSSIL on COM1
internal/finger the finger server
internal/gopher the gopher server
internal/gemini the Gemini server: gopher's capsule and finger's users, over TLS
internal/forum the web forum: its pages (html/template; themes/NAME/ a theme's own, over its parent's and templates/), the themes' CSS (static/), sessions by the API's tokens
internal/api the HTTP API: REST, server-sent events, WebSockets, metrics, OpenAPI; the web terminal page (web/: xterm.js, the page)
proto the WebSocket protocol, bbs.v1 (make proto regenerates internal/api/pb)
internal/tcpserv what finger, gopher and Gemini share: limits, timeouts
internal/screens the sysop's display files: finding them, @-codes, 3270 colour tags
internal/xfer XMODEM, YMODEM, ZMODEM and Kermit, both ways
internal/logo the logo: a PNG, or the name in a pixel font
internal/bbs the BBS itself: nodes, logins, new users, posting, cleaning text
internal/store the database: SQLite, PostgreSQL, MySQL or MariaDB (dialect.go what differs, schema.go the tables), copying between them
internal/config the configuration file

Screens

The screens after the login are written once, in internal/app, and each front end draws them. A screen is a description in internal/view's terms. It is handed to the front end, which shows it, waits for the caller, and reports what they chose:

View Text terminal 3270
Menu [K] Label in two columns (one under 60 columns), RIP buttons, the command prompt a box of keys, Option ===>
Table numbered rows a page at a time; Area number, [N]ew scan, [Q]uit: S by a row; PF keys; PF7/PF8
Message headers, the body paged; [N]ext, [R]eply, ... headers, the body scrolled; PF10/PF11, PF4 ...
Form a question a field one screen of fields
Panel lines, then [L]ocation, [P]assword, ... lines, PF keys
Draft To, Subject, the full-screen editor (ANSI) or the line editor the editor screen
Confirm, Say, Show [Y/n], a line, lines and a pause a Y/N field, the message line, a page

An action has a key for text terminals and a PF key for 3270s (plus a short label for the 3270's key line). Whatever only a front end can do is in its app.Frontend: the sysop's display files, chat, doors (text only), file transfers (protocols on text, IND$FILE on a 3270), the terminal line in the profile and the settings that can be switched there, and the goodbye. Logins stay with the front ends, because they decide what the terminal is.

To add a screen, write a function in internal/app that builds views. To add a front end, implement app.Frontend. internal/app's tests drive the screens with a scripted front end, with no terminal at all.

Tests

With BBS_TEST_DATABASE set to a PostgreSQL, MySQL or MariaDB server, each test gets a database of its own there (a schema on PostgreSQL, a database on MySQL, dropped afterwards), so the whole suite runs on that engine:

BBS_TEST_DATABASE='postgres://bbs:PASSWORD@127.0.0.1/bbs?sslmode=disable' go test ./...
BBS_TEST_DATABASE='mysql://bbs:PASSWORD@127.0.0.1:3306/bbs' go test ./...
BBS_TEST_DATABASE='mariadb://bbs:PASSWORD@127.0.0.1:3306/bbs' go test ./...

The user must be allowed to CREATE SCHEMA (or CREATE DATABASE). The store's own tests check the things the engines do differently: names in any case, ids, upserts, booleans, bytes, long text, cascades, and copying from SQLite.

What the tests cover

make test runs the unit tests (text cleaning, names and passwords, the configuration, telnet negotiation and the 3270 handoff) and whole sessions against a running server on unix sockets: a modem caller with a trusted modemmenu header, one whose header is not trusted, a login and a post, a new user, rlogin with a right and a wrong password and trusted, throttling by caller ID, all lines busy, a telnet ANSI client, a TN3270 client taken through go3270's negotiation to the logon screen and out with PF3, the transfer protocols against lrzsz (sx/rx, sb/rb, sz/rz) and C-Kermit both ways, and ZMODEM over a line that damages bytes; uploads and downloads through the BBS by ZMODEM, Kermit, YMODEM and XMODEM-1K; IND$FILE with a real s3270 (upload, download, a file that is not there); the file store (names, limits, the daily allowance, sysop-only areas), the FTP server with a real FTP client (uploads, downloads and their count, every refused operation, path traversal, wrong logins, anonymous access, TLS required and explicit TLS, the size limit, the per-address limit), telnet terminals with and without sixel, the sixel encoder checked by decoding its output with ImageMagick (when installed), and TN3270 clients that report programmed symbols (the logo is loaded and shown), vector graphics (the logo is drawn) or neither (nothing is sent), articles (parsing, limits, decoding, wildmats), the NNTP server with Python's nntplib as a newsreader (login, reading, overview, posting), its refusals (posting without a login, AUTHINFO before STARTTLS, forged headers, control messages, oversized articles, IHAVE from a stranger), two BBSes peered both ways, and IHAVE. With BBS_TEST_INN=host:port it also syncs with a running INN. FidoNet: packets and messages, a hub with two nodes exchanging echomail by binkp (SEEN-BY, PATH, threads, forwarding), netmail and replies, wrong passwords, unknown addresses and impostors, dupes, loops, bad and foreign packets, the echo-newsgroup gateway, and, when binkd and CrashMail are installed, a hub run by them: echomail and netmail both ways, calling it and called by it. Doors: the drop files, the runner (CP437 and UTF-8, time limit, hanging up, idle, single-caller doors, a pseudo-terminal), a caller opening a door from the menu, and, when the bbs-dosemu2 image is built, a real DOS door talking by FOSSIL in it. Chat: rooms, history, private rooms, flooding, node messages, paging and answering, the page command (and that it runs no shell); two callers chatting and paging on the text BBS; a 3270 (s3270) chatting with a text caller and getting a node message on its message line. Finger, gopher and Gemini: their answers and refusals, with the finger program and curl as clients, and bbs gemini cert asking before it makes a certificate. Customization: finding display files by terminal, SAUCE, @-codes and widths, colour tags; menus and strings in the configuration and what it refuses; a text caller through an ANSI logon (CP437 as it is), the strings, a welcome screen, the sysop's menus and submenus, a screen with a pause and a goodbye screen; and a 3270 (s3270) through a coloured logon screen, welcome, the sysop's main menu and a screen. The shared screens: internal/app driven by a scripted front end (posting, reading on and back, a new scan and reply, a number not on the page, mail to no one and to the sysop, a node message, deleting, the profile, a password that differs, validating with a bad level first); the column layout; the text prompts' keys; and a 3270 (s3270) through the areas, a post from the editor, reading it, the profile, with a text caller reading the 3270's post. The full-screen editor: its text (wrapping as typed and as given, a word too long for a line, joining lines by Backspace and Delete, moving over line ends, quoting, the line limit), and an SSH caller writing a post with the cursor keys, replying with lines from the quote window and /S, then choosing the line editor; the message base (threads from any message, votes, polls and their answers, aliases (taken names refused), signatures, groups, keeping by count and by age; through internal/app and the API); the sysop's tools (a sysop watching a caller's screen as it is sent, calling them into chat at their next menu, being away when they page; the user editor, statistics, bans, the hack log and events through internal/app; events run, failing, timed out and not run twice; their configuration and the API); the last callers, feedback to the sysop, a user's details (more of them to a sysop) and mail from there, a line about oneself, expert menus and ? on a text terminal; keeping callers out (the deny file read again as it changes, networks and IPv6, bans after the throttle and their end, name patterns, the hack log's lines; telnet over TLS with a login, a plain client on the TLS port getting nothing, a denied address closed at once, the API's bans and log); scripts (screens, data kept for all and for a user, posting with refusals caught, a runaway loop stopped and a slow caller not, failures told to the sysop alone, a hang-up the script cannot catch, exit, the sample poll, the configuration's checks, and the sample one-liners on a text terminal and on a 3270 by s3270); the file base (FILE_ID.DIZ in CP437 with colours, a ZIP's and a tar.gz's listings, new files, finding by the DIZ's words, tagging by numbers, a batch of two by YMODEM, ratios and the day's limit together, the API's search and listings); MRC (a fake MRC server: the handshake, arrivals, NEWROOM, lines and actions both ways, colours taken out, our own lines not shown twice, private messages and server answers to one user, PING and IAMHERE, !commands and !!, a user in two rooms, LOGOFF, the line dropped and those here put back, rooms not MRC's not sent, require; on IRC, an MRC user's hostmask and the room's notices); doors elsewhere (a telnet server's negotiation answered and kept from the caller, doubled IACs; RLogin's start and a refusal; BBSLink's token, its MD5s and a wrong code; DoorParty through an SSH server's tunnel, a wrong password and a wrong host key refused; a caller on the BBS going through one and back; their configuration); the IRC server (logins by PASS and SASL PLAIN, and refused; a nick made of a name with a space; lines both ways between an IRC client and a telnet caller, /me, node messages, the room's history with server-time, WHO, WHOIS, LIST, a page answered by joining its room, parting, the node gone with QUIT, TLS, bad nicks; irc-go's own client with SASL and echo-message; BBSServ (help, info, each of the settings and their refusals, the new scan, SSH keys added with the password and deleted, tokens revoked, the password, by /msg and by /BS, CTCP, WHOIS, notices unanswered); permanent rooms (open when empty, their topics shown on coming in and told when set, by /topic, TOPIC and BBSServ, refused to those not the sysop, kept across a restart, let go; @BBSServ in NAMES, WHO, WHOIS and LIST, on every database engine); the configuration's checks); QWK offline mail (packets and REPs read back as a reader would, long subjects as QWKE kludges, CP437, another BBS's REP refused, the indexes' MBF floats; a packet's messages read only once it has gone, the REP's replies posted and mailed as the caller, a REP twice posting once; by ZMODEM on a text line, by IND$FILE from s3270, by FTP and the API); QWK networks (a hub serving FTP and two nodes: what was there before joining kept back, posts going up under their writers' names, a node's own not coming back, a reply threaded at the hub, nothing twice; the configuration's checks); the new scan's areas, messages to you and finding (the scan kept, LIKE's wildcards taken as text, areas not read left out), through internal/app and in the store on every engine; settings kept from call to call (UTF-8 off over SSH, a Commodore's width) and through the API. The HTTP API: logins, tokens and scopes, every REST endpoint and its refusals, rate limits, the OpenAPI document against the routes, server-sent events and node messages, both WebSockets (auth by header and by first message, chat between them, paging and answering, mail events, bad frames, refused origins and tokens) and the metrics. And a terminal caller and a web caller (the JSON WebSocket) together: chatting in one room both ways, /who, node messages each way, and the web caller paging the sysop, who answers at the terminal. SSH: logging in by password, keyboard-interactive and key (and not by a wrong one, someone else's, or into a locked account), new to the BBS's prompt, the pty's terminal type and size, refused commands, SFTP and forwarding, a user adding and deleting a key in their profile, host keys made and kept (ed25519, RSA, ECDSA) and refused when readable, the configuration, legacy ciphers only when asked for, and the OpenSSH client logging in by key with a pty. PETSCII: the translation (letters, colours, reverse, CR and LF, cursor movement, box drawing, accents, runes split across writes, wrapping) and the keys back; a Commodore logging in with DEL editing, its one column menus, a .seq screen as it is, the profile's width, no escape ever sent; a Commodore found while logging in: by DEL typed in the name, by a shifted letter (and not by UTF-8), by a name in capitals and the DEL question (or not, with a backspace), and no question for a name in lower case. RIP: MegaNums, the commands and their escaping, the menu scene; a RIP terminal found by its answer, given a .rip screen, menus as buttons, the scene cleared on a choice and a pause before the next; an ANSI terminal without RIP given none of it. Access levels: requirements read and written back, met and not (flags, !flags, sysop, locked users), flag edits; levels found for a user; areas hidden and posting refused by level and by flags, mail only to the sysop, chat and paging refused, the day's downloads counted and limited, doors, validation; the day's time running out (warned, hung up, counted); a new user waiting and validated by the sysop on the BBS; the API (no user raises their own level, levels and flags seen only by sysops and the user, area requirements); the configuration. Terminal identification: the answers of DEC's terminals and of emulators (VT100 to VT525, xterm old and new, VTE, tmux, SyncTERM, PuTTY, the Linux console, a VT52) read into names, levels, sixel and monochrome; DEC Special Graphics and Latin-1 output, escapes passing through it; VT52 output; a VT220 given DEC graphics, Latin-1 and bold, shown as a VT220; a VT52 given its escapes and its cursor keys ignored; a terminal answering nothing left plain. The web terminal: the page, its headers and files (and no others), WebSockets refused from another origin or without the subprotocol, and a caller logging in through it, its size taken, shown as a web terminal, logging off. The web forum: guests reading (and not what needs a level), logging in and out by the API's session tokens (and not by an API key), signing up; topics and replies, forms refused without their token, posts' HTML escaped and links made links, long lines wrapped and not cut, quotes, polls, likes, unread topics and areas, aliases hiding the writer, deleting (the writer's own, the sysop's any), news-only areas, search and who's online; in the store, topics threaded across areas, read marks, a topic mended when its first post or its replies' parent goes, and threads found for messages from before. Every page in each theme, for a guest and a member; choosing a theme (a member's form needing its token, an unknown theme refused, no going back off the forum), and the forum's own theme until a caller chooses. Updating xterm.js: npm pack @xterm/xterm @xterm/addon-fit @xterm/addon-image, and copy lib/xterm.js, css/xterm.css, lib/addon-fit.js, lib/addon-image.js and the license into internal/api/web (without their source map lines); the image addon's version must be the one for that xterm.js (0.8 for 5.5).

See also: Building