Bundle the remaining imaging hardening and the host/CI fixes from the
"Doe alles" pass into one release.
Furniture:
- Resolve the local bundle extension from FURNI_BUNDLE_EXT (".hab" default,
".nitro" for a stock Nitro client). The configured extension is written by
imports/repairs and preferred on lookup; the other stays a legacy fallback.
Imaging:
- Avatar proxy sends s-maxage=86400 + stale-while-revalidate (300/3600 for the
fallback path) so the edge can hold a render a day.
- Cloudflare cache rule now covers /api/imaging/ (verified MISS->HIT live).
- Document that bare /imaging is shadowed by the edge proxy to the renderer.
Ops/CI:
- docker-update.sh skips a CI-managed host (exit 0) instead of failing cron.
- ci-deploy.sh runs the cross-browser imaging gate (chromium+firefox+webkit)
against the candidate before cutover; DEPLOY_SKIP_CROSS_BROWSER=1 as escape.
- scripts/imager-health-alert.sh alerts (Discord) on imager up/down transitions;
runs from cron every 5 minutes.
Tests:
- Furniture bundle-ext resolver + FURNI_BUNDLE_EXT wiring.
- Imaging performance budget (bytes + warm latency).
- classifyDdos: /api/imaging/* stays in the api bucket.
132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
const CLASSNAME_CHAR_RE = /^[a-z0-9_*\-.]+$/;
|
|
|
|
export function getBaseClassname(classname: string): string {
|
|
const star = classname.indexOf("*");
|
|
return star >= 0 ? classname.substring(0, star) : classname;
|
|
}
|
|
|
|
export function getSafeClassnameStem(classname: string): string {
|
|
return classname.replace(/\*/g, "_");
|
|
}
|
|
|
|
export function getIconFileName(classname: string): string {
|
|
return `${getSafeClassnameStem(classname)}_icon.png`;
|
|
}
|
|
|
|
export function getSwfFileName(classname: string): string {
|
|
return `${getBaseClassname(classname)}.swf`;
|
|
}
|
|
|
|
/** The two extensions a furniture bundle may carry. */
|
|
export const BUNDLE_EXT_CHOICES = [".hab", ".nitro"] as const;
|
|
export type FurnitureBundleExt = (typeof BUNDLE_EXT_CHOICES)[number];
|
|
|
|
/**
|
|
* Read an env value via a computed key so Next does not inline it at build time
|
|
* — changing it should take effect on restart, not require a rebuild (same
|
|
* pattern as `runtime-asset-config.ts`).
|
|
*/
|
|
function readBundleExtEnv(): string | undefined {
|
|
const name = "FURNI_BUNDLE_EXT";
|
|
return process.env[name]?.trim() || undefined;
|
|
}
|
|
|
|
/**
|
|
* Resolve the local furniture-bundle extension this deployment's client asks
|
|
* for. Set `FURNI_BUNDLE_EXT` in `.env` to `hab`/`.hab` or `nitro`/`.nitro`;
|
|
* anything missing or unrecognised falls back to `.hab`.
|
|
*/
|
|
export function resolveLocalBundleExt(
|
|
raw: string | undefined = readBundleExtEnv(),
|
|
): FurnitureBundleExt {
|
|
const value = raw?.trim().toLowerCase();
|
|
if (!value) return ".hab";
|
|
const normalized = value.startsWith(".") ? value : `.${value}`;
|
|
return (BUNDLE_EXT_CHOICES as readonly string[]).includes(normalized)
|
|
? (normalized as FurnitureBundleExt)
|
|
: ".hab";
|
|
}
|
|
|
|
/**
|
|
* Extension locally stored furniture bundles are written with. This deployment's
|
|
* Nitro client requests `<classname>.hab` by default, but a stock Nitro client
|
|
* asks for `.nitro` — choose with `FURNI_BUNDLE_EXT`. New imports and repairs
|
|
* produce this extension.
|
|
*/
|
|
export const LOCAL_BUNDLE_EXT: FurnitureBundleExt = resolveLocalBundleExt();
|
|
|
|
/**
|
|
* Extension remote sources serve bundles with. Upstream (Habbo's CDN and every
|
|
* configured clone source) keeps shipping `.nitro`, so download URLs are built
|
|
* with this and only re-written to `LOCAL_BUNDLE_EXT` once they land on disk.
|
|
*/
|
|
export const SOURCE_BUNDLE_EXT = ".nitro" as const;
|
|
|
|
/**
|
|
* Every extension a local bundle may be stored under, the configured one first
|
|
* so it always wins over a same-stem legacy file.
|
|
*/
|
|
export const LOCAL_BUNDLE_EXTS: readonly FurnitureBundleExt[] = [
|
|
LOCAL_BUNDLE_EXT,
|
|
...BUNDLE_EXT_CHOICES.filter((ext) => ext !== LOCAL_BUNDLE_EXT),
|
|
];
|
|
|
|
/** Primary local bundle name — the configured extension wins over the legacy one. */
|
|
export function getNitroFileName(classname: string): string {
|
|
return `${getBaseClassname(classname)}${LOCAL_BUNDLE_EXT}`;
|
|
}
|
|
|
|
/** Every local file name this classname's bundle may be stored under. */
|
|
export function getLocalBundleFileNames(classname: string): string[] {
|
|
const base = getBaseClassname(classname);
|
|
return LOCAL_BUNDLE_EXTS.map((ext) => `${base}${ext}`);
|
|
}
|
|
|
|
/** File name for an explicit extension — use `SOURCE_BUNDLE_EXT` for URLs. */
|
|
export function getBundleFileName(
|
|
classname: string,
|
|
ext: string = LOCAL_BUNDLE_EXT,
|
|
): string {
|
|
return `${getBaseClassname(classname)}${ext}`;
|
|
}
|
|
|
|
export function normalizeClassname(raw: string): string {
|
|
return raw.trim().toLowerCase();
|
|
}
|
|
|
|
export function isValidClassname(classname: string): boolean {
|
|
if (!classname) return false;
|
|
return CLASSNAME_CHAR_RE.test(classname);
|
|
}
|
|
|
|
/**
|
|
* Asset-name safety check for values that become file names (swf / nitro / icon).
|
|
*
|
|
* The real rule is "cannot escape the directory": no path separators, so `../`
|
|
* is structurally impossible. Case matters here — classnames in gamedata are
|
|
* mixed-case (`recycler_kintsugiB`), so this must not be folded to lowercase the
|
|
* way `isValidClassname` presumes.
|
|
*/
|
|
const ASSET_NAME_RE = /^[\w*.-]+$/;
|
|
|
|
export function isSafeAssetName(name: string): boolean {
|
|
const trimmed = name.trim();
|
|
if (!trimmed) return false;
|
|
if (!ASSET_NAME_RE.test(trimmed)) return false;
|
|
return !trimmed.includes("..");
|
|
}
|
|
|
|
export const LOCAL_ICON_URL_PREFIX = "/swf/dcr/hof_furni/icons/";
|
|
|
|
export function getLocalIconUrl(classname: string): string {
|
|
return `${LOCAL_ICON_URL_PREFIX}${encodeURIComponent(getIconFileName(classname))}`;
|
|
}
|
|
|
|
export function getHabboCdnIconUrl(
|
|
classname: string,
|
|
revision: number,
|
|
): string {
|
|
const base = getBaseClassname(classname);
|
|
return `https://images.habbo.com/dcr/hof_furni/${revision}/${encodeURIComponent(base)}_icon.png`;
|
|
}
|