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).
|
# Expiration for each blocklist decision (re-synced keeps them fresh).
|
||||||
#CROWDSEC_BLOCKLIST_DURATION=24h
|
#CROWDSEC_BLOCKLIST_DURATION=24h
|
||||||
# Combined cap per sync (safety valve against excessive decisions).
|
# 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).
|
# Comma-separated IPs/CIDRs that a sync must always skip (allowlist).
|
||||||
#CROWDSEC_BLOCKLIST_ALLOW=1.2.3.4,10.0.0.0/8
|
#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,
|
Defaults: Spamhaus DROP/EDROP, DShield, CINS, Greensnow, StopForumSpam,
|
||||||
Binary Defense, blocklist.de, Emerging Threats, BruteForceBlocker, abuse.ch
|
Binary Defense, blocklist.de, Emerging Threats, BruteForceBlocker, abuse.ch
|
||||||
Feodo/SSLBL, Botvrij, IPsum, Firehol ipsets and Tor exit nodes
|
Feodo/SSLBL, Darklist, Botvrij, IPsum, Firehol ipsets and Tor exit nodes
|
||||||
(25 sources). URLhaus was removed because its `text_online` feed lists URLs,
|
(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
|
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,
|
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
|
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://feodotracker.abuse.ch/downloads/ipblocklist.txt"
|
||||||
"https://sslbl.abuse.ch/blacklist/sslipblacklist.txt"
|
"https://sslbl.abuse.ch/blacklist/sslipblacklist.txt"
|
||||||
"https://www.botvrij.eu/data/ioclist.ip-dst.raw"
|
"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
|
# Aggregated threat intel
|
||||||
"https://raw.githubusercontent.com/stamparm/ipsum/master/levels/3.txt"
|
"https://raw.githubusercontent.com/stamparm/ipsum/master/levels/3.txt"
|
||||||
"https://raw.githubusercontent.com/stamparm/ipsum/master/levels/2.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
|
fi
|
||||||
|
|
||||||
cutover_started=1
|
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.
|
# 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.
|
# Preserve each before releasing the shared host port; never kill an arbitrary PID.
|
||||||
if [ -n "$secondary_name" ]; then
|
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 HABBO_PUBLIC_UPSTREAM = "https://www.habbo.com/habbo-imaging/avatarimage";
|
||||||
|
|
||||||
const PRIMARY_TIMEOUT_MS = 4_000;
|
// Effect renders (the client asks for effect=14 by default) are encoded as
|
||||||
const FALLBACK_TIMEOUT_MS = 4_000;
|
// 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 =
|
export type ImagerSource =
|
||||||
| "primary"
|
| "primary"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { getAvatarUrl } from "./imager";
|
import { getAvatarUrl } from "./imager";
|
||||||
|
import { FALLBACK_TIMEOUT_MS, PRIMARY_TIMEOUT_MS } from "./imager-upstream";
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllEnvs();
|
vi.unstubAllEnvs();
|
||||||
@@ -62,3 +63,13 @@ describe("getAvatarUrl", () => {
|
|||||||
expect(url).toContain("effect=14");
|
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
|
// @ts-nocheck
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { shouldRedirectAdminRequest } from "./proxy-access";
|
import {
|
||||||
|
isCacheableAssetPath,
|
||||||
|
shouldRedirectAdminRequest,
|
||||||
|
} from "./proxy-access";
|
||||||
|
|
||||||
describe("shouldRedirectAdminRequest", () => {
|
describe("shouldRedirectAdminRequest", () => {
|
||||||
it("redirects anonymous admin requests before rendering", () => {
|
it("redirects anonymous admin requests before rendering", () => {
|
||||||
@@ -17,3 +20,20 @@ describe("shouldRedirectAdminRequest", () => {
|
|||||||
expect(shouldRedirectAdminRequest("/news", null)).toBe(false);
|
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;
|
if (pathname !== "/admin" && !pathname.startsWith("/admin/")) return false;
|
||||||
return token === null;
|
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 { env } from "@/env";
|
||||||
import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp";
|
import { buildContentSecurityPolicy, createCspNonce } from "@/lib/csp";
|
||||||
import { ddosReject, enforceDdosRateLimit } from "@/lib/ddos-guard";
|
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> = {
|
const SECURITY_HEADERS: Record<string, string> = {
|
||||||
"X-Content-Type-Options": "nosniff",
|
"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
|
// always references the current build's chunks. Static assets are
|
||||||
// content-hashed + immutable and can be cached aggressively; a stale HTML
|
// content-hashed + immutable and can be cached aggressively; a stale HTML
|
||||||
// document would reference chunk URLs that no longer exist after a rebuild.
|
// document would reference chunk URLs that no longer exist after a rebuild.
|
||||||
response.headers.set(
|
// Rendered avatars and uploaded media are immutable per key and already
|
||||||
"Cache-Control",
|
// carry their own long-lived Cache-Control, so they keep it here.
|
||||||
"private, no-cache, no-store, max-age=0, must-revalidate",
|
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)) {
|
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
||||||
response.headers.set(key, value);
|
response.headers.set(key, value);
|
||||||
|
|||||||
Reference in new issue
Block a user