import fs from "node:fs"; import path from "node:path"; import crypto from "node:crypto"; import { DCRYPT_UPLOAD_URL, DLC_AES_IV, DLC_AES_KEY, DLC_SERVICE_URL } from "./constants"; import { compactErrorText, inferPackageNameFromLinks, isHttpLink, sanitizeFilename, uniquePreserveOrder } from "./utils"; import { ParsedPackageInput } from "../shared/types"; function decodeDcryptPayload(responseText: string): unknown { let text = String(responseText || "").trim(); const m = text.match(/]*>([\s\S]*?)<\/textarea>/i); if (m) { text = m[1].replace(/"/g, '"').replace(/&/g, "&").trim(); } if (!text) { return ""; } try { return JSON.parse(text); } catch { return text; } } function extractUrlsRecursive(data: unknown): string[] { if (typeof data === "string") { const found = data.match(/https?:\/\/[^\s"'<>]+/gi) ?? []; return uniquePreserveOrder(found.filter((url) => isHttpLink(url))); } if (Array.isArray(data)) { return uniquePreserveOrder(data.flatMap((item) => extractUrlsRecursive(item))); } if (data && typeof data === "object") { return uniquePreserveOrder(Object.values(data as Record).flatMap((value) => extractUrlsRecursive(value))); } return []; } function groupLinksByName(links: string[]): ParsedPackageInput[] { const unique = uniquePreserveOrder(links.filter((link) => isHttpLink(link))); const grouped = new Map(); for (const link of unique) { const name = sanitizeFilename(inferPackageNameFromLinks([link]) || "Paket"); const current = grouped.get(name) ?? []; current.push(link); grouped.set(name, current); } return Array.from(grouped.entries()).map(([name, packageLinks]) => ({ name, links: packageLinks })); } function extractPackagesFromPayload(payload: unknown): ParsedPackageInput[] { const urls = extractUrlsRecursive(payload); if (urls.length === 0) { return []; } return groupLinksByName(urls); } function decryptRcPayload(base64Rc: string): Buffer { const rcBytes = Buffer.from(base64Rc, "base64"); const decipher = crypto.createDecipheriv("aes-128-cbc", DLC_AES_KEY, DLC_AES_IV); decipher.setAutoPadding(false); return Buffer.concat([decipher.update(rcBytes), decipher.final()]); } function parsePackagesFromDlcXml(xml: string): ParsedPackageInput[] { const packages: ParsedPackageInput[] = []; const packageRegex = /]*name="([^"]*)"[^>]*>([\s\S]*?)<\/package>/gi; for (let m = packageRegex.exec(xml); m; m = packageRegex.exec(xml)) { const encodedName = m[1] || ""; const packageBody = m[2] || ""; let packageName = ""; if (encodedName) { try { packageName = Buffer.from(encodedName, "base64").toString("utf8"); } catch { packageName = encodedName; } } const links: string[] = []; const urlRegex = /(.*?)<\/url>/gi; for (let um = urlRegex.exec(packageBody); um; um = urlRegex.exec(packageBody)) { try { const url = Buffer.from((um[1] || "").trim(), "base64").toString("utf8").trim(); if (isHttpLink(url)) { links.push(url); } } catch { // skip broken entries } } const uniqueLinks = uniquePreserveOrder(links); if (uniqueLinks.length > 0) { packages.push({ name: sanitizeFilename(packageName || inferPackageNameFromLinks(uniqueLinks) || `Paket-${packages.length + 1}`), links: uniqueLinks }); } } return packages; } async function decryptDlcLocal(filePath: string): Promise { const content = fs.readFileSync(filePath, "ascii").trim(); if (content.length < 89) { return []; } const dlcKey = content.slice(-88); const dlcData = content.slice(0, -88); const rcUrl = DLC_SERVICE_URL.replace("{KEY}", encodeURIComponent(dlcKey)); const rcResponse = await fetch(rcUrl, { method: "GET" }); if (!rcResponse.ok) { return []; } const rcText = await rcResponse.text(); const rcMatch = rcText.match(/(.*?)<\/rc>/i); if (!rcMatch) { return []; } const realKey = decryptRcPayload(rcMatch[1]).subarray(0, 16); const encrypted = Buffer.from(dlcData, "base64"); const decipher = crypto.createDecipheriv("aes-128-cbc", realKey, realKey); decipher.setAutoPadding(false); let decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]); const pad = decrypted[decrypted.length - 1]; if (pad > 0 && pad <= 16) { decrypted = decrypted.subarray(0, decrypted.length - pad); } const xmlData = Buffer.from(decrypted.toString("utf8"), "base64").toString("utf8"); return parsePackagesFromDlcXml(xmlData); } async function decryptDlcViaDcrypt(filePath: string): Promise { const fileName = path.basename(filePath); const blob = new Blob([fs.readFileSync(filePath)]); const form = new FormData(); form.set("dlcfile", blob, fileName); const response = await fetch(DCRYPT_UPLOAD_URL, { method: "POST", body: form }); const text = await response.text(); if (!response.ok) { throw new Error(compactErrorText(text)); } const payload = decodeDcryptPayload(text); let packages = extractPackagesFromPayload(payload); if (packages.length === 1) { const regrouped = groupLinksByName(packages[0].links); if (regrouped.length > 1) { packages = regrouped; } } if (packages.length === 0) { packages = groupLinksByName(extractUrlsRecursive(text)); } return packages; } export async function importDlcContainers(filePaths: string[]): Promise { const out: ParsedPackageInput[] = []; for (const filePath of filePaths) { if (path.extname(filePath).toLowerCase() !== ".dlc") { continue; } let packages: ParsedPackageInput[] = []; try { packages = await decryptDlcLocal(filePath); } catch { packages = []; } if (packages.length === 0) { packages = await decryptDlcViaDcrypt(filePath); } out.push(...packages); } return out; }