mirror of
https://gitea.shironeko-all.duckdns.org/shironeko/Hytale-F2P-2.git
synced 2026-02-26 02:31:46 -03:00
feat(patcher): Implement DualAuth patcher with enhanced server patching
- Introduce DualAuthPatcher with support for hybrid authentication - Update default auth domain to `auth.sanasol.ws` - Integrate Java detection and bundled JRE handling for patcher execution - Add server patch flag for avoiding redundant patching - Automate DualAuthPatcher setup: download, compile, and execute with dependencies - Enhance patching logic for extended logging and modularity
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -12,3 +12,6 @@ pkg/
|
|||||||
*.zst
|
*.zst
|
||||||
bun.lockb
|
bun.lockb
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Downloaded patcher (from hytale-auth-server)
|
||||||
|
backend/patcher/
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ Set these before running to customize your server:
|
|||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
|
| `HYTALE_SERVER_URL` | (placeholder) | URL to download pre-patched server JAR |
|
||||||
| `HYTALE_AUTH_DOMAIN` | `sanasol.ws` | Auth server domain |
|
| `HYTALE_AUTH_DOMAIN` | `auth.sanasol.ws` | Auth server domain (4-16 chars) |
|
||||||
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
|
| `HYTALE_BIND` | `0.0.0.0:5520` | Server IP and port |
|
||||||
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
| `HYTALE_AUTH_MODE` | `authenticated` | Auth mode (see below) |
|
||||||
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
|
| `HYTALE_SERVER_NAME` | `My Hytale Server` | Server display name |
|
||||||
@@ -400,7 +400,7 @@ docker run -d \
|
|||||||
--name hytale-server \
|
--name hytale-server \
|
||||||
-p 5520:5520/udp \
|
-p 5520:5520/udp \
|
||||||
-v ./data:/data \
|
-v ./data:/data \
|
||||||
-e HYTALE_AUTH_DOMAIN=sanasol.ws \
|
-e HYTALE_AUTH_DOMAIN=auth.sanasol.ws \
|
||||||
-e HYTALE_SERVER_NAME="My Server" \
|
-e HYTALE_SERVER_NAME="My Server" \
|
||||||
-e JVM_XMX=8G \
|
-e JVM_XMX=8G \
|
||||||
ghcr.io/hybrowse/hytale-server-docker:latest
|
ghcr.io/hybrowse/hytale-server-docker:latest
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const os = require('os');
|
|||||||
|
|
||||||
|
|
||||||
// Default auth domain - can be overridden by env var or config
|
// Default auth domain - can be overridden by env var or config
|
||||||
const DEFAULT_AUTH_DOMAIN = 'sanasol.ws';
|
const DEFAULT_AUTH_DOMAIN = 'auth.sanasol.ws';
|
||||||
|
|
||||||
// Get auth domain from env, config, or default
|
// Get auth domain from env, config, or default
|
||||||
function getAuthDomain() {
|
function getAuthDomain() {
|
||||||
@@ -26,9 +26,10 @@ function getAuthDomain() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get full auth server URL
|
// Get full auth server URL
|
||||||
|
// Domain already includes subdomain (auth.sanasol.ws), so use directly
|
||||||
function getAuthServerUrl() {
|
function getAuthServerUrl() {
|
||||||
const domain = getAuthDomain();
|
const domain = getAuthDomain();
|
||||||
return `https://sessions.${domain}`;
|
return `https://${domain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save auth domain to config
|
// Save auth domain to config
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const AdmZip = require('adm-zip');
|
const AdmZip = require('adm-zip');
|
||||||
|
const { execSync, spawn } = require('child_process');
|
||||||
|
const { getJavaExec, getBundledJavaPath } = require('../managers/javaManager');
|
||||||
|
const { JRE_DIR } = require('../core/paths');
|
||||||
|
|
||||||
// Domain configuration
|
// Domain configuration
|
||||||
const ORIGINAL_DOMAIN = 'hytale.com';
|
const ORIGINAL_DOMAIN = 'hytale.com';
|
||||||
@@ -16,19 +19,22 @@ function getTargetDomain() {
|
|||||||
const { getAuthDomain } = require('../core/config');
|
const { getAuthDomain } = require('../core/config');
|
||||||
return getAuthDomain();
|
return getAuthDomain();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'sanasol.ws';
|
return 'auth.sanasol.ws';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_NEW_DOMAIN = 'sanasol.ws';
|
const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
|
* Patches HytaleClient and HytaleServer binaries to replace hytale.com with custom domain
|
||||||
* This allows the game to connect to a custom authentication server
|
* This allows the game to connect to a custom authentication server
|
||||||
*
|
*
|
||||||
* Supports domains from 4 to 16 characters:
|
* Supports domains from 4 to 16 characters:
|
||||||
|
* - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
|
||||||
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
* - Domains <= 10 chars: Direct replacement, subdomains stripped
|
||||||
* - Domains 11-16 chars: Split mode - first 6 chars become subdomain prefix
|
* - Domains 11-16 chars: Split mode - first 6 chars replace subdomain prefix, rest replaces domain
|
||||||
|
*
|
||||||
|
* Official hytale.com keeps original subdomain behavior (sessions., account-data., etc.)
|
||||||
*/
|
*/
|
||||||
class ClientPatcher {
|
class ClientPatcher {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -247,9 +253,9 @@ class ClientPatcher {
|
|||||||
|
|
||||||
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
console.log(` Patching sentry: ${oldSentry.slice(0, 30)}... -> ${newSentry}`);
|
||||||
const sentryResult = this.replaceBytes(
|
const sentryResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(oldSentry),
|
this.stringToLengthPrefixed(oldSentry),
|
||||||
this.stringToLengthPrefixed(newSentry)
|
this.stringToLengthPrefixed(newSentry)
|
||||||
);
|
);
|
||||||
result = sentryResult.buffer;
|
result = sentryResult.buffer;
|
||||||
if (sentryResult.count > 0) {
|
if (sentryResult.count > 0) {
|
||||||
@@ -260,9 +266,9 @@ class ClientPatcher {
|
|||||||
// 2. Patch main domain (hytale.com -> mainDomain)
|
// 2. Patch main domain (hytale.com -> mainDomain)
|
||||||
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
console.log(` Patching domain: ${ORIGINAL_DOMAIN} -> ${strategy.mainDomain}`);
|
||||||
const domainResult = this.replaceBytes(
|
const domainResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
this.stringToLengthPrefixed(ORIGINAL_DOMAIN),
|
||||||
this.stringToLengthPrefixed(strategy.mainDomain)
|
this.stringToLengthPrefixed(strategy.mainDomain)
|
||||||
);
|
);
|
||||||
result = domainResult.buffer;
|
result = domainResult.buffer;
|
||||||
if (domainResult.count > 0) {
|
if (domainResult.count > 0) {
|
||||||
@@ -277,9 +283,9 @@ class ClientPatcher {
|
|||||||
for (const sub of subdomains) {
|
for (const sub of subdomains) {
|
||||||
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
console.log(` Patching subdomain: ${sub} -> ${newSubdomainPrefix}`);
|
||||||
const subResult = this.replaceBytes(
|
const subResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(sub),
|
this.stringToLengthPrefixed(sub),
|
||||||
this.stringToLengthPrefixed(newSubdomainPrefix)
|
this.stringToLengthPrefixed(newSubdomainPrefix)
|
||||||
);
|
);
|
||||||
result = subResult.buffer;
|
result = subResult.buffer;
|
||||||
if (subResult.count > 0) {
|
if (subResult.count > 0) {
|
||||||
@@ -303,9 +309,9 @@ class ClientPatcher {
|
|||||||
|
|
||||||
// Try length-prefixed format first
|
// Try length-prefixed format first
|
||||||
const lpResult = this.replaceBytes(
|
const lpResult = this.replaceBytes(
|
||||||
result,
|
result,
|
||||||
this.stringToLengthPrefixed(oldUrl),
|
this.stringToLengthPrefixed(oldUrl),
|
||||||
this.stringToLengthPrefixed(newUrl)
|
this.stringToLengthPrefixed(newUrl)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (lpResult.count > 0) {
|
if (lpResult.count > 0) {
|
||||||
@@ -450,8 +456,13 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE PATCHING: Always patch, never skip
|
if (this.isPatchedAlready(clientPath)) {
|
||||||
console.log(`Force patching client for ${newDomain}`);
|
console.log(`Client already patched for ${newDomain}, skipping`);
|
||||||
|
if (progressCallback) {
|
||||||
|
progressCallback('Client already patched', 100);
|
||||||
|
}
|
||||||
|
return { success: true, alreadyPatched: true, patchCount: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback('Preparing to patch client...', 10);
|
progressCallback('Preparing to patch client...', 10);
|
||||||
@@ -514,20 +525,19 @@ class ClientPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Patch the server JAR to use the custom domain
|
* Patch the server JAR using DualAuthPatcher for full dual auth support
|
||||||
* JAR files are ZIP archives, so we need to extract, patch class files, and repackage
|
* This uses the same patcher as the Docker server for consistency
|
||||||
* @param {string} serverPath - Path to the HytaleServer.jar
|
* @param {string} serverPath - Path to the HytaleServer.jar
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
* @param {function} progressCallback - Optional callback for progress updates
|
||||||
|
* @param {string} javaPath - Path to Java executable
|
||||||
* @returns {object} Result object with success status and details
|
* @returns {object} Result object with success status and details
|
||||||
*/
|
*/
|
||||||
async patchServer(serverPath, progressCallback) {
|
async patchServer(serverPath, progressCallback, javaPath = null) {
|
||||||
const newDomain = this.getNewDomain();
|
const newDomain = this.getNewDomain();
|
||||||
const strategy = this.getDomainStrategy(newDomain);
|
|
||||||
|
|
||||||
console.log('=== Server Patcher v2.0 ===');
|
console.log('=== Server Patcher v3.0 (DualAuth) ===');
|
||||||
console.log(`Target: ${serverPath}`);
|
console.log(`Target: ${serverPath}`);
|
||||||
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
console.log(`Domain: ${newDomain}`);
|
||||||
console.log(`Mode: ${strategy.mode}`);
|
|
||||||
|
|
||||||
if (!fs.existsSync(serverPath)) {
|
if (!fs.existsSync(serverPath)) {
|
||||||
const error = `Server JAR not found: ${serverPath}`;
|
const error = `Server JAR not found: ${serverPath}`;
|
||||||
@@ -535,90 +545,364 @@ class ClientPatcher {
|
|||||||
return { success: false, error };
|
return { success: false, error };
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORCE PATCHING: Always patch, never skip
|
// Check if already patched with DualAuth
|
||||||
console.log(`Force patching server for ${newDomain}`);
|
const patchFlagFile = serverPath + '.dualauth_patched';
|
||||||
|
if (fs.existsSync(patchFlagFile)) {
|
||||||
if (progressCallback) {
|
try {
|
||||||
progressCallback('Extracting server JAR...', 20);
|
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
|
||||||
|
if (flagData.domain === newDomain) {
|
||||||
|
console.log(`Server already patched with DualAuth for ${newDomain}, skipping`);
|
||||||
|
if (progressCallback) progressCallback('Server already patched', 100);
|
||||||
|
return { success: true, alreadyPatched: true };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Flag file corrupt, re-patch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Preparing DualAuth patcher...', 10);
|
||||||
|
|
||||||
|
// Find Java executable - use bundled JRE first (same as game uses)
|
||||||
|
const java = javaPath || this.findJava();
|
||||||
|
if (!java) {
|
||||||
|
const error = 'Java not found. Please install the game first (it includes Java) or install Java 25 from: https://adoptium.net/';
|
||||||
|
console.error(error);
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
console.log(`Using Java: ${java}`);
|
||||||
|
|
||||||
|
// Setup patcher directory
|
||||||
|
const patcherDir = path.join(__dirname, '..', 'patcher');
|
||||||
|
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
||||||
|
const libDir = path.join(patcherDir, 'lib');
|
||||||
|
|
||||||
|
// Download patcher from hytale-auth-server if not present
|
||||||
|
if (progressCallback) progressCallback('Checking patcher...', 15);
|
||||||
|
try {
|
||||||
|
await this.ensurePatcherDownloaded(patcherDir);
|
||||||
|
} catch (e) {
|
||||||
|
const error = `Failed to download DualAuthPatcher: ${e.message}`;
|
||||||
|
console.error(error);
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(patcherJava)) {
|
||||||
|
const error = `DualAuthPatcher.java not found at ${patcherJava}`;
|
||||||
|
console.error(error);
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download ASM libraries if not present
|
||||||
|
if (progressCallback) progressCallback('Checking ASM libraries...', 20);
|
||||||
|
await this.ensureAsmLibraries(libDir);
|
||||||
|
|
||||||
|
// Compile patcher if needed
|
||||||
|
if (progressCallback) progressCallback('Compiling patcher...', 30);
|
||||||
|
const compileResult = await this.compileDualAuthPatcher(java, patcherDir, libDir);
|
||||||
|
if (!compileResult.success) {
|
||||||
|
return { success: false, error: compileResult.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup
|
||||||
|
if (progressCallback) progressCallback('Creating backup...', 40);
|
||||||
|
console.log('Creating backup...');
|
||||||
|
this.backupClient(serverPath);
|
||||||
|
|
||||||
|
// Run the patcher
|
||||||
|
if (progressCallback) progressCallback('Patching server JAR...', 50);
|
||||||
|
console.log('Running DualAuthPatcher...');
|
||||||
|
|
||||||
|
const classpath = [
|
||||||
|
patcherDir,
|
||||||
|
path.join(libDir, 'asm-9.6.jar'),
|
||||||
|
path.join(libDir, 'asm-tree-9.6.jar'),
|
||||||
|
path.join(libDir, 'asm-util-9.6.jar')
|
||||||
|
].join(process.platform === 'win32' ? ';' : ':');
|
||||||
|
|
||||||
|
const patchResult = await this.runDualAuthPatcher(java, classpath, serverPath, newDomain);
|
||||||
|
|
||||||
|
if (patchResult.success) {
|
||||||
|
// Mark as patched
|
||||||
|
fs.writeFileSync(patchFlagFile, JSON.stringify({
|
||||||
|
domain: newDomain,
|
||||||
|
patchedAt: new Date().toISOString(),
|
||||||
|
patcher: 'DualAuthPatcher'
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Server patching complete', 100);
|
||||||
|
console.log('=== Server Patching Complete ===');
|
||||||
|
return { success: true, patchCount: patchResult.patchCount || 1 };
|
||||||
|
} else {
|
||||||
|
return { success: false, error: patchResult.error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find Java executable - uses bundled JRE first (same as game uses)
|
||||||
|
* Falls back to system Java if bundled not available
|
||||||
|
*/
|
||||||
|
findJava() {
|
||||||
|
// 1. Try bundled JRE first (comes with the game)
|
||||||
|
try {
|
||||||
|
const bundled = getBundledJavaPath(JRE_DIR);
|
||||||
|
if (bundled && fs.existsSync(bundled)) {
|
||||||
|
console.log(`Using bundled Java: ${bundled}`);
|
||||||
|
return bundled;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Bundled not available
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Try javaManager's getJavaExec (handles all fallbacks)
|
||||||
|
try {
|
||||||
|
const javaExec = getJavaExec(JRE_DIR);
|
||||||
|
if (javaExec && fs.existsSync(javaExec)) {
|
||||||
|
console.log(`Using Java from javaManager: ${javaExec}`);
|
||||||
|
return javaExec;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Not available
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check JAVA_HOME
|
||||||
|
if (process.env.JAVA_HOME) {
|
||||||
|
const javaHome = process.env.JAVA_HOME;
|
||||||
|
const javaBin = path.join(javaHome, 'bin', process.platform === 'win32' ? 'java.exe' : 'java');
|
||||||
|
if (fs.existsSync(javaBin)) {
|
||||||
|
console.log(`Using Java from JAVA_HOME: ${javaBin}`);
|
||||||
|
return javaBin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Try 'java' from PATH
|
||||||
|
try {
|
||||||
|
execSync('java -version 2>&1', { encoding: 'utf8' });
|
||||||
|
console.log('Using Java from PATH');
|
||||||
|
return 'java';
|
||||||
|
} catch (e) {
|
||||||
|
// Not in PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download DualAuthPatcher from hytale-auth-server if not present
|
||||||
|
*/
|
||||||
|
async ensurePatcherDownloaded(patcherDir) {
|
||||||
|
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
||||||
|
const patcherUrl = 'https://raw.githubusercontent.com/sanasol/hytale-auth-server/master/patcher/DualAuthPatcher.java';
|
||||||
|
|
||||||
|
if (!fs.existsSync(patcherDir)) {
|
||||||
|
fs.mkdirSync(patcherDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(patcherJava)) {
|
||||||
|
console.log('Downloading DualAuthPatcher from hytale-auth-server...');
|
||||||
|
try {
|
||||||
|
const https = require('https');
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(patcherJava);
|
||||||
|
https.get(patcherUrl, (response) => {
|
||||||
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
||||||
|
// Follow redirect
|
||||||
|
https.get(response.headers.location, (redirectResponse) => {
|
||||||
|
redirectResponse.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}).on('error', reject);
|
||||||
|
} else {
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlink(patcherJava, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log(' Downloaded DualAuthPatcher.java');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` Failed to download DualAuthPatcher: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download ASM libraries if not present
|
||||||
|
*/
|
||||||
|
async ensureAsmLibraries(libDir) {
|
||||||
|
if (!fs.existsSync(libDir)) {
|
||||||
|
fs.mkdirSync(libDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const libs = [
|
||||||
|
{ name: 'asm-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm/9.6/asm-9.6.jar' },
|
||||||
|
{ name: 'asm-tree-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.6/asm-tree-9.6.jar' },
|
||||||
|
{ name: 'asm-util-9.6.jar', url: 'https://repo1.maven.org/maven2/org/ow2/asm/asm-util/9.6/asm-util-9.6.jar' }
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const lib of libs) {
|
||||||
|
const libPath = path.join(libDir, lib.name);
|
||||||
|
if (!fs.existsSync(libPath)) {
|
||||||
|
console.log(`Downloading ${lib.name}...`);
|
||||||
|
try {
|
||||||
|
const https = require('https');
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const file = fs.createWriteStream(libPath);
|
||||||
|
https.get(lib.url, (response) => {
|
||||||
|
response.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
fs.unlink(libPath, () => {});
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log(` Downloaded ${lib.name}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` Failed to download ${lib.name}: ${e.message}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile DualAuthPatcher if needed
|
||||||
|
*/
|
||||||
|
async compileDualAuthPatcher(java, patcherDir, libDir) {
|
||||||
|
const patcherClass = path.join(patcherDir, 'DualAuthPatcher.class');
|
||||||
|
const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
|
||||||
|
|
||||||
|
// Check if already compiled and up to date
|
||||||
|
if (fs.existsSync(patcherClass)) {
|
||||||
|
const classTime = fs.statSync(patcherClass).mtime;
|
||||||
|
const javaTime = fs.statSync(patcherJava).mtime;
|
||||||
|
if (classTime > javaTime) {
|
||||||
|
console.log('DualAuthPatcher already compiled');
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Compiling DualAuthPatcher...');
|
||||||
|
|
||||||
|
const javac = java.replace(/java(\.exe)?$/, 'javac$1');
|
||||||
|
const classpath = [
|
||||||
|
path.join(libDir, 'asm-9.6.jar'),
|
||||||
|
path.join(libDir, 'asm-tree-9.6.jar'),
|
||||||
|
path.join(libDir, 'asm-util-9.6.jar')
|
||||||
|
].join(process.platform === 'win32' ? ';' : ':');
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, {
|
||||||
|
stdio: 'pipe',
|
||||||
|
cwd: patcherDir
|
||||||
|
});
|
||||||
|
console.log(' Compilation successful');
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
const error = `Failed to compile DualAuthPatcher: ${e.message}`;
|
||||||
|
console.error(error);
|
||||||
|
if (e.stderr) console.error(e.stderr.toString());
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run DualAuthPatcher on the server JAR
|
||||||
|
*/
|
||||||
|
async runDualAuthPatcher(java, classpath, serverPath, domain) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const args = ['-cp', classpath, 'DualAuthPatcher', serverPath];
|
||||||
|
const env = { ...process.env, HYTALE_AUTH_DOMAIN: domain };
|
||||||
|
|
||||||
|
console.log(`Running: java ${args.join(' ')}`);
|
||||||
|
console.log(` HYTALE_AUTH_DOMAIN=${domain}`);
|
||||||
|
|
||||||
|
const proc = spawn(java, args, { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
proc.stdout.on('data', (data) => {
|
||||||
|
const str = data.toString();
|
||||||
|
stdout += str;
|
||||||
|
console.log(str.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on('data', (data) => {
|
||||||
|
const str = data.toString();
|
||||||
|
stderr += str;
|
||||||
|
console.error(str.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true, stdout });
|
||||||
|
} else {
|
||||||
|
resolve({ success: false, error: `Patcher exited with code ${code}: ${stderr}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
resolve({ success: false, error: `Failed to run patcher: ${err.message}` });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy server patcher (simple domain replacement, no dual auth)
|
||||||
|
* Use patchServer() for full dual auth support
|
||||||
|
*/
|
||||||
|
async patchServerLegacy(serverPath, progressCallback) {
|
||||||
|
const newDomain = this.getNewDomain();
|
||||||
|
const strategy = this.getDomainStrategy(newDomain);
|
||||||
|
|
||||||
|
console.log('=== Legacy Server Patcher ===');
|
||||||
|
console.log(`Target: ${serverPath}`);
|
||||||
|
console.log(`Domain: ${newDomain} (${newDomain.length} chars)`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(serverPath)) {
|
||||||
|
return { success: false, error: `Server JAR not found: ${serverPath}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progressCallback) progressCallback('Patching server...', 20);
|
||||||
|
|
||||||
console.log('Opening server JAR...');
|
console.log('Opening server JAR...');
|
||||||
let zip;
|
const zip = new AdmZip(serverPath);
|
||||||
try {
|
|
||||||
zip = new AdmZip(serverPath);
|
|
||||||
} catch (zipError) {
|
|
||||||
console.error('Failed to read server JAR:', zipError.message);
|
|
||||||
return { success: false, error: `Failed to read JAR: ${zipError.message}` };
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = zip.getEntries();
|
const entries = zip.getEntries();
|
||||||
console.log(`JAR contains ${entries.length} entries`);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Patching class files...', 40);
|
|
||||||
}
|
|
||||||
|
|
||||||
let totalCount = 0;
|
let totalCount = 0;
|
||||||
// For server JAR, we use UTF-8 and replace with the main domain part
|
|
||||||
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
const oldUtf8 = this.stringToUtf8(ORIGINAL_DOMAIN);
|
||||||
const newUtf8 = this.stringToUtf8(strategy.mainDomain);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const name = entry.entryName;
|
const name = entry.entryName;
|
||||||
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
if (name.endsWith('.class') || name.endsWith('.properties') ||
|
||||||
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
name.endsWith('.json') || name.endsWith('.xml') || name.endsWith('.yml')) {
|
||||||
|
|
||||||
const data = entry.getData();
|
const data = entry.getData();
|
||||||
|
|
||||||
if (data.includes(oldUtf8)) {
|
if (data.includes(oldUtf8)) {
|
||||||
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
const { buffer: patchedData, count } = this.findAndReplaceDomainUtf8(data, ORIGINAL_DOMAIN, strategy.mainDomain);
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
zip.updateFile(entry.entryName, patchedData);
|
zip.updateFile(entry.entryName, patchedData);
|
||||||
console.log(` Patched ${count} occurrences in ${name}`);
|
|
||||||
totalCount += count;
|
totalCount += count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalCount === 0) {
|
if (totalCount > 0) {
|
||||||
console.log('No occurrences of hytale.com found in server JAR entries');
|
zip.writeZip(serverPath);
|
||||||
return { success: true, patchCount: 0, warning: 'No domain occurrences found in JAR' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progressCallback) {
|
if (progressCallback) progressCallback('Complete', 100);
|
||||||
progressCallback('Writing patched JAR...', 80);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Writing patched JAR...');
|
|
||||||
const tempPath = serverPath + '.patched.tmp';
|
|
||||||
|
|
||||||
// Write to temp file first to avoid corruption
|
|
||||||
try {
|
|
||||||
zip.writeZip(tempPath);
|
|
||||||
|
|
||||||
// Replace original with patched version
|
|
||||||
if (fs.existsSync(serverPath)) {
|
|
||||||
fs.unlinkSync(serverPath);
|
|
||||||
}
|
|
||||||
fs.renameSync(tempPath, serverPath);
|
|
||||||
} catch (writeError) {
|
|
||||||
// Cleanup temp file if it exists
|
|
||||||
if (fs.existsSync(tempPath)) {
|
|
||||||
fs.unlinkSync(tempPath);
|
|
||||||
}
|
|
||||||
throw writeError;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.markAsPatched(serverPath);
|
|
||||||
|
|
||||||
if (progressCallback) {
|
|
||||||
progressCallback('Server patching complete', 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Successfully patched ${totalCount} occurrences in server`);
|
|
||||||
console.log('=== Server Patching Complete ===');
|
|
||||||
|
|
||||||
return { success: true, patchCount: totalCount };
|
return { success: true, patchCount: totalCount };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,8 +948,9 @@ class ClientPatcher {
|
|||||||
* Ensure both client and server are patched before launching
|
* Ensure both client and server are patched before launching
|
||||||
* @param {string} gameDir - Path to the game directory
|
* @param {string} gameDir - Path to the game directory
|
||||||
* @param {function} progressCallback - Optional callback for progress updates
|
* @param {function} progressCallback - Optional callback for progress updates
|
||||||
|
* @param {string} javaPath - Optional path to Java executable for server patching
|
||||||
*/
|
*/
|
||||||
async ensureClientPatched(gameDir, progressCallback) {
|
async ensureClientPatched(gameDir, progressCallback, javaPath = null) {
|
||||||
const results = {
|
const results = {
|
||||||
client: null,
|
client: null,
|
||||||
server: null,
|
server: null,
|
||||||
@@ -696,7 +981,7 @@ class ClientPatcher {
|
|||||||
if (progressCallback) {
|
if (progressCallback) {
|
||||||
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
|
||||||
}
|
}
|
||||||
});
|
}, javaPath);
|
||||||
} else {
|
} else {
|
||||||
console.warn('Could not find HytaleServer.jar');
|
console.warn('Could not find HytaleServer.jar');
|
||||||
results.server = { success: false, error: 'Server JAR not found' };
|
results.server = { success: false, error: 'Server JAR not found' };
|
||||||
|
|||||||
Reference in New Issue
Block a user