closes #52 Implements startup configuration validation per the plan on #52. Two commits, TDD: the first commit adds the enforcement tests (red — six test functions fail against the lenient behavior) plus a mechanical extraction of `newFromSmartConfig` from `config.New` so construction is testable without fx; the second commit makes them green and carries the `TODO.md` bookkeeping. ## Behavior - **No silent fallbacks**: a config value that is SET but unparseable or invalid aborts startup with an error naming the key and value. Defaults apply only to OMITTED keys. The old `getString`/`getInt`/`getBool` helpers swallowed every conversion error and returned the default; they are now strict. Fractional ports are rejected, not truncated (smartconfig's `GetInt` would have turned `8080.5` into `8080`). - **Unknown keys abort**: unknown top-level keys and unknown `metrics` subkeys are fatal, each named in the error (`unknown config keys: whitelist_hosts`). The `env` section stays permitted because smartconfig consumes it for environment injection. - **Malformed config file aborts**: a config file that exists at a standard location but fails to parse was previously logged as a warning and skipped (the server would start on defaults); it is now fatal. - **Range/sanity checks**: `port` in 1-65535; `upstream_connections_per_host` at least 1; `signing_key` required, at least 32 characters (keyless mode was never implemented; the stale "leave empty" comment in `config.example.yml` is corrected); `allowlist_hosts` entries must be bare hostnames (leading-dot suffix patterns still allowed; schemes, paths, whitespace, non-string and empty entries rejected); `state_dir` non-empty and verified creatable+writable with a probe file before the listener binds; `sentry_dsn` must be a URL with scheme and host when set; `metrics.username`/`metrics.password` must be set together. ## Verification - `make check` green on the branch head (all tests, golangci-lint 0 issues, fmt-check clean). - End-to-end: `./bin/pixad` with `port: banana` exits 1 printing `config key "port": value "banana" is not an integer`; with `whitelist_hosts:` it exits 1 printing `unknown config keys: whitelist_hosts`. ## Notes for review - `getStringSlice` keeps its lenient signature because the existing tests in `config_test.go` exercise it and modifying existing tests requires explicit approval. Strictness for `allowlist_hosts` is instead enforced up front on the raw value by `validateAllowlistHostsValue`, so nothing is silently skipped; extraction then reuses the existing parser. If you prefer the helper folded into a single strict function, that requires retargeting those three tests — happy to do that as a follow-up with approval. - `TODO.md` here is edited against current `main`; PR #50 (merge-ready) edits adjacent lines, so whichever merges second will need a trivial rebase of `TODO.md` only. - The README Configuration section lists keys that have never existed in the code (`access_control_allow_origin`, `upstream_fetch_timeout`, `upstream_max_response_size`, `downstream_timeout`). Under this change a config using them now fails fast instead of silently doing nothing — that is the intended behavior. Implementing them is already tracked as the P2 "add all configuration options from README" item in `TODO.md`. Co-authored-by: sneak <sneak@sneak.berlin> Reviewed-on: #53 Co-authored-by: clawbot <clawbot@noreply.example.org> Co-committed-by: clawbot <clawbot@noreply.example.org>
pixa
pixa is a GPL-3.0-licensed Go web server by @sneak that proxies images from upstream sources, optionally resizing or transforming them, and serves the results. Both source and transformed images are cached to disk so that subsequent requests are served without origin fetches or additional processing.
Getting Started
# clone and build
git clone https://git.eeqj.de/sneak/pixa.git
cd pixa
make build
# run with a config file
./bin/pixad --config config.example.yml
# or build and run via Docker
make docker
docker run -p 8080:8080 pixad:latest
Rationale
Image-heavy web applications need a fast, caching reverse proxy that can resize and transcode images on the fly. pixa fills that role as a single, self-contained binary with no external runtime dependencies beyond libvips. It supports HMAC-SHA256 signed URLs with expiration to prevent abuse, and allowlisted source hosts for open access.
Design
Storage
- Source content:
<statedir>/cache/src-content/<ab>/<cd>/<sha256 of source content> - Source metadata:
<statedir>/cache/src-metadata/<hostname>/<sha256 of path>.json(fetch time, original headers, request, content hash) - Database:
<statedir>/state.sqlite3(SQLite) - Output documents:
<statedir>/cache/dst-content/<ab>/<cd>/<sha256 of output content>
Multiple source paths may reference the same content blob; the database tracks references rather than using filesystem refcounting. In-process caching of request-to-output mappings targets 1-5k r/s.
Routes
/v1/image/<host>/<path>/<size>.<format>?sig=<signature>&exp=<expiration>
Images are only fetched from origins using TLS with valid certificates.
<format>: one oforig,png,jpeg,webp<size>:origor<width>x<height>(e.g.800x600)
Source Hosts
Source hosts may be allowlisted in the configuration. Non-allowlisted hosts require an HMAC-SHA256 signature.
Signature Specification
Signatures use HMAC-SHA256 and include an expiration timestamp to prevent replay attacks. Signatures are exact match only: every component (host, path, query, dimensions, format, expiration) must match exactly what was signed. No suffix matching, wildcard matching, or partial matching is supported.
Signed data format (colon-separated):
HMAC-SHA256(secret, "host:path:query:width:height:format:expiration")
Where:
host— source origin hostname (e.g.cdn.example.com)path— source path (e.g./photos/cat.jpg)query— source query string, empty string if nonewidth— requested width in pixels,0for originalheight— requested height in pixels,0for originalformat— output format (jpeg, png, webp, avif, gif, orig)expiration— Unix timestamp when signature expires
Example: resize
https://cdn.example.com/photos/cat.jpg to 800x600 WebP with
expiration 1704067200:
- Build input:
cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200 - Compute HMAC-SHA256 with your secret key
- Base64URL-encode the result
- URL:
/v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200
Allowlist patterns:
- Exact match:
cdn.example.com— matches only that host - Suffix match:
.example.com— matchescdn.example.com,images.example.com, andexample.com
Configuration
Configured via YAML file (--config). Key settings:
access_control_allow_origin— CORS originallowlist_hosts— list of allowed upstream hostsupstream_fetch_timeout— timeout for origin requestsupstream_max_response_size— max origin response sizedownstream_timeout— client response timeoutsigning_key— HMAC secret for URL signatures
See config.example.yml for all options with defaults.
Architecture
- Dependency injection: Uber fx
- HTTP router: go-chi
- Image processing: govips (CGO wrapper for libvips)
- Database: SQLite via modernc.org/sqlite
- Static assets: embedded via
//go:embed - Metrics: Prometheus
- Logging: stdlib slog
Entrypoints
This repository adheres to the
Scripts to Rule Them All
standard: normalized scripts in script/ are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call
them. We provide:
script/bootstrap— install all dependencies (idempotent)script/setup— make a fresh clone ready for development (bootstrap, then install-precommit)script/projectname— output the project name ("pixa")script/test— run the test suitescript/lint— run golangci-lintscript/fmt— format all code (writes)script/fmt-check— check formatting (read-only)script/check— run test, lint, and fmt-checkscript/docker— build the Docker image tagged viascript/projectnamescript/cibuild— CI entrypoint:docker build .(the Dockerfile runs the checks, so a green build implies a green repo)script/precommit— pre-commit checks (go mod tidyguard, thenscript/check)script/install-precommit— install the git pre-commit hook that runsscript/precommit
TODO
See TODO.md for the full prioritized task list.
License
GPL-3.0. See LICENSE.