package main // The demo gates (demo.go): bucket derivation, adaptive difficulty, the // bits-sealed-in-the-challenge property, the cooldown semaphore, and the // endpoint boundary (mandatory PoW, cooldown ≥ TTL, kind=demo forwarding). import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" ) func newDemoTestServer(t *testing.T, cloudURL string) (*httptest.Server, *Server) { t.Helper() ts, s := newTestServer(t) gate, err := newDemoGate(4, s.keys) // 4 bits: solvable in microseconds if err != nil { t.Fatal(err) } s.demo = gate if cloudURL != "" { s.cloud = &cloudProvisioner{url: cloudURL, token: "caller-token", client: http.DefaultClient} } return ts, s } // postDemo hits a demo endpoint with a spoofed client IP (the router would // stamp X-Forwarded-For in production). func postDemo(t *testing.T, url, path, ip string, payload any) (*http.Response, map[string]any) { t.Helper() var body []byte if payload != nil { body, _ = json.Marshal(payload) } req, _ := http.NewRequest("POST", url+path, bytes.NewReader(body)) req.Header.Set("X-Forwarded-For", ip) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var m map[string]any _ = json.NewDecoder(resp.Body).Decode(&m) return resp, m } func TestDemoBuckets(t *testing.T) { gate, err := newDemoGate(4, nil) if err != nil { t.Fatal(err) } if gate.bucket("203.0.113.7") != gate.bucket("203.0.113.7") { t.Fatal("same IPv4 must map to the same bucket") } if gate.bucket("203.0.113.7") == gate.bucket("203.0.113.8") { t.Fatal("distinct IPv4s must map to distinct buckets") } // Two addresses inside one /56 delegation share a bucket… if gate.bucket("2001:db8:1:200::1") != gate.bucket("2001:db8:1:2ff::9") { t.Fatal("IPv6 addresses in one /56 must share a bucket") } // …and a different /56 doesn't. if gate.bucket("2001:db8:1:200::1") == gate.bucket("2001:db8:1:300::1") { t.Fatal("distinct /56s must not share a bucket") } // A fresh gate (new process) buckets differently: nothing links restarts. gate2, _ := newDemoGate(4, nil) if gate.bucket("203.0.113.7") == gate2.bucket("203.0.113.7") { t.Fatal("buckets must not be stable across processes") } } func TestDemoBitsEscalation(t *testing.T) { for _, tc := range []struct{ recent, want int }{ {0, 18}, {4, 18}, {5, 19}, {9, 19}, {10, 20}, {19, 20}, {20, 21}, {40, 22}, } { if got := demoBits(18, tc.recent); got != tc.want { t.Fatalf("demoBits(18, %d) = %d, want %d", tc.recent, got, tc.want) } } } func TestDemoGateVerify(t *testing.T) { _, s := newTestServer(t) gate, err := newDemoGate(4, s.keys) if err != nil { t.Fatal(err) } bucket := gate.bucket("203.0.113.7") challenge, err := gate.mint(bucket, 4, time.Now().Add(time.Minute)) if err != nil { t.Fatal(err) } nonce := solvePow(challenge, 4) if err := gate.verify(gate.bucket("198.51.100.1"), challenge, nonce); err == nil { t.Fatal("another bucket's solution must not verify") } if err := gate.verify(bucket, challenge, nonce); err != nil { t.Fatalf("honest solution rejected: %v", err) } if err := gate.verify(bucket, challenge, nonce); err == nil { t.Fatal("replay must not verify") } // The difficulty rides INSIDE the challenge: a challenge minted at 12 bits // is checked at 12 bits even though the gate's base is 4. hard, _ := gate.mint(bucket, 12, time.Now().Add(time.Minute)) weak := solvePow(hard, 4) if powSolves(hard, weak, 12) { t.Skip("lucky nonce cleared 12 bits; nothing to assert") } if err := gate.verify(bucket, hard, weak); err == nil { t.Fatal("an under-difficulty nonce must not verify against the sealed bits") } // Expired challenges die. stale, _ := gate.mint(bucket, 4, time.Now().Add(-time.Minute)) if err := gate.verify(bucket, stale, solvePow(stale, 4)); err == nil { t.Fatal("expired challenge must not verify") } } func TestDemoEndpointsDisabled(t *testing.T) { ts, _ := newTestServer(t) // no demo gate, no cloud for _, path := range []string{"/api/demo/challenge", "/api/demo"} { resp, body := postDemo(t, ts.URL, path, "203.0.113.7", nil) if resp.StatusCode != http.StatusNotImplemented || body["error"] != "unavailable" { t.Fatalf("%s while disabled: HTTP %d %v", path, resp.StatusCode, body) } } } func TestDemoCreateFlow(t *testing.T) { cloud, captured, capturedBody := fakeCloud(t, http.StatusCreated, map[string]any{ "host": "8oe.11ch.at", "url": "https://8oe.11ch.at", "setup_link": "https://8oe.11ch.at/join/setup#T", "kind": "demo", "expires_at": time.Now().Add(24 * time.Hour).Unix(), }) ts, _ := newDemoTestServer(t, cloud.URL) // PoW is mandatory: no solution, no demo. resp, body := postDemo(t, ts.URL, "/api/demo", "203.0.113.7", nil) if resp.StatusCode != http.StatusForbidden || body["error"] != "pow_required" { t.Fatalf("create without pow: HTTP %d %v", resp.StatusCode, body) } // Challenge → solve → create. resp, body = postDemo(t, ts.URL, "/api/demo/challenge", "203.0.113.7", nil) if resp.StatusCode != http.StatusOK { t.Fatalf("challenge: HTTP %d %v", resp.StatusCode, body) } challenge, _ := body["challenge"].(string) bits := int(body["bits"].(float64)) resp, body = postDemo(t, ts.URL, "/api/demo", "203.0.113.7", map[string]string{"challenge": challenge, "nonce": solvePow(challenge, bits)}) if resp.StatusCode != http.StatusCreated || body["host"] != "8oe.11ch.at" { t.Fatalf("create: HTTP %d %v", resp.StatusCode, body) } // Home forwarded kind=demo with an anonymous one-shot requester. var sent map[string]string if err := json.Unmarshal(*capturedBody, &sent); err != nil { t.Fatal(err) } if sent["kind"] != "demo" || len(sent["requester"]) < 10 { t.Fatalf("forwarded body = %v", sent) } if (*captured).URL.Path != "/api/instances" { t.Fatalf("forwarded path = %s", (*captured).URL.Path) } // The bucket is now in cooldown: same connection can't mint or create. resp, body = postDemo(t, ts.URL, "/api/demo/challenge", "203.0.113.7", nil) if resp.StatusCode != http.StatusForbidden || body["error"] != "demo_cooldown" { t.Fatalf("challenge during cooldown: HTTP %d %v", resp.StatusCode, body) } if _, ok := body["retry_at"].(float64); !ok { t.Fatalf("cooldown carries no retry_at: %v", body) } resp, body = postDemo(t, ts.URL, "/api/demo", "203.0.113.7", map[string]string{"challenge": challenge, "nonce": "0"}) if resp.StatusCode != http.StatusForbidden || body["error"] != "demo_cooldown" { t.Fatalf("create during cooldown: HTTP %d %v", resp.StatusCode, body) } // A different connection is unaffected. if resp, body = postDemo(t, ts.URL, "/api/demo/challenge", "198.51.100.1", nil); resp.StatusCode != http.StatusOK { t.Fatalf("other bucket's challenge: HTTP %d %v", resp.StatusCode, body) } } // A failed cloud create must NOT burn the bucket's cooldown. func TestDemoFailedCreateKeepsBucketFree(t *testing.T) { cloud, _, _ := fakeCloud(t, http.StatusServiceUnavailable, map[string]any{ "error": "demo_busy", "message": "every demo slot is taken right now", }) ts, _ := newDemoTestServer(t, cloud.URL) _, body := postDemo(t, ts.URL, "/api/demo/challenge", "203.0.113.7", nil) challenge, _ := body["challenge"].(string) resp, body := postDemo(t, ts.URL, "/api/demo", "203.0.113.7", map[string]string{"challenge": challenge, "nonce": solvePow(challenge, 4)}) if resp.StatusCode != http.StatusServiceUnavailable || body["error"] != "demo_busy" { t.Fatalf("busy relay: HTTP %d %v", resp.StatusCode, body) } // No cooldown recorded — the next challenge mints fine. if resp, body = postDemo(t, ts.URL, "/api/demo/challenge", "203.0.113.7", nil); resp.StatusCode != http.StatusOK { t.Fatalf("challenge after failed create: HTTP %d %v", resp.StatusCode, body) } } func TestDemoCooldownIsExpiryPlusTTL(t *testing.T) { gate, err := newDemoGate(4, nil) if err != nil { t.Fatal(err) } now := time.Unix(2_000_000_000, 0) expires := now.Add(24 * time.Hour).Unix() gate.record("bucket-a", now, expires) until := gate.cooldownUntil("bucket-a", now) if want := expires + 24*3600; until != want { t.Fatalf("cooldown_until = %d, want expiry+TTL = %d", until, want) } // After the cooldown passes, the bucket frees (prune path). if got := gate.cooldownUntil("bucket-a", time.Unix(until+1, 0)); got != 0 { t.Fatalf("bucket still blocked after cooldown: %d", got) } } func TestDemoFeaturesFlag(t *testing.T) { cloud, _, _ := fakeCloud(t, http.StatusCreated, map[string]any{}) ts, _ := newDemoTestServer(t, cloud.URL) resp, err := http.Get(ts.URL + "/api/features") if err != nil { t.Fatal(err) } defer resp.Body.Close() var m map[string]any _ = json.NewDecoder(resp.Body).Decode(&m) if m["demo"] != true { t.Fatalf("features = %v, want demo true", m) } } // ---- the claim relay --------------------------------------------------------- func TestClaimRelay(t *testing.T) { cloud, captured, capturedBody := fakeCloud(t, http.StatusOK, map[string]any{ "host": "8oe.11ch.at", "url": "https://8oe.11ch.at", "entitlement": "free", }) ts, s := newDemoTestServer(t, cloud.URL) uid, linkToken := seedUser(t, s) post := func(token string, payload any) (*http.Response, map[string]any) { t.Helper() body, _ := json.Marshal(payload) req, _ := http.NewRequest("POST", ts.URL+"/api/spaces/claim", bytes.NewReader(body)) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) } defer resp.Body.Close() var m map[string]any _ = json.NewDecoder(resp.Body).Decode(&m) return resp, m } // Auth required; host required. if resp, _ := post("", map[string]string{"host": "8oe.11ch.at"}); resp.StatusCode != http.StatusUnauthorized { t.Fatalf("no token: HTTP %d", resp.StatusCode) } if resp, body := post(linkToken, map[string]string{}); resp.StatusCode != http.StatusBadRequest || body["error"] != "bad_request" { t.Fatalf("no host: HTTP %d %v", resp.StatusCode, body) } // Happy path: home forwards {requester: uid} to the cloud's claim route. resp, body := post(linkToken, map[string]string{"host": "8oe.11ch.at"}) if resp.StatusCode != http.StatusOK || body["entitlement"] != "free" { t.Fatalf("claim: HTTP %d %v", resp.StatusCode, body) } if (*captured).URL.Path != "/api/instances/8oe.11ch.at/claim" { t.Fatalf("forwarded path = %s", (*captured).URL.Path) } var sent map[string]string if err := json.Unmarshal(*capturedBody, &sent); err != nil { t.Fatal(err) } if sent["requester"] != uid { t.Fatalf("forwarded requester = %q, want the signed-in uid", sent["requester"]) } // Stable refusals relay verbatim; the unexpected flattens. gone, _, _ := fakeCloud(t, http.StatusGone, map[string]any{"error": "demo_expired", "message": "this demo's 24 hours are up"}) s.cloud = &cloudProvisioner{url: gone.URL, token: "caller-token", client: http.DefaultClient} if resp, body := post(linkToken, map[string]string{"host": "x.11ch.at"}); resp.StatusCode != http.StatusGone || body["error"] != "demo_expired" { t.Fatalf("expired relay: HTTP %d %v", resp.StatusCode, body) } weird, _, _ := fakeCloud(t, http.StatusTeapot, map[string]any{"error": "internal-detail"}) s.cloud = &cloudProvisioner{url: weird.URL, token: "caller-token", client: http.DefaultClient} if resp, body := post(linkToken, map[string]string{"host": "x.11ch.at"}); resp.StatusCode != http.StatusBadGateway || body["error"] != "claim_failed" { t.Fatalf("weird relay: HTTP %d %v", resp.StatusCode, body) } } // ---- the countdown banner's live status read ----------------------------------- func TestDemoStatusRelay(t *testing.T) { cloud, captured, _ := fakeCloud(t, http.StatusOK, map[string]any{ "demo": true, "expires_at": 1999999999, "internal": "never-relayed", }) ts, _ := newDemoTestServer(t, cloud.URL) get := func(query string) (*http.Response, map[string]any) { t.Helper() resp, err := http.Get(ts.URL + "/api/spaces/demo-status" + query) if err != nil { t.Fatal(err) } defer resp.Body.Close() var m map[string]any _ = json.NewDecoder(resp.Body).Decode(&m) return resp, m } // No auth needed; host required. if resp, body := get(""); resp.StatusCode != http.StatusBadRequest || body["error"] != "bad_request" { t.Fatalf("no host: HTTP %d %v", resp.StatusCode, body) } // Happy path: re-shaped to exactly the banner's two fields, CORS-open (the // banner fetches from the demo space's own origin), forwarded to the // cloud's per-host route. resp, body := get("?host=8OE.11ch.at") if resp.StatusCode != http.StatusOK || body["demo"] != true { t.Fatalf("status: HTTP %d %v", resp.StatusCode, body) } if exp, _ := body["expires_at"].(float64); int64(exp) != 1999999999 { t.Fatalf("expires_at = %v", body["expires_at"]) } if _, leaked := body["internal"]; leaked { t.Fatalf("relayed a field beyond the banner's two: %v", body) } if resp.Header.Get("Access-Control-Allow-Origin") != "*" { t.Fatal("missing CORS header — the banner's cross-origin fetch would fail") } if (*captured).URL.Path != "/api/instances/8oe.11ch.at/demo-status" { t.Fatalf("forwarded path = %s (host not lowercased?)", (*captured).URL.Path) } // Repeats inside the cache window never re-hit the provisioner (the read // auto-fires per tab load/refocus) — and the endpoint sits outside the // shared ceremony limiter, so a burst can't 429 (drained tokens would // starve sign-in, since the router funnels every caller into one bucket). ts4, s := newDemoTestServer(t, cloud.URL) s.limiter = newRateLimiter(0, 0) // every rateLimited endpoint would 429 for i := 0; i < 3; i++ { if resp, body := getFrom(t, ts4, "/api/spaces/demo-status?host=cached.11ch.at"); resp.StatusCode != http.StatusOK || body["demo"] != true { t.Fatalf("read %d: HTTP %d %v", i, resp.StatusCode, body) } } cloud.Close() // cached answers must not need the upstream at all if resp, body := getFrom(t, ts4, "/api/spaces/demo-status?host=cached.11ch.at"); resp.StatusCode != http.StatusOK || body["demo"] != true { t.Fatalf("cached read after upstream gone: HTTP %d %v", resp.StatusCode, body) } // A non-demo answer carries no expiry even if the upstream sent one. claimed, _, _ := fakeCloud(t, http.StatusOK, map[string]any{"demo": false, "expires_at": 123}) ts2, _ := newDemoTestServer(t, claimed.URL) resp, body = getFrom(t, ts2, "/api/spaces/demo-status?host=x.11ch.at") if resp.StatusCode != http.StatusOK || body["demo"] != false { t.Fatalf("claimed status: HTTP %d %v", resp.StatusCode, body) } if _, present := body["expires_at"]; present { t.Fatalf("claimed status leaked an expiry: %v", body) } // No provisioner wired = no demos, quietly. ts3, _ := newDemoTestServer(t, "") resp, body = getFrom(t, ts3, "/api/spaces/demo-status?host=x.11ch.at") if resp.StatusCode != http.StatusOK || body["demo"] != false { t.Fatalf("no-cloud status: HTTP %d %v", resp.StatusCode, body) } } // getFrom fetches a JSON GET off one of the harness servers. func getFrom(t *testing.T, ts *httptest.Server, path string) (*http.Response, map[string]any) { t.Helper() resp, err := http.Get(ts.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() var m map[string]any _ = json.NewDecoder(resp.Body).Decode(&m) return resp, m }