diff --git a/package-lock.json b/package-lock.json index 0e8c5c3..ab5355b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "chalk": "^5.3.0", "ejs": "^3.1.10", "http-proxy": "1.18.1", + "http-proxy-middleware": "^3.0.0", "js-beautify": "1.15.1", "jszip": "^3.10.1", "koa": "2.15.3", @@ -899,7 +900,6 @@ "version": "1.17.14", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -960,7 +960,6 @@ "version": "20.14.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", - "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -1338,7 +1337,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "dependencies": { "fill-range": "^7.1.1" }, @@ -2091,7 +2089,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2501,6 +2498,22 @@ "node": ">=8.0.0" } }, + "node_modules/http-proxy-middleware": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0.tgz", + "integrity": "sha512-36AV1fIaI2cWRzHo+rbcxhe3M3jUDCNzc4D5zRl57sEWRAxdXYtw7FSQKYY6PDKssiAKjLYypbssHk+xs/kMXw==", + "dependencies": { + "@types/http-proxy": "^1.17.10", + "debug": "^4.3.4", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.5" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -2611,7 +2624,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -2642,7 +2654,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -2660,7 +2671,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "engines": { "node": ">=0.12.0" } @@ -2675,6 +2685,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-whitespace": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/is-whitespace/-/is-whitespace-0.3.0.tgz", @@ -3069,7 +3090,6 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -3389,7 +3409,6 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, "engines": { "node": ">=8.6" }, @@ -3951,7 +3970,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "dependencies": { "is-number": "^7.0.0" }, @@ -4060,8 +4078,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "devOptional": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index 9eff40a..da7cea7 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "chalk": "^5.3.0", "ejs": "^3.1.10", "http-proxy": "1.18.1", + "http-proxy-middleware": "^3.0.0", "js-beautify": "1.15.1", "jszip": "^3.10.1", "koa": "2.15.3", diff --git a/src/clientApp.ts b/src/clientApp.ts index 9a45086..d406ef0 100644 --- a/src/clientApp.ts +++ b/src/clientApp.ts @@ -1,5 +1,6 @@ #!/usr/bin/env node import httpProxy from 'http-proxy'; +import { createProxyMiddleware } from 'http-proxy-middleware'; import Koa from 'koa'; import koaConditionalGet from 'koa-conditional-get'; import views from '@ladjs/koa-views'; @@ -23,7 +24,7 @@ import { } from './utils/utils'; import { logError, handleProxyError, handleServerError } from './utils/errors'; import { clientAuth } from './inject/clientAuth'; -import { removeDecorations } from './inject/removeDecorations'; +import { roomDecorations } from './inject/roomDecorations'; import { customMenuLinks } from './inject/customMenuLinks'; // Get the app directory and version @@ -36,6 +37,7 @@ const version = packageJson.version || '1.0.0'; const arrow = '\u2192'; const localhost = 'localhost'; const defaultPort = 8080; +const awsHost = 'https://s3.amazonaws.com'; // Parse program arguments const argv = (() => { @@ -97,6 +99,7 @@ console.log('🧩', chalk.yellowBright(`Screepers Steamless Client v${version}`) // Create proxy const proxy = httpProxy.createProxyServer({ changeOrigin: true }); proxy.on('error', (err, req, res) => handleProxyError(err, res, argv.debug)); +const awsProxy = createProxyMiddleware({ target: awsHost, changeOrigin: true }); const exitOnPackageError = () => { if (argv.package) { @@ -142,6 +145,15 @@ koa.use(views(path.join(__dirname, '../views'), { extension: 'ejs' })); // Serve client assets directly from steam package koa.use(koaConditionalGet()); +// Proxy requests to AWS host to avoid CORS issues +koa.use(async (ctx, next) => { + if (ctx.url.startsWith('/static.screeps.com')) { + await awsProxy(ctx.req, ctx.res, next); + ctx.respond = false; + } + return next(); +}); + // Render the index.ejs file and pass the serverList variable koa.use(async (context, next) => { if (['/', 'index.html'].includes(context.path)) { @@ -220,7 +232,7 @@ koa.use(async (context, next) => { const replaceHeader = [ header, generateScriptTag(clientAuth, { backend: info.backend, guest: argv.guest }), - generateScriptTag(removeDecorations, { backend: info.backend }), + generateScriptTag(roomDecorations, { backend: info.backend, awsHost }), generateScriptTag(customMenuLinks, { backend: info.backend, seasonLink, ptrLink, changeServerLink }), ].join('\n'); src = src.replace(header, replaceHeader); @@ -277,7 +289,23 @@ koa.use(async (context, next) => { return src; } else if (context.path.endsWith('.js')) { let src = await file.async('text'); - if (urlPath === 'build.min.js') { + + if (urlPath.startsWith('app2/main.')) { + // Modify getData() to fetch from the correct API path + src = src.replace(/fetch\(t\+"version"\)/g, 'fetch(window.CONFIG.API_URL+"version")'); + // Remove fetch to forum RSS feed + src = src.replace(/fetch\("https:\/\/screeps\.com\/forum\/.+\.rss"\)/g, 'Promise.resolve()'); + // Remove AWS host from rewards URL + src = src.replace(/https:\/\/s3\.amazonaws\.com/g, ''); + } else if (urlPath.startsWith('vendor/renderer/renderer.js')) { + // Modify renderer to remove AWS host from loadElement() + src = src.replace( + /\(this\.data\.src=this\.url\)/g, + `(this.data.src=this.url.replace("${awsHost}",""))`, + ); + // Remove AWS host from image URLs + src = src.replace(/src=t,/g, `src=t.replace("${awsHost}",""),`); + } else if (urlPath === 'build.min.js') { // Load backend info from underlying server const backend = new URL(info.backend); const isOfficialLike = isOfficial || (await isOfficialLikeVersion(client)); @@ -334,9 +362,9 @@ koa.use(async (context, next) => { const extension = (/\.[^.]+$/.exec(urlPath.toLowerCase())?.[0] ?? '.html') as keyof typeof mimeTypes; context.set('Content-Type', mimeTypes[extension] ?? 'text/html'); - // We can safely cache explicitly-versioned resources forever + // Set cache for resources that change occasionally if (context.request.query.bust) { - context.set('Cache-Control', 'public,max-age=31536000,immutable'); + context.set('Cache-Control', 'public, max-age=604800, immutable'); // Cache for 1 week } }); diff --git a/src/inject/removeDecorations.ts b/src/inject/roomDecorations.ts similarity index 51% rename from src/inject/removeDecorations.ts rename to src/inject/roomDecorations.ts index 82deedf..ef3a744 100644 --- a/src/inject/removeDecorations.ts +++ b/src/inject/roomDecorations.ts @@ -1,11 +1,30 @@ +type AnyObject = Record; +type AnyProps = string | AnyObject | AnyProps[]; + /** - * This function is injected into the client to remove room decorations (and avoid CORS errors). + * This function is injected into the client to modify room decorations. */ -export function removeDecorations(backend: string) { +export function roomDecorations(backend: string, awsHost: string) { if (!backend.includes('screeps.com')) { return; } + // Recursive function to remove AWS host from room decoration URLs + const removeAWSHost = (obj: AnyProps): AnyProps => { + if (typeof obj === 'string') { + return obj.replace(awsHost, ''); + } else if (Array.isArray(obj)) { + return obj.map(removeAWSHost) as AnyProps; + } else if (obj && typeof obj === 'object') { + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + obj[key] = removeAWSHost(obj[key] as AnyProps); + } + } + } + return obj; + }; + const onRoomUpdate = () => { const roomInterval = setInterval(() => { const roomElement = document.querySelector('.room.ng-scope'); @@ -14,8 +33,8 @@ export function removeDecorations(backend: string) { const connection = window.angular.element(document.body).injector().get('Connection'); const roomScope = window.angular.element(roomElement).scope(); connection.onRoomUpdate(roomScope, () => { - // Remove room decorations - roomScope.Room.decorations = []; + // Modify room decorations to avoid CORS errors + roomScope.Room.decorations = removeAWSHost(roomScope.Room.decorations); }); } }, 100);