From cd25f124bd0d5290201e243a0cca008bf47d76df Mon Sep 17 00:00:00 2001 From: AMIAY Date: Sun, 1 Feb 2026 17:23:00 +0100 Subject: [PATCH] Add proxy client and route downloads through it --- .env.example | 2 + backend/managers/uiFileManager.js | 7 ++- backend/services/versionManager.js | 7 +-- backend/utils/clientPatcher.js | 57 +++++++----------- backend/utils/proxyClient.js | 96 ++++++++++++++++++++++++++++++ 5 files changed, 127 insertions(+), 42 deletions(-) create mode 100644 backend/utils/proxyClient.js diff --git a/.env.example b/.env.example index e69de29..36f08f9 100644 --- a/.env.example +++ b/.env.example @@ -0,0 +1,2 @@ +HF2P_SECRET_KEY=YOUR_KEY_HERE +HF2P_PROXY_URL=YOUR_PROXY \ No newline at end of file diff --git a/backend/managers/uiFileManager.js b/backend/managers/uiFileManager.js index 531ca88..7daf374 100644 --- a/backend/managers/uiFileManager.js +++ b/backend/managers/uiFileManager.js @@ -1,6 +1,7 @@ const fs = require('fs'); const path = require('path'); const { downloadFile, findHomePageUIPath, findLogoPath } = require('../utils/fileManager'); +const { proxyRequest } = require('../utils/proxyClient'); async function downloadAndReplaceHomePageUI(gameDir, progressCallback) { try { @@ -13,7 +14,8 @@ async function downloadAndReplaceHomePageUI(gameDir, progressCallback) { const homeUIUrl = 'https://files.hytalef2p.com/api/HomeUI'; const tempHomePath = path.join(path.dirname(gameDir), 'HomePage_temp.ui'); - await downloadFile(homeUIUrl, tempHomePath); + const response = await proxyRequest(homeUIUrl, { responseType: 'arraybuffer' }); + fs.writeFileSync(tempHomePath, response.data); const existingHomePath = findHomePageUIPath(gameDir); @@ -66,7 +68,8 @@ async function downloadAndReplaceLogo(gameDir, progressCallback) { const logoUrl = 'https://files.hytalef2p.com/api/Logo'; const tempLogoPath = path.join(path.dirname(gameDir), 'Logo@2x_temp.png'); - await downloadFile(logoUrl, tempLogoPath); + const response = await proxyRequest(logoUrl, { responseType: 'arraybuffer' }); + fs.writeFileSync(tempLogoPath, response.data); const existingLogoPath = findLogoPath(gameDir); diff --git a/backend/services/versionManager.js b/backend/services/versionManager.js index ff4e037..9f91291 100644 --- a/backend/services/versionManager.js +++ b/backend/services/versionManager.js @@ -2,6 +2,7 @@ const axios = require('axios'); const crypto = require('crypto'); const fs = require('fs'); const { getOS, getArch } = require('../utils/platformUtils'); +const { proxyRequest } = require('../utils/proxyClient'); const BASE_PATCH_URL = 'https://game-patches.hytale.com/patches'; const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest'; @@ -9,8 +10,7 @@ const MANIFEST_API = 'https://files.hytalef2p.com/api/patch_manifest'; async function getLatestClientVersion(branch = 'release') { try { console.log(`Fetching latest client version from API (branch: ${branch})...`); - const response = await axios.get('https://files.hytalef2p.com/api/version_client', { - params: { branch }, + const response = await proxyRequest(`https://files.hytalef2p.com/api/version_client?branch=${branch}`, { timeout: 40000, headers: { 'User-Agent': 'Hytale-F2P-Launcher' @@ -66,8 +66,7 @@ async function fetchPatchManifest(branch = 'release') { try { const os = getOS(); const arch = getArch(); - const response = await axios.get(MANIFEST_API, { - params: { branch, os, arch }, + const response = await proxyRequest(`${MANIFEST_API}?branch=${branch}&os=${os}&arch=${arch}`, { timeout: 10000 }); return response.data.patches || {}; diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js index 8a600d7..218ab79 100644 --- a/backend/utils/clientPatcher.js +++ b/backend/utils/clientPatcher.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const { getProxyDownloadStream } = require('./proxyClient'); // Domain configuration const ORIGINAL_DOMAIN = 'hytale.com'; @@ -605,9 +606,6 @@ class ClientPatcher { console.log('Downloading pre-patched HytaleServer.jar...'); try { - const https = require('https'); - - // Use different URL for pre-release vs release let url; if (branch === 'pre-release') { url = 'https://patcher.authbp.xyz/download/patched_prerelease'; @@ -617,41 +615,28 @@ class ClientPatcher { console.log(' Using release patched server from:', url); } + const file = fs.createWriteStream(serverPath); + let totalSize = 0; + let downloaded = 0; + + const stream = await getProxyDownloadStream(url, (chunk, downloadedBytes, total) => { + downloaded = downloadedBytes; + totalSize = total; + if (progressCallback && totalSize) { + const percent = 30 + Math.floor((downloaded / totalSize) * 60); + progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent); + } + }); + + stream.pipe(file); + await new Promise((resolve, reject) => { - const handleResponse = (response) => { - if (response.statusCode === 302 || response.statusCode === 301) { - https.get(response.headers.location, handleResponse).on('error', reject); - return; - } - - if (response.statusCode !== 200) { - reject(new Error(`Failed to download: HTTP ${response.statusCode}`)); - return; - } - - const file = fs.createWriteStream(serverPath); - const totalSize = parseInt(response.headers['content-length'], 10); - let downloaded = 0; - - response.on('data', (chunk) => { - downloaded += chunk.length; - if (progressCallback && totalSize) { - const percent = 30 + Math.floor((downloaded / totalSize) * 60); - progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent); - } - }); - - response.pipe(file); - file.on('finish', () => { - file.close(); - resolve(); - }); - }; - - https.get(url, handleResponse).on('error', (err) => { - fs.unlink(serverPath, () => {}); - reject(err); + file.on('finish', () => { + file.close(); + resolve(); }); + file.on('error', reject); + stream.on('error', reject); }); console.log(' Download successful'); diff --git a/backend/utils/proxyClient.js b/backend/utils/proxyClient.js new file mode 100644 index 0000000..15d062a --- /dev/null +++ b/backend/utils/proxyClient.js @@ -0,0 +1,96 @@ +const crypto = require('crypto'); +const axios = require('axios'); +const https = require('https'); + +const PROXY_URL = process.env.HF2P_PROXY_URL || 'your_proxy_url_here'; +const SECRET_KEY = process.env.HF2P_SECRET_KEY || 'your_secret_key_here_for_jwt'; + +function generateToken() { + const timestamp = Date.now().toString(); + const hash = crypto + .createHmac('sha256', SECRET_KEY) + .update(timestamp) + .digest('hex'); + return `${timestamp}:${hash}`; +} + +async function proxyRequest(url, options = {}) { + const token = generateToken(); + const urlObj = new URL(url); + const targetUrl = `${urlObj.protocol}//${urlObj.host}`; + + const config = { + method: options.method || 'GET', + url: `${PROXY_URL}/proxy${urlObj.pathname}${urlObj.search}`, + headers: { + 'X-Auth-Token': token, + 'X-Target-URL': targetUrl, + ...(options.headers || {}) + }, + timeout: options.timeout || 30000, + responseType: options.responseType + }; + + return axios(config); +} + +function getProxyDownloadStream(url, onData) { + return new Promise((resolve, reject) => { + const token = generateToken(); + const urlObj = new URL(url); + const targetUrl = `${urlObj.protocol}//${urlObj.host}`; + + const proxyUrl = new URL(PROXY_URL); + const requestPath = `/proxy${urlObj.pathname}${urlObj.search}`; + + const options = { + hostname: proxyUrl.hostname, + port: proxyUrl.port || (proxyUrl.protocol === 'https:' ? 443 : 80), + path: requestPath, + method: 'GET', + headers: { + 'X-Auth-Token': token, + 'X-Target-URL': targetUrl + } + }; + + const protocol = proxyUrl.protocol === 'https:' ? https : require('http'); + + const handleResponse = (response) => { + if (response.statusCode === 302 || response.statusCode === 301) { + const redirectUrl = response.headers.location; + if (redirectUrl.startsWith('http')) { + getProxyDownloadStream(redirectUrl, onData).then(resolve).catch(reject); + } else { + reject(new Error(`Invalid redirect: ${redirectUrl}`)); + } + return; + } + + if (response.statusCode !== 200) { + reject(new Error(`HTTP ${response.statusCode}`)); + return; + } + + const totalSize = parseInt(response.headers['content-length'], 10); + let downloaded = 0; + + response.on('data', (chunk) => { + downloaded += chunk.length; + if (onData) { + onData(chunk, downloaded, totalSize); + } + }); + + resolve(response); + }; + + protocol.get(options, handleResponse).on('error', reject); + }); +} + +module.exports = { + proxyRequest, + getProxyDownloadStream, + generateToken +};