Files
ripster/backend/src/services/makemkvKeyService.js
T
2026-04-29 08:29:44 +00:00

216 lines
6.2 KiB
JavaScript

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
};