Skip to content

Latest commit

 

History

History
233 lines (169 loc) · 10.4 KB

File metadata and controls

233 lines (169 loc) · 10.4 KB

🎓 Pholani — A Friendly Pascal IDE for the Web

Pholani (pronounced Po-LAH-nee, isiZulu for "look") is a small, safe, browser-based Pascal/Delphi IDE built for Grade-12 students. Write Pascal in your browser, click run, see your program come to life — no Delphi install, no terminal gymnastics.

Browser demo — Hello, World


Table of contents


Why Pholani

Most Pascal tooling for high school is either Windows-only Delphi or a raw fpc terminal. Both lose students. Pholani is the middle ground: a friendly browser editor over the real Free Pascal Compiler, with the rough edges (input handling, sandboxing, error UX) already filed down.

What you get out of the box:

  • ✏️ CodeMirror editor with Pascal syntax highlighting, brackets, F5-to-run.
  • ▶️ Real compile + run against fpc, including stdin support for readln — type the input into a side panel and the program reads it.
  • 🛡️ Hardened backend — Zod-validated requests, strict filename allowlist, per-IP rate limit, Helmet + CSP, per-request sandbox dirs.
  • LRU compile cache — re-running the same code is instant.
  • 🧪 Real tests — Vitest + Supertest + Playwright (real Chromium against a real server). No tutorial markdown pretending to be tests.
  • 📦 Docker image for teachers who don't want to install anything.

Quick start

git clone <repo> && cd pholani
npm install
npm start           # → http://localhost:3000

Need the real compiler:

OS Command
macOS brew install fpc
Debian / Ubuntu sudo apt install fpc
Anywhere with Docker docker build -t pholani . && docker run -p 3000:3000 pholani

Don't have fpc and just want to poke around? npm test and npm run test:e2e both work — they use a bundled fake compiler.

Install demo

Terminal install demo


readln works in the browser

The original Pholani couldn't run any program that called readln — the compiled binary had no stdin. Pholani 2 fixes that with a dedicated Program Input textarea: anything you type there is piped to the running program before it starts.

Browser demo — readln program


Architecture

System overview

Module dependency graph:

Module graph

Compile request — sequence:

Compile sequence

Source for the diagrams lives at docs/diagrams/*.d2. Re-render with d2 file.d2 file.svg.

Concern Lives in Notes
HTTP bootstrap src/server.js Reads validated config, starts Express.
App composition src/app.js Wires middleware + routes; no I/O.
Security headers / CORS src/middleware/security.js Helmet + CSP, explicit origin allowlist.
Rate limiting src/middleware/rateLimit.js In-memory LRU token bucket; per-IP.
Error mapping src/middleware/errorHandler.js Maps err.statusCode / err.code to JSON.
Routes src/routes/{compile,files,health}.js Thin — only Zod parsing + service calls.
Compile pipeline src/services/compiler.js execFile for fpc, spawn for the produced binary with stdin piping.
File storage src/services/fileStore.js Path-traversal-proof load/save into temp/uploads/.
In-memory caches src/services/cache.js lru-cache instances.
Safe filenames / paths / hashing src/lib/ Pure helpers, fully unit-tested.
Env validation src/config/index.js zod schema — fail fast on misconfig.

Key invariants:

  1. Per-request sandbox — every compile gets a fresh temp/compile/<uuid>/ directory, removed in finally. Concurrent students never collide.
  2. Cache key = SHA-256 of filename::code::stdin — identical reruns return instantly without spawning fpc.
  3. spawn + manual stdin pipe for the produced binary, with timeout and output-size caps.

Caching

Cache Key Value Size TTL Purpose
compileCache sha256(filename::code::stdin) {output, error, compilationOutput} 200 30 min Skip recompilation for identical submissions (the common case in a classroom).
fpcCheckCache installed {installed, version, message} 1 5 min Avoid spawning fpc -h on every page load.
rateBuckets req:<ip> / compile:<ip> count 10 000 1 min Per-IP token bucket for general + compile traffic.

All caches use lru-cache. They live in the Node process — no Redis or Memcached, no extra ops for a classroom deployment. Reset them in tests with resetAllCaches() from src/services/cache.js.


Security model

Pholani runs student-supplied code on the host. That is inherently risky. Here is what it defends against and what it does not.

Defends against:

Threat Defense Where
Shell / command injection via filename execFile (file form, never a shell) src/services/compiler.js
Path traversal (e.g. ../../etc/passwd) Whitelist ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}\.pas$ + path.resolve containment src/lib/safeFilename.js, src/lib/safePath.js
Body-size DoS express.json({ limit: '100kb' }) + Zod size limits src/app.js, src/routes/compile.js
Compile / run DoS Per-IP token bucket (default 30 compiles / min, 120 requests / min) src/middleware/rateLimit.js
Output-size DoS Run stdout/stderr capped at 64 kB src/services/compiler.js
Infinite loops 10 s SIGKILL timeout src/services/compiler.js
Cross-origin abuse Explicit CORS allowlist src/middleware/security.js
Common header attacks helmet with CSP src/middleware/security.js
Disclosure of host paths / stack traces Sanitized JSON error responses src/middleware/errorHandler.js

Does not defend against:

  • A compiled program calling fpSystem / Exec / making outbound network calls as the server user. Pascal programs run with full Unix privileges of the Node process.
  • A malicious user with shell access to the host.
  • Filesystem exhaustion if you raise PHOLANI_MAX_CODE_BYTES enormously and disable rate limiting.

Therefore: do not expose Pholani to the open internet without an OS-level sandbox. Recommended:

  1. Single-host classroom — run inside the bundled Docker image (docker run -p 3000:3000 pholani). The container's filesystem is ephemeral.
  2. Hardened — rootless container with --read-only, --tmpfs /app/temp, --cpus/--memory caps, auth reverse proxy.
  3. Optional — replace PHOLANI_FPC_BIN with a wrapper that invokes the compiler and produced binary inside nsjail / firejail / bubblewrap.

Configuration

All knobs are environment variables validated by src/config/index.js:

Variable Default Purpose
PORT 3000 HTTP listen port
NODE_ENV development development | test | production
PHOLANI_TEMP_DIR ./temp Scratch dir for uploads + compile sandboxes
PHOLANI_FPC_BIN fpc Path to the compiler (override for sandbox wrappers or tests)
PHOLANI_CORS_ORIGINS http://localhost:3000 Comma-separated allowlist
PHOLANI_MAX_CODE_BYTES 100000 Hard ceiling on submitted source size
PHOLANI_COMPILE_TIMEOUT_MS 30000 Kill the compiler if it stalls
PHOLANI_RUN_TIMEOUT_MS 10000 Kill the produced binary if it loops
PHOLANI_RUN_OUTPUT_BYTES 65536 Clamp run stdout/stderr
PHOLANI_RATE_COMPILE_PER_MIN 30 Per-IP compile budget
PHOLANI_RATE_REQUEST_PER_MIN 120 Per-IP general budget

Invalid values cause the server to crash on boot with a Zod error — by design.


Tests

Command What it does
npm test Vitest unit + API tests with v8 coverage. Fails below 80% lines / functions / statements.
npm run test:unit Just tests/unit/.
npm run test:api Just tests/api/ (Supertest against an in-process Express app).
npm run test:e2e Playwright: spins a real server on port 3456, drives Chromium, runs hello-world + readln + error scenarios.
PHOLANI_RECORD_DEMOS=1 npm run test:e2e -- tests/e2e/demo.spec.js Re-records the browser demo gifs in docs/demos/.

The bundled tests/fixtures/fake-fpc.sh lets the entire pipeline run on machines without Free Pascal installed.


Project layout

.
├── src/
│   ├── server.js              # bootstrap
│   ├── app.js                 # composition root
│   ├── config/index.js        # Zod-validated env
│   ├── middleware/{security,rateLimit,errorHandler}.js
│   ├── routes/{compile,files,health}.js
│   ├── services/{compiler,fileStore,cache}.js
│   └── lib/{safeFilename,safePath,sha}.js
├── public/                    # browser SPA (index.html, script.js, styles.css)
├── tests/
│   ├── unit/                  # Vitest
│   ├── api/                   # Supertest
│   ├── e2e/                   # Playwright
│   └── fixtures/fake-fpc.sh
├── docs/
│   ├── diagrams/              # *.d2 source + *.svg rendered
│   ├── demos/                 # *.tape + *.gif
│   ├── learning-examples/     # Pascal walkthroughs for students
│   └── pholani-overhaul-design.md  # full v2 design spec
├── Dockerfile
├── docker-compose.yml
├── .github/workflows/ci.yml
├── CLAUDE.md
├── CONTRIBUTING.md
└── README.md

Further docs


License

MIT.