Compare commits
2
Commits
7f6febf906
...
fe5a7a6185
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe5a7a6185 | ||
|
|
8486ac4053 |
No files matched your search
+1
-1
@@ -135,7 +135,7 @@ CROWDSEC_LAPI_API_KEY=
|
||||
# Expiration for each blocklist decision (re-synced keeps them fresh).
|
||||
#CROWDSEC_BLOCKLIST_DURATION=24h
|
||||
# Combined cap per sync (safety valve against excessive decisions).
|
||||
#CROWDSEC_BLOCKLIST_MAX_DECISIONS=250000
|
||||
#CROWDSEC_BLOCKLIST_MAX_DECISIONS=1000000
|
||||
# Comma-separated IPs/CIDRs that a sync must always skip (allowlist).
|
||||
#CROWDSEC_BLOCKLIST_ALLOW=1.2.3.4,10.0.0.0/8
|
||||
|
||||
|
||||
@@ -848,8 +848,8 @@ bash cms security blocklists
|
||||
|
||||
Defaults: Spamhaus DROP/EDROP, DShield, CINS, Greensnow, StopForumSpam,
|
||||
Binary Defense, blocklist.de, Emerging Threats, BruteForceBlocker, abuse.ch
|
||||
Feodo/SSLBL, Botvrij, IPsum, Firehol ipsets and Tor exit nodes
|
||||
(25 sources). URLhaus was removed because its `text_online` feed lists URLs,
|
||||
Feodo/SSLBL, Darklist, Botvrij, IPsum, Firehol ipsets and Tor exit nodes
|
||||
(26 sources). URLhaus was removed because its `text_online` feed lists URLs,
|
||||
not IPs; a malformed token in it could otherwise expand into a bogus
|
||||
huge CIDR. The validator only accepts whole-line bare IPs or proper CIDRs,
|
||||
enforces sane prefix bounds and drops reserved/private/loopback space, so a
|
||||
|
||||
@@ -27,6 +27,8 @@ DEFAULT_SOURCES=(
|
||||
"https://feodotracker.abuse.ch/downloads/ipblocklist.txt"
|
||||
"https://sslbl.abuse.ch/blacklist/sslipblacklist.txt"
|
||||
"https://www.botvrij.eu/data/ioclist.ip-dst.raw"
|
||||
# Live SSH/spam attackers (last 48h), bare IPs only
|
||||
"https://www.darklist.de/raw.php"
|
||||
# Aggregated threat intel
|
||||
"https://raw.githubusercontent.com/stamparm/ipsum/master/levels/3.txt"
|
||||
"https://raw.githubusercontent.com/stamparm/ipsum/master/levels/2.txt"
|
||||
|
||||
@@ -146,6 +146,15 @@ if [ "$previous_name" != epicnext-cms-app ] && [ "$secondary_name" != epicnext-c
|
||||
fi
|
||||
|
||||
cutover_started=1
|
||||
# The avatar/badge disk cache lives on the host bind and is written by uid 33
|
||||
# inside the container. Root-owned directories make every cache write fail
|
||||
# silently, which turns each avatar into a fresh live render.
|
||||
for cache_dir in avatars badges; do
|
||||
if ! install -d -o 33 -g 33 -m 0750 "$deploy_dir/storage/imaging/$cache_dir" 2>/dev/null; then
|
||||
mkdir -p "$deploy_dir/storage/imaging/$cache_dir" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
chown -R 33:33 "$deploy_dir/storage/imaging" 2>/dev/null || true
|
||||
# Both legacy Compose and CI containers can exist after earlier failed updates.
|
||||
# Preserve each before releasing the shared host port; never kill an arbitrary PID.
|
||||
if [ -n "$secondary_name" ]; then
|
||||
|
||||
@@ -14,8 +14,13 @@ export const FIGURE_MAX_PARTS = 24;
|
||||
|
||||
const HABBO_PUBLIC_UPSTREAM = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||
|
||||
const PRIMARY_TIMEOUT_MS = 4_000;
|
||||
const FALLBACK_TIMEOUT_MS = 4_000;
|
||||
// Effect renders (the client asks for effect=14 by default) are encoded as
|
||||
// APNG animations and consistently need a little over 4s, so the primary
|
||||
// budget has to stay well above that or every effected avatar times out. The
|
||||
// public Habbo renderer is a best-effort safety net only: it is slow to fail,
|
||||
// so it gets a short budget and the disk cache absorbs the difference.
|
||||
export const PRIMARY_TIMEOUT_MS = 8_000;
|
||||
export const FALLBACK_TIMEOUT_MS = 2_000;
|
||||
|
||||
export type ImagerSource =
|
||||
| "primary"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { getAvatarUrl } from "./imager";
|
||||
import { FALLBACK_TIMEOUT_MS, PRIMARY_TIMEOUT_MS } from "./imager-upstream";
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
@@ -62,3 +63,13 @@ describe("getAvatarUrl", () => {
|
||||
expect(url).toContain("effect=14");
|
||||
});
|
||||
});
|
||||
|
||||
describe("upstream render budgets", () => {
|
||||
it("keeps enough headroom for APNG effect renders", () => {
|
||||
expect(PRIMARY_TIMEOUT_MS).toBeGreaterThanOrEqual(8_000);
|
||||
});
|
||||
|
||||
it("does not stack a long fallback wait on a slow primary", () => {
|
||||
expect(FALLBACK_TIMEOUT_MS).toBeLessThan(PRIMARY_TIMEOUT_MS);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
// @ts-nocheck
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { shouldRedirectAdminRequest } from "./proxy-access";
|
||||
import {
|
||||
isCacheableAssetPath,
|
||||
shouldRedirectAdminRequest,
|
||||
} from "./proxy-access";
|
||||
|
||||
describe("shouldRedirectAdminRequest", () => {
|
||||
it("redirects anonymous admin requests before rendering", () => {
|
||||
@@ -17,3 +20,20 @@ describe("shouldRedirectAdminRequest", () => {
|
||||
expect(shouldRedirectAdminRequest("/news", null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isCacheableAssetPath", () => {
|
||||
it("keeps rendered avatars and uploaded media cacheable", () => {
|
||||
expect(isCacheableAssetPath("/api/imaging/avatar")).toBe(true);
|
||||
expect(isCacheableAssetPath("/api/imaging/badge")).toBe(true);
|
||||
expect(isCacheableAssetPath("/api/media/1730000000000-abc.png")).toBe(true);
|
||||
});
|
||||
|
||||
it("never lets a document or API route reuse the asset cache policy", () => {
|
||||
expect(isCacheableAssetPath("/")).toBe(false);
|
||||
expect(isCacheableAssetPath("/news")).toBe(false);
|
||||
expect(isCacheableAssetPath("/api/news")).toBe(false);
|
||||
expect(isCacheableAssetPath("/api/media-library")).toBe(false);
|
||||
expect(isCacheableAssetPath("/api/imaging")).toBe(false);
|
||||
expect(isCacheableAssetPath("/admin/media")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,3 +10,9 @@ export function shouldRedirectAdminRequest(
|
||||
if (pathname !== "/admin" && !pathname.startsWith("/admin/")) return false;
|
||||
return token === null;
|
||||
}
|
||||
|
||||
const CACHEABLE_ASSET_PREFIXES = ["/api/imaging/", "/api/media/"];
|
||||
|
||||
export function isCacheableAssetPath(pathname: string): boolean {
|
||||
return CACHEABLE_ASSET_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
||||
}
|
||||
+12
-5
@@ -3,7 +3,10 @@ import { getToken } from "next-auth/jwt";
|
||||
import { env } from "@/env";
|
||||
import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp";
|
||||
import { ddosReject, enforceDdosRateLimit } from "@/lib/ddos-guard";
|
||||
import { shouldRedirectAdminRequest } from "@/lib/proxy-access";
|
||||
import {
|
||||
isCacheableAssetPath,
|
||||
shouldRedirectAdminRequest,
|
||||
} from "@/lib/proxy-access";
|
||||
|
||||
const SECURITY_HEADERS: Record<string, string> = {
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
@@ -56,10 +59,14 @@ export const proxy = async (req: import("next/server").NextRequest) => {
|
||||
// always references the current build's chunks. Static assets are
|
||||
// content-hashed + immutable and can be cached aggressively; a stale HTML
|
||||
// document would reference chunk URLs that no longer exist after a rebuild.
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"private, no-cache, no-store, max-age=0, must-revalidate",
|
||||
);
|
||||
// Rendered avatars and uploaded media are immutable per key and already
|
||||
// carry their own long-lived Cache-Control, so they keep it here.
|
||||
if (!isCacheableAssetPath(pathname)) {
|
||||
response.headers.set(
|
||||
"Cache-Control",
|
||||
"private, no-cache, no-store, max-age=0, must-revalidate",
|
||||
);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
||||
response.headers.set(key, value);
|
||||
|
||||
Reference in new issue
Block a user