Files

5.5 KiB

SSH Portfolio

A public, anonymous Node.js SSH server that turns a terminal into a customizable portfolio. Visitors connect with:

ssh -p 2222 localhost

Quick start

npm install
npm start

The first run creates data/config.json from config.example.json. Edit persona, pages, colors, ports, and admin keys, then use reload as an admin or restart the server. Pages are content-only; actions are registered in src/commands.js, so adding a command does not require changing the SSH transport.

PM2 deployment

The included ecosystem.config.cjs watches src/ and automatically restarts the SSH server after code changes. Runtime data, SQLite files, dependencies, tests, and logs are ignored to prevent restart loops.

mkdir -p logs
pm2 start ecosystem.config.cjs
pm2 save

Use pm2 logs ssh-portfolio to inspect output. Set PORT or NOTIFICATION_PORT when starting PM2 if the defaults are not suitable, for example PORT=2222 NOTIFICATION_PORT=7777 pm2 start ecosystem.config.cjs.

Authentication model

Anonymous none authentication is intentionally accepted for visitors. Add one or more base64 public-key blobs to adminPublicKeys to enable administrator mode; key authentication is optional and never required. Admins get reload, theme, and announce commands.

Because this is an intentionally open service, run it in a container or restricted account, use a dedicated port, and do not expose private data through configured pages.

Owner notifications

Run npm run notify on the owner machine (or use NOTIFICATION_PORT) to receive chat and announcement notifications over a localhost-only socket. The protocol is newline-delimited JSON, so it is straightforward to replace with a desktop/mobile notification client. For a separate owner machine, forward the port with ssh -N -L 7777:127.0.0.1:7777 <server>.

SSH monitoring dashboard

The server also starts a small, localhost-only web dashboard (default http://127.0.0.1:3000). On first start it creates a random bearer token in data/dashboard-token (mode 0600) and prints a tokenized URL to the server log. Set DASHBOARD_PORT, DASHBOARD_TOKEN, or the dashboard.host, dashboard.port, and dashboard.tokenFile config fields to customize it. Keep the dashboard bound to localhost and access it through an SSH tunnel when needed, for example ssh -N -L 3000:127.0.0.1:3000 <server>.

The dashboard shows active/recent connections, source IPs, SSH client identifiers, and recorded commands. An administrator can close a connection, ban its IP (which also closes matching active connections), or remove a ban. Bans and audit history are stored in SQLite; command history is retained for operational monitoring, so protect the database and dashboard token.

Bulletin board and SQLite

Persistent data is stored in data/portfolio.sqlite by default using SQLite in WAL mode. The server creates board_posts and leaderboard_scores automatically. src/database.js provides small parameterized methods for reading/writing both tables; use those methods for extensions instead of building SQL from visitor input.

Bulletin commands:

  • board or board list — read the latest notes.
  • board post <message> — leave a note.
  • board delete <id> — delete a note as an authenticated administrator.

Posts are limited by character count and UTF-8 byte size. Control characters, terminal escapes, pictographs, and excessive combining marks are rejected. IP addresses are stored only as SHA-256 hashes and are subject to a cooldown and daily posting limit. Configure these limits with board.maxCharacters, board.cooldownSeconds, and board.dailyLimit.

Games

  • flappy starts a live game; press Space to flap and q to quit.
  • pong or pong bot starts a live bot match; use the Up/Down arrow keys and q to quit.
  • pong online waits for or joins another connected visitor. Both players use the Up/Down arrow keys.
  • leaderboard flappy and leaderboard pong show server-recorded scores.

Both games are ASCII-only and calculate scores on the server; visitors cannot submit leaderboard values directly.

Extending

Pages and commands are source modules. Page metadata (name, title, optional order, aliases, visible, requiredRole, and description) lives in the module and is authoritative:

  • src/pages/home.js, about.js, projects.js, and links.js are the premade editable pages.
  • src/pages/index.js is the page registry. Add an import and registry entry when creating a new page; its exported name becomes the SSH command. Aliases are also accepted.
  • src/commands/ contains the premade command modules (chat, contact, announce, and so on).
  • Each game is self-contained in its command module: src/commands/flappy.js owns Flappy physics/live state, and src/commands/pong.js owns Pong physics/matchmaking/live state. The shared src/database.js is intentionally separate because the bulletin board and both games persist data through it.
  • src/commands.js is the command registry. Add an import there to expose a new command.

Each page exports name, title, and render({ config, ctx, api }). The renderer returns terminal text, so you can build extensive custom pages while inheriting the shared header, theme, and prompt. The config.pages entries in the generated JSON are retained only as a legacy content fallback for the premade projects/links pages; titles and navigation metadata come from modules.