Yet another form handler. FastCGI Form Collector which turns submissions into to-do items for manual completion. Form fields are arbitrary and stored as submitted in JSON format. This project provides a minimal FastCGI listener for HTML form submissions, backed by PostgreSQL storage and an authenticated admin dashboard served over FastCGI.
- FastCGI listener on a Unix domain socket for dynamic form submissions and the admin dashboard (served via the same socket),
with an optional TCP FastCGI listener via
-tcp <port>. - PostgreSQL persistence with JSONB storage for arbitrary form fields and request metadata (real source IP, the client-supplied
X-Forwarded-Forheader, user agent, referrer, timestamp). Both the trusted peer address and the forwarded header are stored as-is — the forwarded value is attacker-controllable, so treat it accordingly when reviewing. - Admin dashboard with login (bcrypt passwords stored in the database), pagination, status management (
new,in_progress,complete,archived), CSRF protection, hardened cookies, and nicely formatted JSON payloads. - Built-in throttling that blocks abusive IPs (over 4 submissions per minute) for 24 hours and temporarily pauses all submissions for 5 minutes when a burst of distinct IPs appears.
- Request caps to protect the FastCGI endpoint from floods (64KB body and 200-field limit, with an adjustable upload budget on top).
- Optional file capture that, when enabled, stores an uploaded file inside the
_ftdchroot with a unique timestamped name and records both the stored path and the original filename alongside the submission JSON. File uploads are disabled by default. - Sample submitter forms:
sample_form.html(no file upload) andsample_form_upload.html(includes a single file field) that post to the FastCGI endpoint.
Set the following environment variables before running the server:
| Variable | Description | Default |
|---|---|---|
DATABASE_URL |
PostgreSQL connection string (e.g., postgres://user:pass@localhost:5432/forms). |
Required |
FASTCGI_SOCKET |
Unix socket path for the FastCGI listener. | /var/www/run/ftd.sock |
FORM_PATH |
FastCGI path for submissions. | /form |
ADMIN_PREFIX |
FastCGI path prefix for the admin dashboard (login, dashboard, static). | /form/admin |
SESSION_SECRET |
Secret used to sign admin session cookies; if omitted, an ephemeral random key is generated (sessions reset on restart). | Generated per process when unset |
SESSION_COOKIE_INSECURE |
If set, disables the default Secure cookie flag for admin/csrf cookies (useful for plain HTTP dev). |
Not set (Secure cookies enabled) |
MAX_UPLOAD_MB |
Maximum allowed file upload size in megabytes; 0 disables uploads. |
0 |
Initialize the schema before the first run (ftd does not apply migrations at runtime and will exit if required tables are missi ng):
psql "$DATABASE_URL" -f schema.sqlThe schema creates submissions (with metadata, optional stored file path, reviewer comment, and JSONB payload), admin_users (bcrypt password hashes), and submission_blocks (temporary throttling windows). Indexes are added for status filtering and date ordering to keep pagination fast.
The schema also seeds a default admin account (admin / change-me); the dashboard will display a red reminder until you change it via the password form.
If you prefer to rotate the password directly from psql instead of using the dashboard, run these single-line commands at the psql> prompt to enable pgcrypto and set a new bcrypt hash for the admin account:
CREATE EXTENSION IF NOT EXISTS pgcrypto;
UPDATE admin_users SET password_hash = crypt('your-new-password', gen_salt('bf')) WHERE username = 'admin';
This is a standard Go module (Go 1.22+). Before your first build, fetch the
dependencies and write go.sum:
go mod tidyThen build the binary:
go build -o /usr/local/bin/ftdgo mod tidy downloads github.com/lib/pq, golang.org/x/crypto, and
golang.org/x/sys (the last is used for OpenBSD pledge(2)), so make sure
outbound module downloads are permitted by your environment. You only need to
re-run it after changing dependencies; routine rebuilds can call go build
directly.
- Export the required environment variables (see above).
- Fetch dependencies (first run only):
go mod tidy. - Start the service:
go run . - Point your FastCGI-capable web server at the configured socket (default
/var/www/run/ftd.sock) for both form and admin paths. The service defaults to/formfor submissions and/form/adminfor the dashboard, configurable viaFORM_PATHandADMIN_PREFIXenv vars. Alternatively, start the service with-tcp 9000(or another port) and configure your front-end to FastCGI proxy to127.0.0.1:9000. - Serve
sample_form.htmlvia your web server (or open from disk) and point itsactionat/form(or yourFORM_PATH) on your FastCGI front-end. Access the admin dashboard through the same front-end at/form/admin/(or yourADMIN_PREFIX). To redirect submitters to a thank-you page after a successful submission, include a hidden field namedredirect; the handler issues a 303 See Other to that target once the form is stored. To avoid the endpoint being abused as an open redirect, the target must be either a root-relative path (e.g./thank-you) or an absolutehttp(s)URL whose host matches the request host. Cross-host URLs, protocol-relative URLs (//host), and other schemes are rejected with HTTP 400.
- Uploads are disabled by default. Set
MAX_UPLOAD_MBto a positive integer to allow a single file upload per submission, capped to that size and counted against the FastCGI body budget. - Uploaded files are stored under
uploads/inside the_ftdchroot (created on demand) using names likeftd.20240101T000000Z,89abcd12. The stored path lives in thefile_pathcolumn, and both the stored name and any client-supplied original name are injected into theform_dataJSON as_upload_stored_filenameand_upload_original_filename. When a write fails, the row keepsFailed Upload (<status code>)infile_pathand in_upload_stored_filenameso reviewers can see the error. - The lightweight sample
sample_form.htmlremains text-only; usesample_form_upload.htmlfor an upload-capable example (withenctype="multipart/form-data").
Rows start as new. The admin UI lets you move them to in_progress, complete, or archived. Completed submissions remain available but collapse into the lower section; archived items stay out of the main dashboard and live in the dedicated Archived view with its own pagination. A bulk "Archive completed" control is available on the Active dashboard to sweep all completed rows into the archived view at once. Each submission also supports an internal reviewer comment field; it can be edited alongside status updates and is rendered in a muted, read-only state when the submission is archived.
- Each client IP may submit at most 4 forms per rolling minute. Exceeding that threshold blocks the IP for 24 hours and returns HTTP 429 with a
Retry-Afterhint. - If a sudden burst of 30 or more distinct IPs arrives within a minute, the service pauses all submissions for 5 minutes to mitigate abuse and returns HTTP 503 with
Retry-After. - Block information is stored in the
submission_blockstable; expired blocks are cleaned when new requests arrive.
main.go– FastCGI listener, admin routes, and handlers.schema.sql– PostgreSQL schema and indexes.templates/– Admin HTML templates.static/– Admin CSS assets.sample_form.html– Example HTML form posting to the FastCGI endpoint.rc.d/ftd– OpenBSDrc.dhelper that loads/etc/ftd.envand backgrounds the daemon underrcctl.
- Set a strong
SESSION_SECRETbefore first run; if you omit it, the server will generate a random per-process key and all sessions will be invalidated on restart. The initial admin account (admin) ships in the schema with passwordchange-me—the dashboard surfaces a warning until you change it via the built-in password form. - Admin sessions are signed (with refresh-on-activity to avoid logging out active tabs) and constrained with
Secure,HttpOnly, andSameSite=Strictflags by default. SetSESSION_COOKIE_INSECURE=1only for non-TLS local testing. - CSRF tokens are required on admin POSTs (login and status updates) and validated against secure cookies.
- Admin responses set conservative security headers (CSP, frame-ancestors deny, referrer/permissions policies, cache disabling, MIME sniff protection) to reduce injection and clickjacking risk.
- Submission bodies are capped (64KB) and oversized/overlong forms are rejected to slow data flooding.
- Post-submission redirects (
redirectfield) are restricted to the request host or root-relative paths, so the public endpoint cannot be used as an open redirect for phishing. - Terminate TLS at your front-end web server (nginx/httpd). The app records the connection peer (
REMOTE_ADDR, as supplied by the front-end over FastCGI) as the trusted source IP and keys rate limiting on it; if the front-end also forwards anX-Forwarded-Forheader, that client-supplied value is stored verbatim alongside it for reference. The forwarded header is never trusted for rate limiting because it can be spoofed. - Restrict filesystem permissions on the FastCGI socket (
FASTCGI_SOCKET) so only the web server can connect. The socket is created before chroot/drop-privilege when starting asroot. - If the process starts as
root, it will chroot to the_ftduser's home and drop privileges to that account after opening the PostgreSQL socket and FastCGI listener. Create the_ftduser and ensure its home directory exists before launching. - On OpenBSD, pledge(2) is used: startup allows file/socket setup and DNS, then pledges are tightened after connecting to PostgreSQL and preparing listener sockets (with promises adjusted depending on whether a Unix socket or TCP FastCGI port is used).
- Install dependencies and create the service account:
pkg_add go postgresql-client useradd -m _ftd
- Initialize the database schema and admin user (replace credentials as needed):
createdb ftd psql ftd -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" psql ftd < schema.sql go mod tidy env \ DATABASE_URL="postgres://<user>:<pass>@<host>:<port>/<db>" \ go build -o /usr/local/bin/ftd
go mod tidyonly needs to run once (or after dependency changes) to fetch modules and writego.sum. Other environment variables use the defaults noted in the Configuration table; override them if you need a custom socket path or URL prefixes. - Create the run directory and permissions for httpd:
install -d -m 750 -o _ftd -g www /var/www/run
- Configure
/etc/httpd.conf:server "example.com" { listen on * port 80 location "/form" { fastcgi socket "/var/www/run/ftd.sock" } location "/form/admin/*" { fastcgi socket "/var/www/run/ftd.sock" } } - Templates and admin static assets are embedded in the binary and served from the
_ftdchroot (the_ftduser home). You do not need to copy thetemplates/orstatic/directories to the filesystem; only place the sample HTML forms in your web root if you want to expose them directly. - Install the provided
rc.dhelper and supply environment via/etc/ftd.env:install -m 755 rc.d/ftd /etc/rc.d/ftd cat <<'EOF' > /etc/ftd.env
DATABASE_URL="postgres://:@:/"
EOF
7. Enable and start the services (the daemon opens sockets before chrooting/dropping to `_ftd`):
```sh
rcctl enable httpd
rcctl start httpd
rcctl enable ftd
rcctl start ftd
```
8. Log into the dashboard at `/form/admin/` with `admin` / `change-me`, then update the password using the on-page form (a warning remains until you do).
### Linux + nginx (FastCGI over Unix socket)
1. Install dependencies and create the service account:
```sh
sudo apt-get update && sudo apt-get install -y golang postgresql-client nginx
sudo useradd -m -s /usr/sbin/nologin _ftd
- Initialize the database schema and admin user:
createdb ftd psql ftd -c "CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\";" psql ftd < schema.sql go mod tidy env \ DATABASE_URL="postgres://<user>:<pass>@<host>:<port>/<db>" \ go build -o /usr/local/bin/ftd
go mod tidyonly needs to run once (or after dependency changes) to fetch modules and writego.sum. All other environment variables keep their documented defaults unless you override them (e.g., socket path or URL prefixes). - Prepare the FastCGI socket path for nginx:
sudo install -d -m 750 -o _ftd -g www-data /var/www/run
- Configure nginx (e.g.,
/etc/nginx/sites-available/ftd.conf):Enable the site and reload nginx:server { listen 80; server_name example.com; location /form { include fastcgi_params; fastcgi_pass unix:/var/www/run/ftd.sock; } location /form/admin/ { include fastcgi_params; fastcgi_pass unix:/var/www/run/ftd.sock; } }sudo ln -s /etc/nginx/sites-available/ftd.conf /etc/nginx/sites-enabled/ftd.conf sudo nginx -t sudo systemctl reload nginx
- Templates and admin static assets are embedded in the binary and served from the
_ftdchroot (the_ftduser home). There is no need to copytemplates/orstatic/onto the host filesystem; only publish the sample HTML forms if you wish to serve them directly. - Run the FastCGI service (with socket creation before chroot/drop-privilege):
sudo -u _ftd /usr/local/bin/ftd
- Sign in at
/form/admin/asadmin/change-meand rotate the password via the dashboard form; the UI warns while the default remains.