Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3e744a744 | ||
|
|
a31dbcb70a | ||
|
|
8f66a795ad | ||
|
|
d9fa7d9130 | ||
|
|
7d2a9a2a96 |
+1259
File diff suppressed because it is too large
Load Diff
@@ -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 configs/config.example.yml config.yml
|
cp 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
|
||||||
|
|
||||||
@@ -541,7 +541,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 `configs/config.example.yml` for all options with defaults.
|
See `config.example.yml` for all options with defaults.
|
||||||
|
|
||||||
### Architecture
|
### Architecture
|
||||||
|
|
||||||
|
|||||||
@@ -41,18 +41,8 @@ P2: security: per-IP rate limiting on the image routes
|
|||||||
the signature, the cache and the upstream fetch, so it fetches nothing and is
|
the signature, the cache and the upstream fetch, so it fetches nothing and is
|
||||||
refused whether or not the image is cached. A request with no `Referer`, or
|
refused whether or not the image is cached. A request with no `Referer`, or
|
||||||
one that does not parse as a URL with a host, is served, so the list is easily
|
one that does not parse as a URL with a host, is served, so the list is easily
|
||||||
got around; `README.md` and `configs/config.example.yml` say so. It does not
|
got around; `README.md` and `config.example.yml` say so. It does not apply to
|
||||||
apply to the login and generator pages.
|
the login and generator pages.
|
||||||
- 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
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// placeholderSigningKey is the dummy signing_key shipped in
|
// placeholderSigningKey is the dummy signing_key shipped in
|
||||||
// configs/config.example.yml. It is 45 characters, so it passes the length
|
// 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"
|
||||||
@@ -90,7 +90,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 configs/config.example.yml; " +
|
"is the placeholder from 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")
|
||||||
@@ -561,8 +561,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 configs/config.example.yml.
|
// enough, and not the public placeholder from config.example.yml. The
|
||||||
// The key value itself is never echoed in error messages.
|
// 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)
|
||||||
|
|||||||
Executable
+147
@@ -0,0 +1,147 @@
|
|||||||
|
#!/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! ==="
|
||||||
Reference in New Issue
Block a user