feat: browser headers on audit fetches + verified clone furnidata sources
This commit is contained in:
1 parent
080dc34e23
commit
2fba923a3a
8 files changed
+97
-32
No files matched your search
@@ -602,6 +602,7 @@ export async function runCatalogAudit(
|
||||
|
||||
const sources = await listSources();
|
||||
for (const source of sources) {
|
||||
if (!source.furnidataUrl) continue;
|
||||
try {
|
||||
const entries = await fetchSourceFurnidata(source.furnidataUrl);
|
||||
for (const entry of entries) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ensureDirectories,
|
||||
getOrCreateCategoryPage,
|
||||
} from "@/lib/services/furni-import";
|
||||
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
||||
import { downloadFile } from "@/lib/services/import/core/download";
|
||||
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
||||
|
||||
@@ -87,7 +88,10 @@ export async function fetchSourceFurnidata(
|
||||
): Promise<SourceFurni[]> {
|
||||
const hit = cache.get(url);
|
||||
if (hit && now && now - hit.ts < TTL) return hit.list;
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
||||
const res = await fetch(url, {
|
||||
signal: AbortSignal.timeout(30000),
|
||||
headers: browserHeaders(),
|
||||
});
|
||||
if (!res.ok) throw new Error(`furnidata fetch failed: ${res.status} ${url}`);
|
||||
let list: SourceFurni[];
|
||||
try {
|
||||
|
||||
@@ -11,47 +11,50 @@ export interface CloneSource {
|
||||
|
||||
const KEY = "clone_sources";
|
||||
|
||||
export const DEFAULT_ICON_SOURCES: CloneSource[] = [
|
||||
/**
|
||||
* Verified working sources (checked Aug 2026). Used as the default list and
|
||||
* as the fallback when the `clone_sources` site setting is empty.
|
||||
*/
|
||||
export const DEFAULT_SOURCES: CloneSource[] = [
|
||||
{
|
||||
id: "default-habboassets",
|
||||
name: "HabboAssets",
|
||||
furnidataUrl: "",
|
||||
id: "default-habbo",
|
||||
name: "Habbo",
|
||||
furnidataUrl:
|
||||
"https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json",
|
||||
nitroBaseUrl: "",
|
||||
iconBaseUrl: "https://www.habboassets.com/assets/furniture",
|
||||
iconBaseUrl: "",
|
||||
},
|
||||
{
|
||||
id: "default-hubbly",
|
||||
name: "Hubbly",
|
||||
furnidataUrl: "",
|
||||
nitroBaseUrl: "",
|
||||
iconBaseUrl: "https://hubbly.pw/swf/dcr/hof_furni/icons",
|
||||
},
|
||||
{
|
||||
id: "default-leet",
|
||||
name: "Leet",
|
||||
furnidataUrl: "",
|
||||
nitroBaseUrl: "",
|
||||
iconBaseUrl: "https://images.leet.city/library/hof_furni/icons",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_NITRO_SOURCES: CloneSource[] = [
|
||||
{
|
||||
id: "default-wibbo",
|
||||
name: "Wibbo",
|
||||
furnidataUrl: "",
|
||||
furnidataUrl: "https://assets.wibbo.org/gamedata/FurnitureData.json",
|
||||
nitroBaseUrl: "https://assets.wibbo.org/bundled/furniture",
|
||||
iconBaseUrl: "https://assets.wibbo.org/icons",
|
||||
},
|
||||
{
|
||||
id: "default-hubba",
|
||||
name: "Hubba.cc",
|
||||
furnidataUrl: "https://nitro.hubba.cc/gamedata/FurnitureData.json",
|
||||
nitroBaseUrl: "https://nitro.hubba.cc/bundled/furniture",
|
||||
iconBaseUrl: "",
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_ICON_SOURCES: CloneSource[] = DEFAULT_SOURCES.filter(
|
||||
(s) => s.iconBaseUrl,
|
||||
);
|
||||
|
||||
export const DEFAULT_NITRO_SOURCES: CloneSource[] = DEFAULT_SOURCES.filter(
|
||||
(s) => s.nitroBaseUrl,
|
||||
);
|
||||
|
||||
export async function listSources(): Promise<CloneSource[]> {
|
||||
const raw = (await siteSettings.get(KEY, "[]")) ?? "[]";
|
||||
try {
|
||||
const arr = JSON.parse(raw);
|
||||
return Array.isArray(arr) ? arr : [];
|
||||
return Array.isArray(arr) && arr.length > 0 ? arr : DEFAULT_SOURCES;
|
||||
} catch {
|
||||
return [];
|
||||
return DEFAULT_SOURCES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,11 @@ describe("official Habbo furnidata cache", () => {
|
||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"https://www.habbo.nl/gamedata/furnidata_json/1",
|
||||
expect.objectContaining({ headers: { Accept: "application/json" } }),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Accept: expect.stringContaining("application/json"),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/lib/habbo-gamedata-hotel";
|
||||
import { logger } from "@/lib/logger";
|
||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
||||
import type { OfficialHabboFurniEntry } from "@/types/furni";
|
||||
|
||||
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
||||
@@ -42,7 +43,7 @@ export async function getOfficialHabboFurnidata(): Promise<
|
||||
console.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
||||
const res = await fetch(url, {
|
||||
signal: AbortSignal.timeout(20000),
|
||||
headers: { Accept: "application/json" },
|
||||
headers: browserHeaders(),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
export type BrowserFetchDest =
|
||||
| "document"
|
||||
| "image"
|
||||
| "script"
|
||||
| "style"
|
||||
| "empty";
|
||||
|
||||
const CHROME_UA =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
||||
|
||||
const SEC_CH_UA =
|
||||
'"Google Chrome";v="125", "Chromium";v="125", "Not.A/Brand";v="24"';
|
||||
|
||||
/**
|
||||
* Realistic browser request headers so CDNs (Cloudflare, etc.) treat the
|
||||
* server-side fetches as a normal browser instead of a bot/curl and block us.
|
||||
*/
|
||||
export function browserHeaders(
|
||||
dest: BrowserFetchDest = "empty",
|
||||
extra: Record<string, string> = {},
|
||||
): Record<string, string> {
|
||||
const accept =
|
||||
dest === "document"
|
||||
? "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
|
||||
: dest === "image"
|
||||
? "image/avif,image/webp,image/apng,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5"
|
||||
: dest === "script"
|
||||
? "*/*"
|
||||
: "application/json,text/plain,*/*;q=0.9";
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": CHROME_UA,
|
||||
"sec-ch-ua": SEC_CH_UA,
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
Accept: accept,
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Sec-Fetch-Site": "cross-site",
|
||||
"Sec-Fetch-Mode":
|
||||
dest === "document" ? "navigate" : dest === "image" ? "no-cors" : "cors",
|
||||
"Sec-Fetch-Dest": dest,
|
||||
"Cache-Control": "no-cache",
|
||||
Pragma: "no-cache",
|
||||
};
|
||||
|
||||
if (dest === "document") headers["Upgrade-Insecure-Requests"] = "1";
|
||||
|
||||
return { ...headers, ...extra };
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
|
||||
import { browserHeaders } from "./browser-headers";
|
||||
|
||||
export function validateSwfBytes(buffer: Buffer): boolean {
|
||||
if (buffer.length < 8) return false;
|
||||
const sig = buffer.toString("ascii", 0, 3);
|
||||
@@ -31,11 +33,9 @@ export async function downloadFile(
|
||||
}
|
||||
const res = await fetch(url, {
|
||||
signal: AbortSignal.timeout(15000),
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36",
|
||||
headers: browserHeaders("image", {
|
||||
Accept: "image/png,image/*,*/*;q=0.8",
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const deterministic =
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { browserHeaders } from "./browser-headers";
|
||||
|
||||
const EXTERNAL_VARIABLES_URL =
|
||||
"https://www.habbo.com/gamedata/external_variables/1";
|
||||
const CACHE_TTL = 30 * 60 * 1000;
|
||||
@@ -14,6 +16,7 @@ export async function resolveGordonBuildUrl(): Promise<string> {
|
||||
if (gordonCache && now - gordonCache.ts < CACHE_TTL) return gordonCache.url;
|
||||
const res = await fetch(EXTERNAL_VARIABLES_URL, {
|
||||
signal: AbortSignal.timeout(20000),
|
||||
headers: browserHeaders("document"),
|
||||
});
|
||||
if (!res.ok)
|
||||
throw new Error(`external_variables fetch failed: ${res.status}`);
|
||||
|
||||
Reference in new issue
Block a user