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();
|
const sources = await listSources();
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
|
if (!source.furnidataUrl) continue;
|
||||||
try {
|
try {
|
||||||
const entries = await fetchSourceFurnidata(source.furnidataUrl);
|
const entries = await fetchSourceFurnidata(source.furnidataUrl);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ensureDirectories,
|
ensureDirectories,
|
||||||
getOrCreateCategoryPage,
|
getOrCreateCategoryPage,
|
||||||
} from "@/lib/services/furni-import";
|
} from "@/lib/services/furni-import";
|
||||||
|
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
||||||
import { downloadFile } from "@/lib/services/import/core/download";
|
import { downloadFile } from "@/lib/services/import/core/download";
|
||||||
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
import { parseNitroBundle } from "@/lib/services/swf/nitro-builder";
|
||||||
|
|
||||||
@@ -87,7 +88,10 @@ export async function fetchSourceFurnidata(
|
|||||||
): Promise<SourceFurni[]> {
|
): Promise<SourceFurni[]> {
|
||||||
const hit = cache.get(url);
|
const hit = cache.get(url);
|
||||||
if (hit && now && now - hit.ts < TTL) return hit.list;
|
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}`);
|
if (!res.ok) throw new Error(`furnidata fetch failed: ${res.status} ${url}`);
|
||||||
let list: SourceFurni[];
|
let list: SourceFurni[];
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,47 +11,50 @@ export interface CloneSource {
|
|||||||
|
|
||||||
const KEY = "clone_sources";
|
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",
|
id: "default-habbo",
|
||||||
name: "HabboAssets",
|
name: "Habbo",
|
||||||
furnidataUrl: "",
|
furnidataUrl:
|
||||||
|
"https://raw.githubusercontent.com/Izzxt/habbo-resource/master/gamedata/furnidata.json",
|
||||||
nitroBaseUrl: "",
|
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",
|
id: "default-wibbo",
|
||||||
name: "Wibbo",
|
name: "Wibbo",
|
||||||
furnidataUrl: "",
|
furnidataUrl: "https://assets.wibbo.org/gamedata/FurnitureData.json",
|
||||||
nitroBaseUrl: "https://assets.wibbo.org/bundled/furniture",
|
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: "",
|
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[]> {
|
export async function listSources(): Promise<CloneSource[]> {
|
||||||
const raw = (await siteSettings.get(KEY, "[]")) ?? "[]";
|
const raw = (await siteSettings.get(KEY, "[]")) ?? "[]";
|
||||||
try {
|
try {
|
||||||
const arr = JSON.parse(raw);
|
const arr = JSON.parse(raw);
|
||||||
return Array.isArray(arr) ? arr : [];
|
return Array.isArray(arr) && arr.length > 0 ? arr : DEFAULT_SOURCES;
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return DEFAULT_SOURCES;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ describe("official Habbo furnidata cache", () => {
|
|||||||
expect(fetchMock).toHaveBeenNthCalledWith(
|
expect(fetchMock).toHaveBeenNthCalledWith(
|
||||||
2,
|
2,
|
||||||
"https://www.habbo.nl/gamedata/furnidata_json/1",
|
"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";
|
} from "@/lib/habbo-gamedata-hotel";
|
||||||
import { logger } from "@/lib/logger";
|
import { logger } from "@/lib/logger";
|
||||||
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
import { getHabboGamedataHotel } from "@/lib/services/habbo-gamedata-hotel";
|
||||||
|
import { browserHeaders } from "@/lib/services/import/core/browser-headers";
|
||||||
import type { OfficialHabboFurniEntry } from "@/types/furni";
|
import type { OfficialHabboFurniEntry } from "@/types/furni";
|
||||||
|
|
||||||
const CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
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}...`);
|
console.debug(`[habbo-furnidata] Fetching furnidata from ${url}...`);
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
signal: AbortSignal.timeout(20000),
|
signal: AbortSignal.timeout(20000),
|
||||||
headers: { Accept: "application/json" },
|
headers: browserHeaders(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
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 { promises as fs } from "node:fs";
|
||||||
|
|
||||||
|
import { browserHeaders } from "./browser-headers";
|
||||||
|
|
||||||
export function validateSwfBytes(buffer: Buffer): boolean {
|
export function validateSwfBytes(buffer: Buffer): boolean {
|
||||||
if (buffer.length < 8) return false;
|
if (buffer.length < 8) return false;
|
||||||
const sig = buffer.toString("ascii", 0, 3);
|
const sig = buffer.toString("ascii", 0, 3);
|
||||||
@@ -31,11 +33,9 @@ export async function downloadFile(
|
|||||||
}
|
}
|
||||||
const res = await fetch(url, {
|
const res = await fetch(url, {
|
||||||
signal: AbortSignal.timeout(15000),
|
signal: AbortSignal.timeout(15000),
|
||||||
headers: {
|
headers: browserHeaders("image", {
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0 Safari/537.36",
|
|
||||||
Accept: "image/png,image/*,*/*;q=0.8",
|
Accept: "image/png,image/*,*/*;q=0.8",
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const deterministic =
|
const deterministic =
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { browserHeaders } from "./browser-headers";
|
||||||
|
|
||||||
const EXTERNAL_VARIABLES_URL =
|
const EXTERNAL_VARIABLES_URL =
|
||||||
"https://www.habbo.com/gamedata/external_variables/1";
|
"https://www.habbo.com/gamedata/external_variables/1";
|
||||||
const CACHE_TTL = 30 * 60 * 1000;
|
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;
|
if (gordonCache && now - gordonCache.ts < CACHE_TTL) return gordonCache.url;
|
||||||
const res = await fetch(EXTERNAL_VARIABLES_URL, {
|
const res = await fetch(EXTERNAL_VARIABLES_URL, {
|
||||||
signal: AbortSignal.timeout(20000),
|
signal: AbortSignal.timeout(20000),
|
||||||
|
headers: browserHeaders("document"),
|
||||||
});
|
});
|
||||||
if (!res.ok)
|
if (!res.ok)
|
||||||
throw new Error(`external_variables fetch failed: ${res.status}`);
|
throw new Error(`external_variables fetch failed: ${res.status}`);
|
||||||
|
|||||||
Reference in new issue
Block a user