import express, { Application } from "express"; import fs from "fs"; import path from "path"; import fetch, { Response as FetchResponse, Headers } from "node-fetch"; import ProxyAgent from "proxy-agent"; import { Config } from "@fosscord/util"; const ASSET_FOLDER_PATH = path.join(__dirname, "..", "..", "..", "assets"); export default function TestClient(app: Application) { app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "public"))); app.use("/assets", express.static(path.join(ASSET_FOLDER_PATH, "cache"))); // Test client is disabled, so don't need to run any more. Above should probably be moved somewhere? if (!Config.get().client.useTestClient) { app.get("*", (req, res) => { return res.redirect("/api/ping"); }); return; } const agent = new ProxyAgent(); let html = fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "index.html"), { encoding: "utf-8" }); html = applyEnv(html); // update window.GLOBAL_ENV according to config html = applyPlugins(html); // inject our plugins app.use("/assets/plugins", express.static(path.join(ASSET_FOLDER_PATH, "plugins"))); app.use("/assets/inline-plugins", express.static(path.join(ASSET_FOLDER_PATH, "inline-plugins"))); // Asset memory cache const assetCache = new Map(); // Fetches uncached ( on disk ) assets from discord.com and stores them in memory cache. app.get("/assets/:file", async (req, res) => { delete req.headers.host; if (req.params.file.endsWith(".map")) return res.status(404); let response: FetchResponse; let buffer: Buffer; const cache = assetCache.get(req.params.file); if (!cache) { response = await fetch(`https://discord.com/assets/${req.params.file}`, { agent, headers: { ...req.headers as { [key: string]: string; } }, }); buffer = await response.buffer(); } else { response = cache.response; buffer = cache.buffer; } [ "content-length", "content-security-policy", "strict-transport-security", "set-cookie", "transfer-encoding", "expect-ct", "access-control-allow-origin", "content-encoding" ].forEach(headerName => { response.headers.delete(headerName); }); response.headers.forEach((value, name) => res.set(name, value)); assetCache.set(req.params.file, { buffer, response }); // TODO: I don't like this. Figure out a way to get client cacher to download *all* assets. if (response.status == 200) { console.warn(`[TestClient] Cache miss for file ${req.params.file}! Use 'npm run generate:client' to cache and patch.`); await fs.promises.appendFile(path.join(ASSET_FOLDER_PATH, "cacheMisses"), req.params.file + "\n"); } return res.send(buffer); }); // Instead of our generated html, send developers.html for developers endpoint app.get("/developers*", (req, res) => { res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24); // 24 hours res.set("content-type", "text/html"); res.send(fs.readFileSync(path.join(ASSET_FOLDER_PATH, "client_test", "developers.html"), { encoding: "utf-8" })); }); // Send our generated index.html for all routes. app.get("*", (req, res) => { res.set("Cache-Control", "public, max-age=" + 60 * 60 * 24); // 24 hours res.set("content-type", "text/html"); if (req.url.startsWith("/api") || req.url.startsWith("/__development")) return; return res.send(html); }); } // Apply gateway/cdn endpoint values from config to index.html. const applyEnv = (html: string): string => { const config = Config.get(); const cdn = (config.cdn.endpointClient || config.cdn.endpointPublic || process.env.CDN || "") .replace(/(https?)?(:\/\/?)/g, ""); const gateway = (config.gateway.endpointClient || config.gateway.endpointPublic || process.env.GATEWAY || ""); if (cdn) html = html.replace(/CDN_HOST: .+/, `CDN_HOST: \`${cdn}\`,`); if (gateway) html = html.replace(/GATEWAY_ENDPOINT: .+/, `GATEWAY_ENDPOINT: \`${gateway}\`,`); return html; }; // Injects inline, preload, and standard plugins into index.html. const applyPlugins = (html: string): string => { // Inline plugins. Injected as `) .join("\n"); html = html.replace("", inline); // Preload plugins. Text content of each plugin is injected into head. const preloadFiles = fs.readdirSync(path.join(ASSET_FOLDER_PATH, "preload-plugins")); const preload = preloadFiles .filter(x => x.endsWith(".js")) .map(x => ``) .join("\n"); html = html.replace("", preload); // Normal plugins. Injected as `) .join("\n"); html = html.replace("", plugins); return html; };