Author SHA1 Message Date
clawbot 948ce4800f Move the example config to configs/, delete scripts/ and CONVENTIONS.md (closes #97)
check / check (push) Failing after 3s
config.example.yml moves unchanged to configs/config.example.yml, the
directory REPO_POLICIES.md names for configuration examples. README.md,
the comments in internal/config/config.go and the startup error for the
placeholder signing key name the new path. scripts/manual-test.sh and
its directory are deleted. The handler tests in internal/handlers cover
every check it made except two: fetching a real image from the internet,
and a URL made on the generator page with a ttl answering 410 once the
ttl has passed (#199).
CONVENTIONS.md, a reformatted copy of the Go HTTP server conventions, is
deleted, as REPO_POLICIES.md links the canonical document.

Model: opus-5-5
2026-10-04 18:59:51 +00:00
6 changed files with 16 additions and 1412 deletions

No files matched your search

-1259
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -18,7 +18,7 @@ make build
# run with a config file: copy the example and set a real signing key # run with a config file: copy the example and set a real signing key
# (the example placeholder is refused at startup), e.g. with # (the example placeholder is refused at startup), e.g. with
# openssl rand -base64 32 # openssl rand -base64 32
cp config.example.yml config.yml cp configs/config.example.yml config.yml
$EDITOR config.yml # replace the signing_key placeholder $EDITOR config.yml # replace the signing_key placeholder
./bin/pixad --config config.yml ./bin/pixad --config config.yml
@@ -520,7 +520,7 @@ Key settings in more detail:
the container unhealthy, and upaas marks a deploy failed when its container the container unhealthy, and upaas marks a deploy failed when its container
is unhealthy. The login and URL generator pages and `/metrics` keep working is unhealthy. The login and URL generator pages and `/metrics` keep working
See `config.example.yml` for all options with defaults. See `configs/config.example.yml` for all options with defaults.
### Architecture ### Architecture
+10
View File
@@ -31,6 +31,16 @@ P2: security: referer blacklist
# Completed Steps # Completed Steps
- 2026-10-04 fewer files in the repository root (closes #97):
`config.example.yml` moved unchanged to `configs/config.example.yml`, and
`README.md`, the comments in `internal/config/config.go` and the startup error
for the placeholder signing key name the new path; `scripts/manual-test.sh`
and its directory are deleted, as the handler tests in `internal/handlers`
cover every check it made except two: fetching a real image from the
internet, and a URL made on the generator page with a `ttl` answering 410 once
the `ttl` has passed (https://git.eeqj.de/sneak/pixa/issues/199);
`CONVENTIONS.md` is deleted, as `REPO_POLICIES.md` links the canonical Go HTTP
server conventions.
- 2026-10-04 SQLite writes no longer fail with "database is locked" (closes - 2026-10-04 SQLite writes no longer fail with "database is locked" (closes
#198): pixa adds `_pragma=busy_timeout(5000)` to every `db_url`, so a write #198): pixa adds `_pragma=busy_timeout(5000)` to every `db_url`, so a write
that finds another in progress on another connection waits up to five seconds that finds another in progress on another connection waits up to five seconds
File renamed without changes.
+4 -4
View File
@@ -62,7 +62,7 @@ const (
) )
// placeholderSigningKey is the dummy signing_key shipped in // placeholderSigningKey is the dummy signing_key shipped in
// config.example.yml. It is 45 characters, so it passes the length // configs/config.example.yml. It is 45 characters, so it passes the length
// check, but it is public in this repository and must be rejected at // check, but it is public in this repository and must be rejected at
// startup so no deployment ever signs URLs with it. // startup so no deployment ever signs URLs with it.
const placeholderSigningKey = "CHANGE_ME_generate_with_openssl_rand_base64_32" const placeholderSigningKey = "CHANGE_ME_generate_with_openssl_rand_base64_32"
@@ -88,7 +88,7 @@ var (
errMustBeAtLeastOne = errors.New("must be at least 1") errMustBeAtLeastOne = errors.New("must be at least 1")
errValueTooShort = errors.New("value too short") errValueTooShort = errors.New("value too short")
errPlaceholderKey = errors.New( errPlaceholderKey = errors.New(
"is the placeholder from config.example.yml; " + "is the placeholder from configs/config.example.yml; " +
"generate a real key with: openssl rand -base64 32") "generate a real key with: openssl rand -base64 32")
errMustBeSetTogether = errors.New("must be set together") errMustBeSetTogether = errors.New("must be set together")
errMustNotBeNegative = errors.New("must not be negative") errMustNotBeNegative = errors.New("must not be negative")
@@ -554,8 +554,8 @@ func (c *Config) ensureStateDirWritable() error {
} }
// validateSigningKey checks that the signing key is present, long // validateSigningKey checks that the signing key is present, long
// enough, and not the public placeholder from config.example.yml. The // enough, and not the public placeholder from configs/config.example.yml.
// key value itself is never echoed in error messages. // The key value itself is never echoed in error messages.
func (c *Config) validateSigningKey() error { func (c *Config) validateSigningKey() error {
if c.SigningKey == "" { if c.SigningKey == "" {
return fmt.Errorf("%s: %w", settingName(keySigningKey), errValueRequired) return fmt.Errorf("%s: %w", settingName(keySigningKey), errValueRequired)
-147
View File
@@ -1,147 +0,0 @@
#!/bin/bash
#
# Manual test script for pixa server
# Requires: server running on localhost:8080
#
set -e
BASE_URL="${BASE_URL:-http://localhost:8080}"
SIGNING_KEY="${SIGNING_KEY:-test-signing-key-for-development-only}"
TEST_IMAGE_URL="https://s3.sneak.cloud/sneak-public/2021/2021-04-18.untitled.a7r4.07723.jpg"
COOKIE_JAR=$(mktemp)
cleanup() {
rm -f "$COOKIE_JAR"
}
trap cleanup EXIT
pass() {
echo "✓ PASS: $1"
}
fail() {
echo "✗ FAIL: $1"
exit 1
}
echo "=== Pixa Manual Test Suite ==="
echo "Base URL: $BASE_URL"
echo ""
# Test 1: Healthcheck
echo "--- Test 1: Healthcheck endpoint ---"
HEALTH=$(curl -sf "$BASE_URL/.well-known/healthcheck.json")
if echo "$HEALTH" | grep -q '"status"'; then
pass "Healthcheck returns status"
else
fail "Healthcheck did not return expected response"
fi
# Test 2: Login page displays
echo "--- Test 2: Login page (GET /) ---"
LOGIN_PAGE=$(curl -sf "$BASE_URL/")
if echo "$LOGIN_PAGE" | grep -qi "password\|login\|sign"; then
pass "Login page displays password form"
else
fail "Login page did not display expected content"
fi
# Test 3: Wrong password shows error
echo "--- Test 3: Login with wrong password ---"
WRONG_LOGIN=$(curl -sf -X POST "$BASE_URL/" -d "key=wrong-key" -c "$COOKIE_JAR")
if echo "$WRONG_LOGIN" | grep -qi "invalid\|error\|incorrect\|wrong"; then
pass "Wrong password shows error message"
else
fail "Wrong password did not show error"
fi
# Test 4: Correct password redirects to generator
echo "--- Test 4: Login with correct signing key ---"
curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
GENERATOR_PAGE=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR")
if echo "$GENERATOR_PAGE" | grep -qi "generate\|url\|source\|logout"; then
pass "Correct password shows generator page"
else
fail "Generator page not displayed after login"
fi
# Test 5: Generate encrypted URL
echo "--- Test 5: Generate encrypted URL ---"
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "url=$TEST_IMAGE_URL" \
-d "width=800" \
-d "height=600" \
-d "format=jpeg" \
-d "quality=85" \
-d "fit=cover" \
-d "ttl=3600")
if echo "$GEN_RESULT" | grep -q "/v1/e/"; then
pass "Encrypted URL generated"
# Extract the encrypted URL
ENC_URL=$(echo "$GEN_RESULT" | grep -o '/v1/e/[^"<]*' | head -1)
echo " Generated URL: $ENC_URL"
else
fail "Failed to generate encrypted URL"
fi
# Test 6: Fetch image via encrypted URL
echo "--- Test 6: Fetch image via encrypted URL ---"
if [ -n "$ENC_URL" ]; then
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL$ENC_URL")
if [ "$HTTP_CODE" = "200" ]; then
pass "Encrypted URL returns image (HTTP 200)"
else
fail "Encrypted URL returned HTTP $HTTP_CODE"
fi
else
fail "No encrypted URL to test"
fi
# Test 7: Fetch image via allowlisted host (direct proxy)
echo "--- Test 7: Fetch image via direct proxy (allowlisted host) ---"
# URL format: /v1/image/<host>/<path>/<WxH>.<format>
PROXY_PATH="/v1/image/s3.sneak.cloud/sneak-public/2021/2021-04-18.untitled.a7r4.07723.jpg/400x300.jpeg"
HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" "$BASE_URL$PROXY_PATH")
if [ "$HTTP_CODE" = "200" ]; then
pass "Direct proxy returns image (HTTP 200)"
else
fail "Direct proxy returned HTTP $HTTP_CODE"
fi
# Test 8: Logout
echo "--- Test 8: Logout ---"
curl -sf "$BASE_URL/logout" -b "$COOKIE_JAR" -c "$COOKIE_JAR" -L -o /dev/null
AFTER_LOGOUT=$(curl -sf "$BASE_URL/" -b "$COOKIE_JAR")
if echo "$AFTER_LOGOUT" | grep -qi "password\|login"; then
pass "Logout redirects to login page"
else
fail "Logout did not redirect to login"
fi
# Test 9: Generate short-TTL URL and verify expiration
echo "--- Test 9: Expired URL returns 410 ---"
# Login again
curl -sf -X POST "$BASE_URL/" -d "key=$SIGNING_KEY" -c "$COOKIE_JAR" -b "$COOKIE_JAR" -L -o /dev/null
# Generate URL with 1 second TTL
GEN_RESULT=$(curl -sf -X POST "$BASE_URL/generate" -b "$COOKIE_JAR" \
-d "url=$TEST_IMAGE_URL" \
-d "width=100" \
-d "height=100" \
-d "format=jpeg" \
-d "ttl=1")
SHORT_URL=$(echo "$GEN_RESULT" | grep -o '/v1/e/[^"<]*' | head -1)
if [ -n "$SHORT_URL" ]; then
echo " Waiting 2 seconds for URL to expire..."
sleep 2
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$SHORT_URL")
if [ "$HTTP_CODE" = "410" ]; then
pass "Expired URL returns 410 Gone"
else
fail "Expired URL returned HTTP $HTTP_CODE (expected 410)"
fi
else
fail "Could not generate short-TTL URL"
fi
echo ""
echo "=== All tests passed! ==="