v. vanilla 2.0
THE VANILLA HANDBOOK / 2.0

Apache CGI applications

Generate HTML and JSON with a supervised per-request interpreter.

This reference describes the implemented native runtime. Historical paths and internal version labels are retained where relevant. Later extension chapters supersede older limitations.

Vanilla1 now supports server-side HTML/JSON applications through a CGI front controller, not an Apache module. Each request starts a fresh interpreter process. The front controller is Python 3 standard-library code; the language request/response API is implemented in C++. There is no shell evaluation of request data, no persistent worker, and no HTML-embedded template syntax.

Request flow

Apache → fixed executable launcher → gateway/vanilla1_cgi.py → vanilla1 --cgi-worker ... run /operator/selected/script.v.

The gateway parses CGI input, supervises the worker and validates its entire JSON response before sending CGI headers or body. A script that prints half a page and then fails produces a generic 500, not half a successful response. Normal CLI run output is unchanged. --cgi-worker is an internal protocol, not a CGI entry point: do not point Apache directly at the interpreter or use the historical unsupported cgi command. Use the fixed launcher.

Language API

web request $object
web status 201
web header Content-Type "text/html; charset=utf-8"
web header Set-Cookie "session=...; Secure; HttpOnly; SameSite=Lax"
web escape $safe $text
print "<p>" $safe "</p>"
  • web request returns a structured JSON object, assignable to normal JSON/map operations. The object has method, path, query_string, query, form, headers, body and remote_addr fields. Request changes in script variables do not change the original request.
  • path is Apache's PATH_INFO relative to the launcher, not a filesystem path. Missing/empty PATH_INFO maps to /. Only exact configured routes are accepted.
  • query and form are objects whose values are arrays of strings. Repeated keys remain repeated, empty values remain empty. + decodes to a space; percent escapes and decoded UTF-8 are validated strictly. NUL parameters are rejected. Read a value with successive json get operations and json text.
  • headers contains lower-case names derived from CGI HTTP_* variables plus Content-Type. HTTP_PROXY is omitted. Headers remain untrusted input; proxy forwarding headers do not establish client identity. Duplicate raw HTTP headers may already have been combined by Apache.
  • body is the exact UTF-8 text body, not an automatically flattened object. JSON syntax is validated on JSON input; scripts can use json parse and their own schema checks. Binary uploads and multipart forms are not supported.
  • web status accepts 200..599. Default is 200. Body is suppressed for HEAD and statuses 204/205/304. HEAD receives the would-be body length where appropriate.
  • web header replaces a header case-insensitively; Set-Cookie appends. Names are restricted to letters, digits and hyphens; values must be printable ASCII, with no CR/LF. Status, framing/hop-by-hop headers and gateway-generated X-Vanilla-Request-Id / X-Content-Type-Options are reserved. The gateway repeats validation and sets Content-Length from the encoded body, never from a script.
  • print and echo append to the buffered body in worker mode. Default media type is text/plain; charset=utf-8. All response text must be valid UTF-8.
  • web escape escapes & < > " ' for HTML text/quoted-attribute contexts. It is also available in normal CLI mode. It is not JavaScript, CSS or URL escaping. Use appropriate encoders and schema checks for those contexts.

Examples in examples/cgi/: hello.v (escaped HTML with a restrictive CSP), request.v (request inspection), sqlite.v (prepared query). The request inspection example can expose submitted cookies or other secrets; do not publish it as a production diagnostic endpoint.

Deployment shape

Use a dedicated unprivileged application identity such as vanilla-web in a dedicated Apache instance/container. Do not run the gateway as root: it refuses. The test harness starts the Apache parent as root solely so it can drop actual CGI execution to nobody; production should use a dedicated application user, not a shared nobody or a shared account for mutually untrusted tenants.

Install the native executable and its shared-library dependencies, Python 3, and the gateway at operator-owned locations outside DocumentRoot. Example paths:

/opt/vanilla1/bin/vanilla1
/opt/vanilla1/gateway/vanilla1_cgi.py
/opt/vanilla1/cgi-bin/app             # executable fixed launcher
/etc/vanilla1/web.json               # operator configuration
/srv/vanilla1/app/hello.v            # source outside DocumentRoot
/srv/vanilla1/app/request.v
/srv/vanilla1/app/sqlite.v

Templates: gateway/app.cgi.example, gateway/web.json.example, and gateway/apache.conf.example. Replace/install paths deliberately; copying these files into the repository does not modify or deploy any live server. The launcher deliberately omits $@: CGI indexed-query arguments must never be forwarded as interpreter/gateway switches. Run Python with -I to ignore ambient Python import/path settings.

The operator config supplies absolute binary and root paths, an exact route→relative .v script map, optional permissions, optional sql_config, timeout, read_timeout, max_steps, and a test-only sanitizer boolean. Unknown options fail closed. Defaults: no permissions, 5-second execution and body-read deadlines, 100000 instructions. No listen permission can be granted through this gateway. Permission flags are fixed per application, not per URL. Use different app configurations/services for different privilege sets.

Configuration, binaries and selected scripts must be owned by root or the application UID and not group/other writable. Final-component symlinks are rejected for config/binary; route script components below the canonical root cannot be symlinks. Parent directories must remain operator-controlled; this is not an openat-based filesystem sandbox and does not eliminate replacement races by trusted owners. Includes retain the language's existing behavior.

For example, keep config root-owned, mode 0640 with application-group read access; binaries/scripts should be root-owned and not application-writable. Keep writable data and uploads in a separate directory, never the script root or CGI executable directory. Apache must not alias the script root or private configuration directory into public URLs. Use a dedicated DocumentRoot for static content and only ScriptAlias the fixed launcher. Test config changes with Apache's syntax check before deployment.

Remote SQL additionally requires permissions: ["db", "network"] and an absolute sql_config path. The SQL profile file must satisfy the native SQL policy: owned by the effective application UID, mode 0600/0400, regular file. No DB passwords belong in Vanilla scripts, launcher arguments, web.json or request headers. Use restricted database accounts and verified TLS; see REMOTE-SQL.md. SQLite only requires the db permission.

Limits and error handling

  • Methods: GET, HEAD, POST; others return 405 with Allow.
  • Query string: 8 KiB. Request body: 64 KiB. Query and form: at most 128 fields each, keys at most 1024 UTF-8 bytes. Request headers: 64 names / 16 KiB.
  • POST requires a valid CONTENT_LENGTH. Short bodies fail; oversized bodies return 413 before reading. The adapter reads exactly the declared length; Apache remains responsible for HTTP framing, dechunking and ambiguous headers.
  • Nonempty bodies support text/plain, application/json and application/x-www-form-urlencoded. Everything is UTF-8 regardless of a charset parameter; configure clients accordingly. Unsupported media gives
  • Invalid JSON/UTF-8/percent encodings give 400.
  • Responses: 1 MiB UTF-8 body, 32 script headers, 8 KiB aggregate header storage, 4096 bytes per native header value. The worker JSON wire has a separate 7 MiB ceiling for escaping overhead. Worker diagnostics have a 64 KiB ceiling and are discarded, not logged or returned.
  • timeout and read_timeout: 0.1..30 seconds. Parent monotonic deadlines supervise pipe I/O and execution. Timeout kills the worker process group and waits for it; a Linux parent-death SIGKILL also prevents an executing orphan if Apache kills the gateway. Worker core dumps are disabled; CPU and file descriptor limits are applied. This is not a cgroup memory sandbox or a guarantee about kernel uninterruptible waits. Gateway startup/config-file I/O and the final downstream response write are governed by the host/Apache, not the worker execution timer. Apache Timeout should exceed the configured body-read + execution time plus cleanup margin.
  • Runtime/parse failures produce generic 500. Worker deadline gives 504; body read deadline gives 408. A random request ID and a fixed failure category go to stderr; raw worker messages, submitted data and credentials do not. Apache access logging is separate: avoid logging sensitive query strings.
  • Killing a worker cannot undo committed database writes, DDL, external HTTP requests or file changes. A client timeout does not prove that a write failed.

This runs trusted operator-installed scripts, not arbitrary user code. Authentication, sessions, CSRF protection, authorization, business validation, rate limiting and TLS termination remain application/server responsibilities. There is no FastCGI, embedded HTML interpreter, streaming response, SSE or WebSocket-over-CGI support. Keep the existing WebSocket service separate.

Real tests and retained evidence

tests/test_cgi.py runs direct CGI subprocess tests and an isolated real Apache instance on an ephemeral loopback port, using extracted Apache modules. It verifies unprivileged execution, GET/HEAD/POST, HTML escaping, JSON/forms, repeated parameters/cookies, status/header rules, errors without partial data, limits, timeouts, environment isolation, parent-death cleanup, fresh request state, concurrent HTTP clients, private source/config paths and the shipped examples including prepared SQLite. It stops its Apache process group.

make release sanitize
# On this Ubuntu host, the test-only Apache was extracted without installing
# or starting a system Apache service. Its APR runtime dependencies are needed.
# apt-get install libapr1t64 libaprutil1t64
# cd build/cgi && apt-get download apache2-bin
# dpkg-deb -x apache2-bin_*.deb apache-package
make test-cgi
make test-cgi-sanitize
make test
make test-sanitize

The separate tests/run_cgi_sql.py BINARY DISPOSABLE_PROFILES_JSON probe also opens MySQL, MariaDB and Microsoft SQL Server through actual Apache HTTP requests, executes a bound query inside begin/rollback, and closes each handle. It copies generated credentials to a mode-0600 file owned by the CGI UID and removes that copy afterward. Run it only against disposable profiles from tests/provision_remote_sql.py, before invoking that fixture's cleanup script. Both release and sanitizer HTTP/SQL results are in build/cgi/sql-http-{release,asan}.json; the complete native SQL regression results are in build/cgi/sql-final-fixtures/integration-{release,asan}.json.

The fixture harness requires root only to prove the child UID/GID drop; the project path must already be traversable by that UID. It will not change permissions on parent directories. Sanitizer mode sets ASan/UBSan options in the clean child environment; vendor libraries/Python/Apache are not thereby instrumented. Local evidence and absolute fixture/log paths are in build/cgi/verification.json and build/cgi/results-{release,asan}.json. Earlier failed runs are preserved, including an initial concurrent-load browser timeout, a harness naming error and a disappearing-/proc test race. The final results must have complete: true; interrupted runs cannot masquerade as an old passing summary.

References consulted: - Apache CGI tutorial: https://httpd.apache.org/docs/2.4/howto/cgi.html - CGI/1.1 request/response semantics: https://www.rfc-editor.org/rfc/rfc3875 - HTTP HEAD/no-body semantics: https://www.rfc-editor.org/rfc/rfc9110 Raw retrieval evidence: build/cgi/authoritative-references.json.