Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c5af66094 |
@@ -140,16 +140,8 @@ path under `/v1/` answers 200, in maintenance mode too.
|
|||||||
Both `POST` routes accept only a form that pixa's own page served: the page puts
|
Both `POST` routes accept only a form that pixa's own page served: the page puts
|
||||||
a token in the form and sets a cookie to match, and a request without both is
|
a token in the form and sets a cookie to match, and a request without both is
|
||||||
refused with 403, so another site cannot submit the form from a visitor's
|
refused with 403, so another site cannot submit the form from a visitor's
|
||||||
browser. The login and generator pages are meant to be opened over HTTPS: while
|
browser. A form body over 1 MiB is refused with 413. The image routes answer the
|
||||||
`debug` is off, a form sent from a page opened over plain HTTP is refused with
|
errors listed for them with JSON holding `error`, `status` and `timestamp`.
|
||||||
403, and while it is on, so is one sent from a page opened over HTTPS. Plain
|
|
||||||
HTTP is for development on the browser's own machine: the login session cookie
|
|
||||||
is always marked `Secure`, and over plain HTTP a browser keeps such a cookie only
|
|
||||||
for its own machine (`localhost`), if at all. A form is also refused with 403
|
|
||||||
when the page's host is not the `Host` header pixa receives, so a reverse proxy
|
|
||||||
in front of pixa must pass that header on unchanged. A form body over 1 MiB is
|
|
||||||
refused with 413. The image routes answer the errors listed for them with JSON
|
|
||||||
holding `error`, `status` and `timestamp`.
|
|
||||||
|
|
||||||
An image URL has this form:
|
An image URL has this form:
|
||||||
|
|
||||||
@@ -212,9 +204,8 @@ knows the signing key. It works for any upstream host, allowlisted or not,
|
|||||||
without a signature, and whoever gets it can neither read the source URL from it
|
without a signature, and whoever gets it can neither read the source URL from it
|
||||||
nor change what it asks for.
|
nor change what it asks for.
|
||||||
|
|
||||||
1. Open `/` in a browser over HTTPS (or over plain HTTP while `debug` is on, see
|
1. Open `/` in a browser and log in with the signing key (`signing_key`). The
|
||||||
Routes) and log in with the signing key (`signing_key`). The login session
|
login session lasts 30 days, or until `/logout`.
|
||||||
lasts 30 days, or until `/logout`.
|
|
||||||
2. On the generator page, give the source image's URL, the width and height, the
|
2. On the generator page, give the source image's URL, the width and height, the
|
||||||
format, quality and fit, and how long the URL lasts, then submit the form
|
format, quality and fit, and how long the URL lasts, then submit the form
|
||||||
(`POST /generate`). Width and height both empty or `0` keep the original
|
(`POST /generate`). Width and height both empty or `0` keep the original
|
||||||
@@ -429,12 +420,12 @@ Key settings in more detail:
|
|||||||
seconds for one to free up; if none does, and `downstream_timeout` has not
|
seconds for one to free up; if none does, and `downstream_timeout` has not
|
||||||
ended first, it is answered 503 the same way
|
ended first, it is answered 503 the same way
|
||||||
- `maintenance_mode` — while `true`, the image routes (`/v1/image/` and
|
- `maintenance_mode` — while `true`, the image routes (`/v1/image/` and
|
||||||
`/v1/e/`) answer every request for an image with 503, a `Retry-After` header
|
`/v1/e/`) answer every request with 503, a `Retry-After` header and a JSON
|
||||||
and a JSON error body. The health check (`/.well-known/healthcheck.json`)
|
error body. The health check (`/.well-known/healthcheck.json`) still answers
|
||||||
still answers 200 and reports `"maintenance_mode": true`. It stays 200
|
200 and reports `"maintenance_mode": true`. It stays 200 because the image's
|
||||||
because the image's Docker `HEALTHCHECK` requests it: a 503 there would make
|
Docker `HEALTHCHECK` requests it: a 503 there would make the container
|
||||||
the container unhealthy, and upaas marks a deploy failed when its container
|
unhealthy, and upaas marks a deploy failed when its container is unhealthy.
|
||||||
is unhealthy. The login and URL generator pages and `/metrics` keep working
|
The login and URL generator pages and `/metrics` keep working
|
||||||
|
|
||||||
See `config.example.yml` for all options with defaults.
|
See `config.example.yml` for all options with defaults.
|
||||||
|
|
||||||
|
|||||||
@@ -37,11 +37,6 @@ P2: security: referer blacklist
|
|||||||
"Configuration" gives the order in which pixa looks for its config file;
|
"Configuration" gives the order in which pixa looks for its config file;
|
||||||
`config.example.yml` lists `db_url` and `env` and gives every key's default;
|
`config.example.yml` lists `db_url` and `env` and gives every key's default;
|
||||||
`scripts/manual-test.sh` is left to #97.
|
`scripts/manual-test.sh` is left to #97.
|
||||||
- 2026-10-04 dead code in `internal/imgcache` is gone (closes #73): `Purge`,
|
|
||||||
which only returned an error and which nothing called, is no longer part of
|
|
||||||
the `ImageCache` interface or `Service`; the `SignatureValidator`,
|
|
||||||
`Allowlist` and `Storage` interfaces, which nothing implemented or used, are
|
|
||||||
deleted. Nothing else changes.
|
|
||||||
- 2026-10-04 upstream host semaphores and variant `.meta` files no longer
|
- 2026-10-04 upstream host semaphores and variant `.meta` files no longer
|
||||||
outlive their use (closes #87): the fetcher counts the fetches holding or
|
outlive their use (closes #87): the fetcher counts the fetches holding or
|
||||||
waiting for a slot of each upstream host's semaphore and removes the host's
|
waiting for a slot of each upstream host's semaphore and removes the host's
|
||||||
|
|||||||
+5
-5
@@ -22,11 +22,11 @@ port: 8080
|
|||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
# While true, the image routes (/v1/image/ and /v1/e/) answer every request
|
# While true, the image routes (/v1/image/ and /v1/e/) answer every request
|
||||||
# for an image with 503 and a Retry-After header. The health check keeps
|
# with 503 and a Retry-After header. The health check keeps answering 200 and
|
||||||
# answering 200 and reports maintenance_mode as true. It stays 200 because
|
# reports maintenance_mode as true. It stays 200 because the image's Docker
|
||||||
# the image's Docker HEALTHCHECK requests it: a 503 there would make the
|
# HEALTHCHECK requests it: a 503 there would make the container unhealthy, and
|
||||||
# container unhealthy, and upaas marks a deploy failed when its container is
|
# upaas marks a deploy failed when its container is unhealthy.
|
||||||
# unhealthy. (default: false)
|
# (default: false)
|
||||||
maintenance_mode: false
|
maintenance_mode: false
|
||||||
|
|
||||||
# Data directory for SQLite database and cache files
|
# Data directory for SQLite database and cache files
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -153,6 +154,9 @@ type ImageCache interface {
|
|||||||
// Warm pre-fetches and caches an image without returning it
|
// Warm pre-fetches and caches an image without returning it
|
||||||
Warm(ctx context.Context, req *ImageRequest) error
|
Warm(ctx context.Context, req *ImageRequest) error
|
||||||
|
|
||||||
|
// Purge removes a cached image
|
||||||
|
Purge(ctx context.Context, req *ImageRequest) error
|
||||||
|
|
||||||
// Stats returns cache statistics
|
// Stats returns cache statistics
|
||||||
Stats(ctx context.Context) (*CacheStats, error)
|
Stats(ctx context.Context) (*CacheStats, error)
|
||||||
}
|
}
|
||||||
@@ -172,3 +176,29 @@ type CacheStats struct {
|
|||||||
// HitRate is HitCount / (HitCount + MissCount)
|
// HitRate is HitCount / (HitCount + MissCount)
|
||||||
HitRate float64
|
HitRate float64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignatureValidator validates request signatures
|
||||||
|
type SignatureValidator interface {
|
||||||
|
// Validate checks if the signature is valid for the request
|
||||||
|
Validate(req *ImageRequest) error
|
||||||
|
// Generate creates a signature for a request
|
||||||
|
Generate(req *ImageRequest) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allowlist checks if a URL is allowlisted (no signature required)
|
||||||
|
type Allowlist interface {
|
||||||
|
// IsAllowlisted returns true if the URL doesn't require a signature
|
||||||
|
IsAllowlisted(u *url.URL) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage handles persistent storage of cached content
|
||||||
|
type Storage interface {
|
||||||
|
// Store saves content and returns its hash
|
||||||
|
Store(ctx context.Context, content io.Reader) (hash string, err error)
|
||||||
|
// Load retrieves content by hash
|
||||||
|
Load(ctx context.Context, hash string) (io.ReadCloser, error)
|
||||||
|
// Delete removes content by hash
|
||||||
|
Delete(ctx context.Context, hash string) error
|
||||||
|
// Exists checks if content exists
|
||||||
|
Exists(ctx context.Context, hash string) (bool, error)
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,10 +56,11 @@ type ServiceConfig struct {
|
|||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static errors for service construction.
|
// Static errors for service construction and unimplemented operations.
|
||||||
var (
|
var (
|
||||||
errCacheRequired = errors.New("cache is required")
|
errCacheRequired = errors.New("cache is required")
|
||||||
errSigningKeyRequired = errors.New("signing key is required")
|
errSigningKeyRequired = errors.New("signing key is required")
|
||||||
|
errPurgeNotImplemented = errors.New("purge not implemented")
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewService creates a new image service.
|
// NewService creates a new image service.
|
||||||
@@ -188,6 +189,11 @@ func (s *Service) Warm(ctx context.Context, req *ImageRequest) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purge removes a cached image. Purging is not implemented yet.
|
||||||
|
func (s *Service) Purge(_ context.Context, _ *ImageRequest) error {
|
||||||
|
return errPurgeNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
// Stats returns cache statistics.
|
// Stats returns cache statistics.
|
||||||
func (s *Service) Stats(ctx context.Context) (*CacheStats, error) {
|
func (s *Service) Stats(ctx context.Context) (*CacheStats, error) {
|
||||||
return s.cache.Stats(ctx)
|
return s.cache.Stats(ctx)
|
||||||
|
|||||||
Reference in New Issue
Block a user