Files
pixa/TODO.md

214 lines
10 KiB
Markdown

# Pixa Implementation TODO
A single linear checklist of tasks to implement the complete pixa caching image reverse proxy server.
## Login, Sessions & Encrypted URLs Feature
### Phase 1: Crypto Foundation
- [x] Create `internal/seal/crypto.go` with:
- [x] `DeriveKey(masterKey []byte, salt string) ([32]byte, error)` - HKDF-SHA256 key derivation
- [x] `Encrypt(key [32]byte, plaintext []byte) (string, error)` - NaCl secretbox encrypt, returns base64url
- [x] `Decrypt(key [32]byte, ciphertext string) ([]byte, error)` - NaCl secretbox decrypt from base64url
- [x] Create `internal/seal/crypto_test.go` with tests for:
- [x] Key derivation produces consistent keys for same input
- [x] Encrypt/decrypt round-trip
- [x] Decryption fails with wrong key
- [x] Decryption fails with tampered ciphertext
### Phase 2: Session Management
- [x] Add `github.com/gorilla/securecookie` dependency
- [x] Create `internal/session/session.go` with:
- [x] `Data` struct: `Authenticated bool`, `CreatedAt time.Time`, `ExpiresAt time.Time`
- [x] `Manager` struct using gorilla/securecookie with keys derived via HKDF
- [x] `NewManager(signingKey string, secure bool) (*Manager, error)`
- [x] `CreateSession(w http.ResponseWriter) error` - creates 30-day encrypted cookie
- [x] `ValidateSession(r *http.Request) (*Data, error)` - decrypts and validates cookie
- [x] `ClearSession(w http.ResponseWriter)` - clears cookie (logout)
- [x] `IsAuthenticated(r *http.Request) bool` - convenience wrapper
- [x] Cookie settings: `HttpOnly`, `Secure` (prod), `SameSite=Strict`, name `pixa_session`
- [x] Create `internal/session/session_test.go` with tests for:
- [x] Session creation and validation round-trip
- [x] Expired session rejection
- [x] Tampered cookie rejection
- [x] Missing cookie returns unauthenticated
### Phase 3: Encrypted URL Generation
- [x] Add `github.com/fxamacker/cbor/v2` dependency for compact binary encoding
- [x] Create `internal/encurl/encurl.go` with:
- [x] `Payload` struct (short CBOR field names, omitempty for fields with sane defaults):
- `SourceHost string` (`cbor:"h"`) - required
- `SourcePath string` (`cbor:"p"`) - required
- `SourceQuery string` (`cbor:"q,omitempty"`) - optional
- `Width int` (`cbor:"w,omitempty"`) - 0 = original
- `Height int` (`cbor:"ht,omitempty"`) - 0 = original
- `Format ImageFormat` (`cbor:"f,omitempty"`) - default: orig
- `Quality int` (`cbor:"ql,omitempty"`) - default: 85
- `FitMode FitMode` (`cbor:"fm,omitempty"`) - default: cover
- `ExpiresAt int64` (`cbor:"e"`) - required, Unix timestamp
- [x] `Generator` struct with URL key derived via HKDF (salt: `"pixa-urlenc-v1"`)
- [x] `NewGenerator(signingKey string) (*Generator, error)`
- [x] `Generate(p *Payload) (string, error)` - CBOR encode, encrypt, base64url
- [x] `Parse(token string) (*Payload, error)` - base64url decode, decrypt, CBOR decode, validate expiration
- [x] `(p *Payload) ToImageRequest() *imgcache.ImageRequest`
- [x] Create `internal/encurl/encurl_test.go` with tests for:
- [x] Generate/parse round-trip preserves all fields
- [x] Expired URL returns `ErrExpired`
- [x] Malformed token returns error
- [x] Tampered token fails decryption
### Phase 4: HTML Templates
- [x] Create `internal/templates/templates.go` with:
- [x] `//go:embed *.html` for embedded templates
- [x] `Get() *template.Template` function
- [x] Create `internal/templates/login.html`:
- [x] Simple form with password input for signing key
- [x] POST to `/`
- [x] Error message display area
- [x] Tailwind CSS styling
- [x] Create `internal/templates/generator.html`:
- [x] Logout link at top
- [x] Form with fields: Source URL, Width, Height, Format (dropdown), Quality, Fit Mode (dropdown), Expiration TTL (dropdown)
- [x] POST to `/generate`
- [x] Result display area showing generated URL and expiration
- [x] Click-to-copy functionality for generated URL
### Phase 5: Auth Handlers
- [x] Create `internal/handlers/auth.go` with:
- [x] `HandleRoot() http.HandlerFunc` - serves login form (GET) or authenticates (POST) if not logged in; serves generator form if logged in
- [x] `handleLoginPost(w, r)` - validates key with constant-time comparison, creates session on success
- [x] `HandleLogout() http.HandlerFunc` - clears session, redirects to `/`
- [x] `HandleGenerateURL() http.HandlerFunc` - parses form, generates encrypted URL, renders generator.html with result
### Phase 6: Encrypted Image Handler
- [x] Create `internal/handlers/imageenc.go` with:
- [x] `HandleImageEnc() http.HandlerFunc` - handles `/v1/e/{token}`
- [x] Extract token from chi URL param
- [x] Decrypt and validate via `encGen.Parse(token)`
- [x] Convert to `ImageRequest` via `payload.ToImageRequest()`
- [x] Serve via `imgSvc.Get()` (bypass signature validation - encrypted URL is trusted)
- [x] Set same response headers as regular image handler (Cache-Control, Content-Type, etc.)
- [x] Handle errors: expired → 410 Gone, decrypt fail → 400 Bad Request
### Phase 7: Handler Integration
- [x] Modify `internal/handlers/handlers.go`:
- [x] Add `sessMgr *session.Manager` field to `Handlers` struct
- [x] Add `encGen *encurl.Generator` field to `Handlers` struct
- [x] Initialize session manager and URL generator in `initImageService()`
### Phase 8: Route Registration
- [x] Modify `internal/server/routes.go`:
- [x] Add `s.router.Get("/", s.h.HandleRoot())` - login or generator page
- [x] Add `s.router.Post("/", s.h.HandleRoot())` - login form submission
- [x] Add `s.router.Get("/logout", s.h.HandleLogout())` - logout
- [x] Add `s.router.Post("/generate", s.h.HandleGenerateURL())` - URL generation
- [x] Add `s.router.Get("/v1/e/{token}", s.h.HandleImageEnc())` - encrypted image serving
- [x] Add static file serving for Tailwind CSS
### Phase 9: Testing & Verification
- [x] Run `make check` to verify lint and tests pass
- [ ] Manual test: visit `/`, see login form
- [ ] Manual test: enter wrong key, see error
- [ ] Manual test: enter correct signing key, see generator form
- [ ] Manual test: generate encrypted URL, verify it works
- [ ] Manual test: wait for expiration or use short TTL, verify expired URL returns 410
- [ ] Manual test: logout, verify redirected to login
## Project Setup
- [x] Create Makefile with check, lint, test, fmt targets
- [x] Create project structure (cmd/pixad, internal/*)
- [x] Implement globals package
- [x] Implement logger package
- [x] Implement config package
- [x] Implement database package (SQLite)
- [x] Implement healthcheck service
- [x] Implement middleware package
- [x] Implement handlers package with placeholder routes
- [x] Implement server package (lifecycle, routing, HTTP)
- [x] Wire up fx dependency injection in main.go
- [x] Verify basic server starts and healthcheck works
## Core Image Proxy Features
- [x] Implement URL parsing for `/v1/image/<host>/<path>/<size>.<format>`
- [x] Implement upstream HTTP client with TLS verification
- [x] Implement SSRF protection (block private/internal IPs)
- [x] Implement source host whitelist checking
- [x] Implement HMAC-SHA256 signature generation
- [x] Implement HMAC-SHA256 signature verification
- [x] Implement signature expiration checking
- [x] Implement upstream fetch with timeout and size limits
- [x] Implement Content-Type validation (whitelist MIME types)
- [x] Implement magic byte verification
## Caching Layer
- [x] Design and create SQLite schema for cache metadata
- [x] Implement source content storage (`cache/src-content/<hash>`)
- [x] Implement source metadata storage (`cache/src-metadata/<host>/<hash>.json`)
- [x] Implement output content storage (`cache/dst-content/<hash>`)
- [x] Implement cache key generation
- [x] Implement cache lookup (in-memory hot path)
- [x] Implement cache write
- [x] Implement negative caching (404s)
- [x] Implement cache TTL and expiration
- [ ] Implement cache size management/eviction
## Image Processing
- [x] Select and integrate image processing library (libvips bindings or pure Go)
- [x] Implement image decoding (JPEG, PNG, WebP, GIF, AVIF)
- [x] Implement image resizing with size options (WxH, 0x0, orig)
- [x] Implement format conversion (JPEG, PNG, WebP, AVIF)
- [x] Implement quality parameter support
- [x] Implement max input dimensions validation
- [x] Implement max output dimensions validation
- [ ] Implement EXIF/metadata stripping
- [x] Implement fit modes (cover, contain, fill, inside, outside)
## Security
- [x] Implement path traversal prevention
- [ ] Implement request sanitization
- [ ] Implement response header sanitization
- [ ] Implement referer blacklist
- [ ] Implement blocked networks configuration
- [ ] Add rate limiting per-IP
- [ ] Add rate limiting per-origin
- [ ] Add rate limiting global concurrent fetches
## HTTP Response Handling
- [x] Implement proper Cache-Control headers
- [ ] Implement ETag generation and validation
- [ ] Implement Last-Modified headers
- [ ] Implement conditional requests (If-None-Match, If-Modified-Since)
- [ ] Implement HEAD request support
- [ ] Implement Vary header for content negotiation
- [x] Implement X-Pixa-Cache debug header (HIT/MISS/STALE)
- [ ] Implement X-Request-ID propagation
- [x] Implement proper error response format (JSON)
## Additional Endpoints
- [x] Implement robots.txt endpoint
- [ ] Implement metrics endpoint with auth
- [ ] Implement auto-format selection (format=auto based on Accept header)
## Configuration
- [ ] Add all configuration options from README
- [ ] Implement environment variable overrides
- [ ] Implement YAML config file support
- [ ] Validate configuration on startup
## Operational
- [ ] Implement graceful shutdown
- [ ] Implement Sentry error reporting (optional)
- [ ] Add comprehensive request logging
- [ ] Add performance metrics (Prometheus)
- [x] Write unit tests for URL parsing
- [x] Write unit tests for signature generation/verification
- [x] Write unit tests for cache operations
- [x] Write unit tests for image processing
- [ ] Write integration tests for image proxy flow
- [ ] Write load tests to verify 1-5k req/s target
## Documentation
- [ ] Document configuration options
- [ ] Document API endpoints
- [ ] Document deployment guide
- [ ] Add example nginx/caddy reverse proxy config