Compare commits

..

1 Commits

Author SHA1 Message Date
Alex
e7324eb176 Merge pull request #268 from amiayweb/macos-notarization
feat(macos): add code signing and notarization support
2026-02-03 17:06:27 +07:00
2 changed files with 187 additions and 87 deletions

View File

@@ -252,8 +252,8 @@ async function launchGame(playerNameOverride = null, progressCallback, javaPathO
if (patchResult.client) { if (patchResult.client) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`); console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
} }
if (patchResult.agent) { if (patchResult.server) {
console.log(` Agent: ${patchResult.agent.alreadyExists ? 'already present' : patchResult.agent.success ? 'downloaded' : 'failed'}`); console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
} }
} else { } else {
console.warn('Game patching failed:', patchResult.error); console.warn('Game patching failed:', patchResult.error);
@@ -408,17 +408,6 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
} }
} }
// DualAuth Agent: Set JAVA_TOOL_OPTIONS so java picks up -javaagent: flag
// This enables runtime auth patching without modifying the server JAR
const agentJar = path.join(gameLatest, 'Server', 'dualauth-agent.jar');
if (fs.existsSync(agentJar)) {
const agentFlag = `-javaagent:${agentJar}`;
env.JAVA_TOOL_OPTIONS = env.JAVA_TOOL_OPTIONS
? `${env.JAVA_TOOL_OPTIONS} ${agentFlag}`
: agentFlag;
console.log('DualAuth Agent: enabled via JAVA_TOOL_OPTIONS');
}
try { try {
let spawnOptions = { let spawnOptions = {
stdio: ['ignore', 'pipe', 'pipe'], stdio: ['ignore', 'pipe', 'pipe'],

View File

@@ -7,10 +7,6 @@ const ORIGINAL_DOMAIN = 'hytale.com';
const MIN_DOMAIN_LENGTH = 4; const MIN_DOMAIN_LENGTH = 4;
const MAX_DOMAIN_LENGTH = 16; const MAX_DOMAIN_LENGTH = 16;
// DualAuth ByteBuddy Agent (runtime class transformation, no JAR modification)
const DUALAUTH_AGENT_URL = 'https://github.com/sanasol/hytale-auth-server/releases/latest/download/dualauth-agent.jar';
const DUALAUTH_AGENT_FILENAME = 'dualauth-agent.jar';
function getTargetDomain() { function getTargetDomain() {
if (process.env.HYTALE_AUTH_DOMAIN) { if (process.env.HYTALE_AUTH_DOMAIN) {
return process.env.HYTALE_AUTH_DOMAIN; return process.env.HYTALE_AUTH_DOMAIN;
@@ -27,7 +23,7 @@ const DEFAULT_NEW_DOMAIN = 'auth.sanasol.ws';
/** /**
* Patches HytaleClient binary to replace hytale.com with custom domain * Patches HytaleClient binary to replace hytale.com with custom domain
* Server auth is handled by DualAuth ByteBuddy Agent (-javaagent: flag) * Server patching is done via pre-patched JAR download from CDN
* *
* 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) * - All F2P traffic routes to single endpoint: https://{domain} (no subdomains)
@@ -498,95 +494,211 @@ class ClientPatcher {
} }
/** /**
* Get the path to the DualAuth Agent JAR in a directory * Check if server JAR contains DualAuth classes (was patched)
*/ */
getAgentPath(dir) { serverJarContainsDualAuth(serverPath) {
return path.join(dir, DUALAUTH_AGENT_FILENAME); try {
const data = fs.readFileSync(serverPath);
// Check for DualAuthContext class signature in JAR
const signature = Buffer.from('DualAuthContext', 'utf8');
return data.includes(signature);
} catch (e) {
return false;
}
} }
/** /**
* Download DualAuth ByteBuddy Agent (replaces old pre-patched JAR approach) * Validate downloaded file is not corrupt/partial
* The agent provides runtime class transformation via -javaagent: flag * Server JAR should be at least 50MB
* No server JAR modification needed - original JAR stays pristine
*/ */
async ensureAgentAvailable(serverDir, progressCallback) { validateServerJarSize(serverPath) {
const agentPath = this.getAgentPath(serverDir);
console.log('=== DualAuth Agent (ByteBuddy) ===');
console.log(`Target: ${agentPath}`);
// Check if agent already exists and is valid
if (fs.existsSync(agentPath)) {
try { try {
const stats = fs.statSync(agentPath); const stats = fs.statSync(serverPath);
if (stats.size > 1024) { const minSize = 50 * 1024 * 1024; // 50MB minimum
console.log(`DualAuth Agent present (${(stats.size / 1024).toFixed(0)} KB)`); if (stats.size < minSize) {
if (progressCallback) progressCallback('DualAuth Agent ready', 100); console.error(` Downloaded JAR too small: ${(stats.size / 1024 / 1024).toFixed(2)} MB (expected >50MB)`);
return { success: true, agentPath, alreadyExists: true }; return false;
} }
// File exists but too small - corrupt, re-download console.log(` Downloaded size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
console.log('Agent file appears corrupt, re-downloading...'); return true;
fs.unlinkSync(agentPath);
} catch (e) { } catch (e) {
console.warn('Could not check agent file:', e.message); return false;
} }
} }
// Download agent from GitHub releases /**
if (progressCallback) progressCallback('Downloading DualAuth Agent...', 20); * Patch server JAR by downloading pre-patched version from CDN
console.log(`Downloading from: ${DUALAUTH_AGENT_URL}`); */
async patchServer(serverPath, progressCallback, branch = 'release') {
const newDomain = this.getNewDomain();
console.log('=== Server Patcher (Pre-patched Download) ===');
console.log(`Target: ${serverPath}`);
console.log(`Branch: ${branch}`);
console.log(`Domain: ${newDomain}`);
if (!fs.existsSync(serverPath)) {
const error = `Server JAR not found: ${serverPath}`;
console.error(error);
return { success: false, error };
}
// Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched';
let needsRestore = false;
if (fs.existsSync(patchFlagFile)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.domain === newDomain && flagData.branch === branch) {
// Verify JAR actually contains DualAuth classes (game may have auto-updated)
if (this.serverJarContainsDualAuth(serverPath)) {
console.log(`Server already patched for ${newDomain} (${branch}), skipping`);
if (progressCallback) progressCallback('Server already patched', 100);
return { success: true, alreadyPatched: true };
} else {
console.log(' Flag exists but JAR not patched (was auto-updated?), will re-download...');
// Delete stale flag file
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
}
} else {
console.log(`Server patched for "${flagData.domain}" (${flagData.branch}), need to change to "${newDomain}" (${branch})`);
needsRestore = true;
}
} catch (e) {
// Flag file corrupt, re-patch
console.log(' Flag file corrupt, will re-download');
try { fs.unlinkSync(patchFlagFile); } catch (e) { /* ignore */ }
}
}
// Restore backup if patched for different domain
if (needsRestore) {
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
if (progressCallback) progressCallback('Restoring original for domain change...', 5);
console.log('Restoring original JAR from backup for re-patching...');
fs.copyFileSync(backupPath, serverPath);
if (fs.existsSync(patchFlagFile)) {
fs.unlinkSync(patchFlagFile);
}
} else {
console.warn(' No backup found to restore - will download fresh patched JAR');
}
}
// Create backup
if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...');
const backupResult = this.backupClient(serverPath);
if (!backupResult) {
console.warn(' Could not create backup - proceeding without backup');
}
// Only support standard domain (auth.sanasol.ws) via pre-patched download
if (newDomain !== 'auth.sanasol.ws' && newDomain !== 'sanasol.ws') {
console.error(`Domain "${newDomain}" requires DualAuthPatcher - only auth.sanasol.ws is supported via pre-patched download`);
return { success: false, error: `Unsupported domain: ${newDomain}. Only auth.sanasol.ws is supported.` };
}
// Download pre-patched JAR
if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
console.log('Downloading pre-patched HytaleServer.jar...');
try { try {
// Ensure server directory exists let url;
if (!fs.existsSync(serverDir)) { if (branch === 'pre-release') {
fs.mkdirSync(serverDir, { recursive: true }); url = 'https://patcher.authbp.xyz/download/patched_prerelease';
console.log(' Using pre-release patched server from:', url);
} else {
url = 'https://patcher.authbp.xyz/download/patched_release';
console.log(' Using release patched server from:', url);
} }
const tmpPath = agentPath + '.tmp'; const file = fs.createWriteStream(serverPath);
const file = fs.createWriteStream(tmpPath); let totalSize = 0;
let downloaded = 0;
const stream = await smartDownloadStream(DUALAUTH_AGENT_URL, (chunk, downloadedBytes, total) => { const stream = await smartDownloadStream(url, (chunk, downloadedBytes, total) => {
if (progressCallback && total) { downloaded = downloadedBytes;
const percent = 20 + Math.floor((downloadedBytes / total) * 70); totalSize = total;
progressCallback(`Downloading agent... ${(downloadedBytes / 1024).toFixed(0)} KB`, percent); if (progressCallback && totalSize) {
const percent = 30 + Math.floor((downloaded / totalSize) * 60);
progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
} }
}); });
stream.pipe(file); stream.pipe(file);
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
file.on('finish', () => { file.close(); resolve(); }); file.on('finish', () => {
file.close();
resolve();
});
file.on('error', reject); file.on('error', reject);
stream.on('error', reject); stream.on('error', reject);
}); });
// Verify download console.log(' Download successful');
const stats = fs.statSync(tmpPath);
if (stats.size < 1024) { // Verify downloaded JAR size and contents
fs.unlinkSync(tmpPath); if (progressCallback) progressCallback('Verifying downloaded JAR...', 95);
const error = 'Downloaded agent too small (corrupt or failed download)';
console.error(error); if (!this.validateServerJarSize(serverPath)) {
return { success: false, error }; console.error('Downloaded JAR appears corrupt or incomplete');
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
} }
// Atomic move return { success: false, error: 'Downloaded JAR verification failed - file too small (corrupt/partial download)' };
if (fs.existsSync(agentPath)) {
fs.unlinkSync(agentPath);
} }
fs.renameSync(tmpPath, agentPath);
console.log(`DualAuth Agent downloaded (${(stats.size / 1024).toFixed(0)} KB)`); if (!this.serverJarContainsDualAuth(serverPath)) {
if (progressCallback) progressCallback('DualAuth Agent ready', 100); console.error('Downloaded JAR does not contain DualAuth classes - invalid or corrupt download');
return { success: true, agentPath };
// Restore backup on verification failure
const backupPath = serverPath + '.original';
if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after verification failure');
}
return { success: false, error: 'Downloaded JAR verification failed - missing DualAuth classes' };
}
console.log(' Verification successful - DualAuth classes present');
// Mark as patched
const sourceUrl = branch === 'pre-release'
? 'https://patcher.authbp.xyz/download/patched_prerelease'
: 'https://patcher.authbp.xyz/download/patched_release';
fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain,
branch: branch,
patchedAt: new Date().toISOString(),
patcher: 'PrePatchedDownload',
source: sourceUrl
}));
if (progressCallback) progressCallback('Server patching complete', 100);
console.log('=== Server Patching Complete ===');
return { success: true, patchCount: 1 };
} catch (downloadError) { } catch (downloadError) {
console.error(`Failed to download DualAuth Agent: ${downloadError.message}`); console.error(`Failed to download patched JAR: ${downloadError.message}`);
// Clean up temp file
const tmpPath = agentPath + '.tmp'; // Restore backup on failure
if (fs.existsSync(tmpPath)) { const backupPath = serverPath + '.original';
try { fs.unlinkSync(tmpPath); } catch (e) { /* ignore */ } if (fs.existsSync(backupPath)) {
fs.copyFileSync(backupPath, serverPath);
console.log('Restored backup after download failure');
} }
return { success: false, error: downloadError.message };
return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
} }
} }
@@ -631,12 +743,12 @@ class ClientPatcher {
} }
/** /**
* Ensure client is patched and DualAuth Agent is available before launching * Ensure both client and server are patched before launching
*/ */
async ensureClientPatched(gameDir, progressCallback, javaPath = null, branch = 'release') { async ensureClientPatched(gameDir, progressCallback, javaPath = null, branch = 'release') {
const results = { const results = {
client: null, client: null,
agent: null, server: null,
success: true success: true
}; };
@@ -653,23 +765,22 @@ class ClientPatcher {
results.client = { success: false, error: 'Client binary not found' }; results.client = { success: false, error: 'Client binary not found' };
} }
// Download DualAuth ByteBuddy Agent (runtime patching, no JAR modification) const serverPath = this.findServerPath(gameDir);
const serverDir = path.join(gameDir, 'Server'); if (serverPath) {
if (fs.existsSync(serverDir)) { if (progressCallback) progressCallback('Patching server JAR...', 50);
if (progressCallback) progressCallback('Checking DualAuth Agent...', 50); results.server = await this.patchServer(serverPath, (msg, pct) => {
results.agent = await this.ensureAgentAvailable(serverDir, (msg, pct) => {
if (progressCallback) { if (progressCallback) {
progressCallback(`Agent: ${msg}`, pct ? 50 + pct / 2 : null); progressCallback(`Server: ${msg}`, pct ? 50 + pct / 2 : null);
} }
}); }, branch);
} else { } else {
console.warn('Server directory not found, skipping agent download'); console.warn('Could not find HytaleServer.jar');
results.agent = { success: true, skipped: true }; results.server = { success: false, error: 'Server JAR not found' };
} }
results.success = (results.client && results.client.success) || (results.agent && results.agent.success); results.success = (results.client && results.client.success) || (results.server && results.server.success);
results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.agent && results.agent.alreadyExists); results.alreadyPatched = (results.client && results.client.alreadyPatched) && (results.server && results.server.alreadyPatched);
results.patchCount = results.client ? results.client.patchCount || 0 : 0; results.patchCount = (results.client ? results.client.patchCount || 0 : 0) + (results.server ? results.server.patchCount || 0 : 0);
if (progressCallback) progressCallback('Patching complete', 100); if (progressCallback) progressCallback('Patching complete', 100);