Go to file
clawbot 85729d9181
All checks were successful
check / check (push) Successful in 1m41s
fix: update Dockerfile to Go 1.25.4 and resolve gosec lint findings
- Update Dockerfile base image from golang:1.24-alpine to golang:1.25.4-alpine
  (pinned by sha256 digest) to match go.mod requirement of go >= 1.25.4
- Fix gosec G703 (path traversal) false positives by adding filepath.Clean()
  at call sites with nolint annotations for internally-constructed paths
- Fix gosec G704 (SSRF) false positive with nolint annotation; URL is already
  validated by validateURL() which checks scheme, resolves DNS, and blocks
  private IPs
- All make check passes clean (lint + tests)
2026-02-25 05:44:49 -08:00
.gitea/workflows chore: add Gitea Actions CI workflow 2026-02-25 18:22:24 +07:00
cmd/pixad Remove Buildarch from ldflags, use runtime.GOARCH instead 2026-01-08 12:38:24 -08:00
internal fix: update Dockerfile to Go 1.25.4 and resolve gosec lint findings 2026-02-25 05:44:49 -08:00
scripts Add manual test script for auth and encrypted URLs 2026-01-08 10:53:02 -08:00
.dockerignore chore: update .dockerignore to policy standards 2026-02-25 18:22:35 +07:00
.editorconfig chore: add .editorconfig 2026-02-25 18:18:06 +07:00
.gitignore chore: update .gitignore to policy standards 2026-02-25 18:22:33 +07:00
.golangci.yml Add project documentation and linter config 2026-01-08 02:18:45 -08:00
CLAUDE.md Add no-silent-fallback rule to CLAUDE.md 2026-01-08 11:08:33 -08:00
config.example.yml fix: restore original whitelist hosts in config.example.yml 2026-02-25 19:53:23 +07:00
CONVENTIONS.md Add project documentation and linter config 2026-01-08 02:18:45 -08:00
Dockerfile fix: update Dockerfile to Go 1.25.4 and resolve gosec lint findings 2026-02-25 05:44:49 -08:00
go.mod Switch to govips for native CGO image processing 2026-01-08 15:16:34 -08:00
go.sum Switch to govips for native CGO image processing 2026-01-08 15:16:34 -08:00
LICENSE chore: add GPL-3.0 LICENSE file 2026-02-25 18:22:13 +07:00
Makefile fix: auto-detect native deps, skip nix-shell in Docker 2026-02-25 20:11:02 +07:00
README.md chore: restructure README with required policy sections 2026-02-25 19:47:34 +07:00
REPO_POLICIES.md chore: add REPO_POLICIES.md from prompts repo 2026-02-25 18:22:19 +07:00
TODO.md Add WebP encoding support 2026-01-08 11:55:45 -08:00

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 whitelisted 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 of orig, png, jpeg, webp
  • <size>: orig or <width>x<height> (e.g. 800x600)

Source Hosts

Source hosts may be whitelisted in the configuration. Non-whitelisted hosts require an HMAC-SHA256 signature.

Signature Specification

Signatures use HMAC-SHA256 and include an expiration timestamp to prevent replay attacks.

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 none
  • width — requested width in pixels, 0 for original
  • height — requested height in pixels, 0 for original
  • format — 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:

  1. Build input: cdn.example.com:/photos/cat.jpg::800:600:webp:1704067200
  2. Compute HMAC-SHA256 with your secret key
  3. Base64URL-encode the result
  4. URL: /v1/image/cdn.example.com/photos/cat.jpg/800x600.webp?sig=<base64url>&exp=1704067200

Whitelist patterns:

  • Exact match: cdn.example.com — matches only that host
  • Suffix match: .example.com — matches cdn.example.com, images.example.com, and example.com

Configuration

Configured via YAML file (--config). Key settings:

  • access_control_allow_origin — CORS origin
  • source_host_whitelist — list of allowed upstream hosts
  • upstream_fetch_timeout — timeout for origin requests
  • upstream_max_response_size — max origin response size
  • downstream_timeout — client response timeout
  • signing_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

TODO

See TODO.md for the full prioritized task list.

License

GPL-3.0. See LICENSE.

Author

@sneak