const fs = require('fs'); const os = require('os'); const path = require('path'); const { execFile } = require('child_process'); const logger = require('./logger').child('MAKEMKV_KEY'); const MAKEMKV_BETA_KEY_API_URL = 'https://cable.ayra.ch/makemkv/api.php?json'; const MAKEMKV_BETA_KEY_API_USER_AGENT = process.env.MAKEMKV_BETA_KEY_API_USER_AGENT || 'Ripster/1.0 (MakeMKV beta key sync)'; const MAKEMKV_BETA_KEY_API_FROM = process.env.MAKEMKV_BETA_KEY_API_FROM || 'admin@example.invalid'; const MAKEMKV_BETA_KEY_API_TIMEOUT_MS = Math.max( 1000, Number(process.env.MAKEMKV_BETA_KEY_API_TIMEOUT_MS || 15000) ); function normalizeRegistrationKey(rawValue) { return String(rawValue || '').trim(); } function getMakeMKVConfigDir(homeDir = os.homedir()) { return path.join(String(homeDir || '').trim() || os.homedir(), '.MakeMKV'); } function getMakeMKVSettingsFilePath(homeDir = os.homedir()) { return path.join(getMakeMKVConfigDir(homeDir), 'settings.conf'); } function escapeKeyForSettingsFile(value) { return String(value || '') .replace(/\\/g, '\\\\') .replace(/"/g, '\\"'); } function buildUpdatedSettingsContent(currentContent, registrationKey) { const existingLines = String(currentContent || '') .split(/\r?\n/) .filter((line) => !/^\s*app_Key\s*=/.test(line)); const normalizedKey = normalizeRegistrationKey(registrationKey); while (existingLines.length > 0 && existingLines[existingLines.length - 1] === '') { existingLines.pop(); } if (normalizedKey) { existingLines.push(`app_Key = "${escapeKeyForSettingsFile(normalizedKey)}"`); } return existingLines.length > 0 ? `${existingLines.join('\n')}\n` : ''; } async function syncRegistrationKeyToConfig(rawValue, options = {}) { const normalizedKey = normalizeRegistrationKey(rawValue); const homeDir = String(options?.homeDir || '').trim() || os.homedir(); const configDir = getMakeMKVConfigDir(homeDir); const settingsFilePath = getMakeMKVSettingsFilePath(homeDir); const fileExists = fs.existsSync(settingsFilePath); const currentContent = fileExists ? await fs.promises.readFile(settingsFilePath, 'utf8') : ''; const nextContent = buildUpdatedSettingsContent(currentContent, normalizedKey); if (!normalizedKey && !fileExists) { return { changed: false, path: settingsFilePath, hasKey: false }; } await fs.promises.mkdir(configDir, { recursive: true }); if (nextContent) { await fs.promises.writeFile(settingsFilePath, nextContent, 'utf8'); await fs.promises.chmod(settingsFilePath, 0o600).catch(() => {}); logger.info('settings-conf:key-synced', { path: settingsFilePath, hasKey: true }); } else { await fs.promises.writeFile(settingsFilePath, '', 'utf8'); await fs.promises.chmod(settingsFilePath, 0o600).catch(() => {}); logger.info('settings-conf:key-cleared', { path: settingsFilePath, hasKey: false }); } return { changed: currentContent !== nextContent, path: settingsFilePath, hasKey: Boolean(normalizedKey) }; } async function fetchCurrentBetaKey() { const errors = []; try { return await fetchCurrentBetaKeyViaCurl(); } catch (error) { errors.push(error?.message || String(error)); logger.warn('beta-key:curl-failed', { error: error?.message || String(error) }); } try { return await fetchCurrentBetaKeyViaFetch(); } catch (error) { errors.push(error?.message || String(error)); logger.warn('beta-key:fetch-failed', { error: error?.message || String(error) }); } const error = new Error(`Betakey konnte nicht geladen werden. Details: ${errors.join(' | ')}`); error.statusCode = 502; throw error; } function parseBetaKeyResponse(rawBody) { let payload = null; try { payload = JSON.parse(String(rawBody || '{}')); } catch (_error) { const error = new Error('Betakey-Antwort ist kein gültiges JSON.'); error.statusCode = 502; throw error; } const betaKey = String(payload?.key || '').trim(); if (!/^[A-Za-z0-9][A-Za-z0-9-]{15,}$/.test(betaKey)) { const error = new Error('Betakey-Antwort ist ungültig.'); error.statusCode = 502; throw error; } return { key: betaKey, sourceUrl: MAKEMKV_BETA_KEY_API_URL }; } function fetchCurrentBetaKeyViaCurl() { return new Promise((resolve, reject) => { execFile( 'curl', [ '-fsSL', '--max-time', String(Math.ceil(MAKEMKV_BETA_KEY_API_TIMEOUT_MS / 1000)), '-A', MAKEMKV_BETA_KEY_API_USER_AGENT, '-H', `From: ${MAKEMKV_BETA_KEY_API_FROM}`, '-H', 'Accept: text/plain, text/html;q=0.9, */*;q=0.8', '-H', 'Accept-Language: de,en-US;q=0.7,en;q=0.3', '-H', 'Cache-Control: no-cache', '-H', 'Pragma: no-cache', MAKEMKV_BETA_KEY_API_URL ], { timeout: MAKEMKV_BETA_KEY_API_TIMEOUT_MS, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { if (error) { const resolvedError = new Error( `curl fehlgeschlagen (${error.code || 'unknown'}): ${String(stderr || error.message || '').trim() || 'keine Details'}` ); resolvedError.statusCode = 502; reject(resolvedError); return; } try { resolve(parseBetaKeyResponse(stdout)); } catch (parseError) { reject(parseError); } } ); }); } async function fetchCurrentBetaKeyViaFetch() { const response = await fetch(MAKEMKV_BETA_KEY_API_URL, { headers: { 'User-Agent': MAKEMKV_BETA_KEY_API_USER_AGENT, 'From': MAKEMKV_BETA_KEY_API_FROM, 'Accept': 'text/plain, text/html;q=0.9, */*;q=0.8', 'Accept-Language': 'de,en-US;q=0.7,en;q=0.3', 'Cache-Control': 'no-cache', 'Pragma': 'no-cache' }, signal: AbortSignal.timeout(MAKEMKV_BETA_KEY_API_TIMEOUT_MS) }); if (!response.ok) { const error = new Error(`HTTP ${response.status}`); error.statusCode = 502; throw error; } const rawBody = await response.text(); return parseBetaKeyResponse(rawBody); } module.exports = { MAKEMKV_BETA_KEY_API_URL, fetchCurrentBetaKey, getMakeMKVConfigDir, getMakeMKVSettingsFilePath, normalizeRegistrationKey, syncRegistrationKeyToConfig };