EN /HU | Login

BBS Web

bbsweb puts our telnet BBSes on the web without changing a line of them. The dialer screen lists the systems, and each one opens in a real terminal in the browser.

Live at bbsweb.teletypegames.org

Overview

Our BBSes speak plain telnet on ports 2323–2327, which needs a telnet client. bbsweb adds a web front end in front of them: a Node server bridges WebSocket to TCP, and the browser runs xterm.js.

It is a generic product, not a Teletype installation. A single config.json describes which systems appear, under what names, in which languages and with what styling — the application itself contains no site-specific text, colour or address. Adding a foreign BBS is one block of JSON.

TXT
browser                 nginx (TLS)              bbsweb container      BBSes
┌────────────┐  wss://  ┌──────────────┐  http   ┌────────────────┐ TCP ┌────────┐
│ xterm.js   ├─────────►│ proxy /ws    ├────────►│ WS↔telnet brdg │────►│ :2323  │
│ 256 colour │          │ proxy /      │         │ + static web   │     │ :2324  │
│ UTF-8, mouse          └──────────────┘         └────────────────┘     │ :2325  │
└────────────┘                                                          │ :2326  │
                                                                        │ :2327  │
                                                                        └────────┘

What it carries

The BBSes send full-screen ANSI with 256 colours, UTF-8 box drawing and SGR mouse reporting. All of that survives the trip:

  • Clickable menus — the mouse reports go through, so the menu bar works exactly as in a terminal.
  • Terminal size — the browser sends NAWS on every resize, so the BBS knows how many columns to draw. Two modes: a fixed 80×25 that scales the font to the window, and a fitted mode that gives the BBS the whole window.
  • English / Hungarian interface, English by default, the choice kept in the browser. Server-side status messages travel as keys, not sentences, so a running session switches language too.
  • Full screen — the icon at the right end of the top bar shows the terminal alone. In Chromium browsers Esc still reaches the BBS (hold it to leave full screen); elsewhere Esc leaves full screen.
  • Touch key bar — ESC / TAB / CTRL / arrows on phones and tablets.
  • File downloads (ZMODEM) — when a BBS sends a file by ZMODEM (the WarpEngine BBS does), the page receives it with zmodem.js and the browser saves it; the status line shows the progress, Esc cancels.

The five systems on the dialer are the boards of our BBS network: Teletype BBS, Impostor BBS, the TTG Wiki BBS, Alexandria BBS and Feedia BBS. Every one of them is still reachable over plain telnet at the address shown on its card, and each has the C64 switch (petscii: true) for its PETSCII face.

Configuration

config/config.json sits next to the compose file (the config folder is mounted into the container) and covers the whole installation:

Group What it controls
site name, tagline, ASCII logo, footer, favicon, accent colour
languages, defaultLanguage the switcher's buttons and the starting language
terminal size, font, theme, cursor, scrollback, starting view mode
server session limits, timeouts, ping, the TTYPE answer
boards the systems, each with its own colour and size

Every key is optional — what is missing falls back to a built-in default. Any text field may be a per-language object: "name": { "en": "…", "hu": "…" }.

The server watches the file's modification time, so an edit needs no restart — reloading the page is enough. If the JSON is broken, the last good configuration stays in effect and the error goes to the log.

The full reference is in the repo's README.

Why it is not an open proxy

The browser only ever sends an id (/ws?id=teletype). The actual host and port never come from the client — they come from config.json, and they do not appear in the public API at all. Without this the bridge would forward to any TCP address on request.

Telnet details worth knowing

The bridge handles IAC negotiation (ECHO, SGA, BINARY, TTYPE, NAWS) and refuses everything else politely. Two things matter in practice:

  • A lone CR is not a line ending. xterm.js sends only CR on Enter, while the BBS waits for a line ending, so in NVT ASCII mode the bridge appends LF the way a real telnet client does. Without it the login screen never advances — and the symptom is deceptive, because the typed name appears on screen normally.
  • nginx drops silent connections. A BBS session stays quiet for minutes while someone reads, and the default proxy_read_timeout of 60s would cut it off. The vhost uses 3600s, and the server pings over WebSocket every 30 seconds so intermediate NATs keep the connection too.

Deployment

A push to master deploys from Woodpecker when it changes compose.yaml, bbsweb/ or config/config.json. The pipeline first checks the scripts (node --check), the JSON files and compose.yaml; the deploy step then only asks the server to deploy over SSH. Its key (secret deploy_ssh_key) is bound on the server to one fixed command, ci-deploy bbs-web (host-config), which clones master from Gitea itself, checks compose.yaml against a policy (no privileged mode, no extra capabilities, no docker socket, bind mounts only inside the stack, no public port), copies those paths into /srv/stacks/bbs-web and runs docker compose up -d --build.

A config-only change needs no rebuild or restart: the deploy replaces config/config.json, and because the whole folder is mounted, the server sees the new file and reloads it by its mtime. (A single-file mount would keep showing the old file after the replacement.)

Operating it

BASH
cd /srv/stacks/bbs-web
docker compose up -d --build     # start by hand (normally the pipeline does it)
docker compose logs -f bbsweb    # connection log (IP -> BBS)
curl -s localhost:9140/api/health

The container listens on 127.0.0.1:9140; TLS and the domain are nginx's job. The vhost is /etc/nginx/sites-available/bbsweb.teletypegames.org, the certificate is handled by certbot.

One limitation: the BBS sees every web visitor as coming from the bridge container's IP, because telnet has no X-Forwarded-For. The real client IP is in the bbsweb log.

Repository

https://git.teletypegames.org/services/bbs-web