From eba447cd069e15b53afbc7bc9dab2a11d11bce3e Mon Sep 17 00:00:00 2001 From: zefie Date: Sun, 3 May 2026 14:51:59 -0400 Subject: [PATCH] buncha stuff, v0.9.74 --- QuickSetup.md | 57 ++++ zefie_wtvp_minisrv/app.js | 9 +- .../includes/classes/WTV-MSNTV2.js | 28 +- zefie_wtvp_minisrv/package-lock.json | 4 +- zefie_wtvp_minisrv/package.json | 3 +- zefie_wtvp_minisrv/tools/configurator.js | 215 +++++++++++++++ zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js | 2 + .../tools/update_user_data_key.js | 257 ++++++++++++++++++ zefie_wtvp_minisrv/user_config.example.json | 3 + 9 files changed, 559 insertions(+), 19 deletions(-) create mode 100644 QuickSetup.md create mode 100644 zefie_wtvp_minisrv/tools/configurator.js create mode 100644 zefie_wtvp_minisrv/tools/update_user_data_key.js diff --git a/QuickSetup.md b/QuickSetup.md new file mode 100644 index 00000000..a4cdece3 --- /dev/null +++ b/QuickSetup.md @@ -0,0 +1,57 @@ +# Quick Setup + +## user_config.json + +`user_config.json` (in the project root) is where you put your local configuration overrides. It merges on top of `includes/config.json` — **do not edit `includes/config.json` directly**. + +You only need to include keys you want to override. Copy `user_config.example.json` as a starting point, or start with a minimal file: + +```json +{ + "config": { + "service_ip": "192.168.1.x" + } +} +``` + +The file supports `// line comments` and `/* block comments */`. + +--- + +## configurator.js + +`tools/configurator.js` is a command-line tool that sets or deletes individual keys in `user_config.json` without manually editing JSON. + +**Usage:** +``` +node tools/configurator.js [--overwrite] +node tools/configurator.js --delete [--overwrite] +``` + +- Use `--overwrite` to replace a key that already exists. +- Keys are expressed as dot-separated paths (e.g. `config.keys.user_data_key`). + +--- + +## Setting service_ip + +`service_ip` tells the box where to connect, this CANNOT be `0.0.0.0`, and must be an address reachable by your box when it connects via your setup. Can be `127.0.0.1` if you are running MAME/Viewer on the same machine as minisrv. + +``` +node tools/configurator.js config.bind_ip 192.168.1.x --overwrite +``` + +--- + +## Setting user_data_key + +`user_data_key` is used to encrypt user data. It should be a random secret string and **must be set before registering any users**. Changing it after users have registered will break existing accounts. + +``` +node tools/configurator.js config.keys.user_data_key YOUR_RANDOM_SECRET --overwrite +``` + +To generate a random key: +``` +openssl rand -base64 32 +``` diff --git a/zefie_wtvp_minisrv/app.js b/zefie_wtvp_minisrv/app.js index daf8d916..9b3c77a6 100644 --- a/zefie_wtvp_minisrv/app.js +++ b/zefie_wtvp_minisrv/app.js @@ -2683,4 +2683,11 @@ if (bind_ports.length > 0) console.log(` * Started WTVP Server on port${bind_por if (pc_bind_ports.length > 0) console.log(` * Started HTTP Server on port${pc_bind_ports.length !== 1 ? "s" : ""} ` + pc_bind_ports.join(", ") + "..."); if (protocolHandledPorts.size > 0) console.log(` * Started ${protocolHandledPorts.size} specialized protocol handler${protocolHandledPorts.size !== 1 ? "s" : ""} on port${protocolHandledPorts.size !== 1 ? "s" : ""} ` + [...protocolHandledPorts].map(([sn, sp, pt]) => `${pt} (${sp.toUpperCase()})`).join(", ") + "..."); const listening_ip_string = (minisrv_config.config.bind_ip !== "0.0.0.0") ? "IP: " + minisrv_config.config.bind_ip : "all interfaces"; -console.log(" * Listening on", listening_ip_string, "~", "Service IP:", service_ip); \ No newline at end of file +console.log(" * Listening on", listening_ip_string, "~", "Service IP:", service_ip); + +if (minisrv_config.config.keys.user_data_key === "PNa$WN7gz}!T=t6X7^=|Ii##CEB~p\\EP") { + console.log(" * WARNING: You are using the default user data encryption key. This is not secure, and you should change it in the configuration file before registering any users."); + console.log(" * To generate a random key in bash or PowerShell, you can run: node ./tools/configurator.js config.keys.user_data_key $(openssl rand -base64 32)"); + console.log(" * After changing the key in the user_config, you can run tools/update_user_data_key.js to update existing accounts with the new key."); + console.log(" * Making a backup of your user accounts before doing this is highly recommended, in case something goes wrong during the update process."); +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/includes/classes/WTV-MSNTV2.js b/zefie_wtvp_minisrv/includes/classes/WTV-MSNTV2.js index 8b2cd0e3..ee7254d7 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTV-MSNTV2.js +++ b/zefie_wtvp_minisrv/includes/classes/WTV-MSNTV2.js @@ -129,7 +129,7 @@ class WTVMSNTV2 { socket.rawDataListener = (chunk) => this.handleData(socket, chunk); socket.on('data', socket.rawDataListener); socket.on('error', (err) => { - if (this.service_config.show_verbose_errors) console.error('[WTV-MSNTV2] socket error:', err.message); + if (this.service_config.debug) console.error('[WTV-MSNTV2] socket error:', err.message); }); } @@ -202,14 +202,14 @@ class WTVMSNTV2 { const userAgent = request_headers['User-Agent'] || request_headers['user-agent'] || ''; if (!this.isAllowedUserAgent(userAgent)) { - if (this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3) { + if (this.service_config.debug || this.minisrv_config.config.verbosity >= 3) { console.warn('[WTV-MSNTV2] unsupported User-Agent rejected:', userAgent || ''); } this.writeError(socket, 403, 'Forbidden', request_headers); return; } - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; if (verbose) { console.log('[WTV-MSNTV2] incoming request:', requestLine); if (body.length) { @@ -250,7 +250,7 @@ class WTVMSNTV2 { handleConnect(socket, requestUrl) { const [host, portString] = requestUrl.split(':'); const port = parseInt(portString, 10) || 443; - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; const connectIntercept = this.getConnectIntercept(host); if (verbose) console.log('[WTV-MSNTV2] CONNECT request:', requestUrl, 'intercept:', !!connectIntercept); @@ -276,13 +276,13 @@ class WTVMSNTV2 { }); remote.on('error', (err) => { - if (this.service_config.show_verbose_errors) console.error('[WTV-MSNTV2] CONNECT error:', err.message); + if (this.service_config.debug) console.error('[WTV-MSNTV2] CONNECT error:', err.message); this.writeError(socket, 502, 'Bad Gateway'); }); } setupTlsSocket(tlsSocket) { - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; const sslDebug = this.sslv2Debug; tlsSocket.on('secureConnect', () => { if (sslDebug) console.log('[WTV-MSNTV2] TLS handshake complete for intercepted CONNECT', tlsSocket.connectIntercept.match); @@ -302,7 +302,7 @@ class WTVMSNTV2 { } setupForgeTls(socket, connectIntercept) { - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; const sslDebug = this.sslv2Debug; const creds = this.forgeTlsCredentials; if (!creds) { @@ -1234,7 +1234,7 @@ class WTVMSNTV2 { const requestLine = headerLines.shift(); const requestParts = requestLine.split(' '); if (requestParts.length < 3) { - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; if (verbose) { console.warn('[WTV-MSNTV2] TLS invalid request line:', requestLine); console.warn('[WTV-MSNTV2] TLS raw header block:', headerBlock); @@ -1280,7 +1280,7 @@ class WTVMSNTV2 { }; Object.assign(request_headers, headers); - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; if (verbose) { console.log('[WTV-MSNTV2] decrypted request:', requestLine); console.log('[WTV-MSNTV2] decrypted headers:\n' + rawHeaders.join('\r\n')); @@ -1440,14 +1440,14 @@ class WTVMSNTV2 { const candidate = this.wtvshared.makeSafePath(base, ''); // Exact match first if (candidate && fs.existsSync(candidate) && fs.lstatSync(candidate).isFile()) { - if (this.service_config.show_verbose_errors) console.log('[WTV-MSNTV2] local intercept:', candidate); + if (this.service_config.debug) console.log('[WTV-MSNTV2] local intercept:', candidate); return candidate; } // Dynamic suffixes: e.g. kickstart.aspx -> kickstart.aspx.js for (const suffix of dynSuffixes) { const dynCandidate = this.wtvshared.makeSafePath(base + suffix, ''); if (dynCandidate && fs.existsSync(dynCandidate) && fs.lstatSync(dynCandidate).isFile()) { - if (this.service_config.show_verbose_errors) console.log('[WTV-MSNTV2] local intercept (dynamic):', dynCandidate); + if (this.service_config.debug) console.log('[WTV-MSNTV2] local intercept (dynamic):', dynCandidate); return dynCandidate; } } @@ -1753,14 +1753,14 @@ class WTVMSNTV2 { target = new url.URL(requestUrl); } } catch (err) { - if (this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3) { + if (this.service_config.debug || this.minisrv_config.config.verbosity >= 3) { console.error('[WTV-MSNTV2] invalid URL:', requestUrl, err.message); } this.writeError(socket, 400, 'Bad Request', request_headers); return; } - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; const isHttps = target.protocol === 'https:'; const agent = isHttps ? https : http; const requestPath = target.pathname + (target.search || ''); @@ -1793,7 +1793,7 @@ class WTVMSNTV2 { const maxResponseBytes = this.maxProxyResponseBytes; const proxyReq = agent.request(options, (res) => { - const verbose = this.service_config.show_verbose_errors || this.minisrv_config.config.verbosity >= 3; + const verbose = this.service_config.debug || this.minisrv_config.config.verbosity >= 3; if (verbose) { console.log('[WTV-MSNTV2] upstream response:', res.statusCode, res.statusMessage); console.log('[WTV-MSNTV2] upstream response headers:', JSON.stringify(res.headers)); diff --git a/zefie_wtvp_minisrv/package-lock.json b/zefie_wtvp_minisrv/package-lock.json index 07df53dd..b734a848 100644 --- a/zefie_wtvp_minisrv/package-lock.json +++ b/zefie_wtvp_minisrv/package-lock.json @@ -1,12 +1,12 @@ { "name": "zefie_wtvp_minisrv", - "version": "0.9.73", + "version": "0.9.74", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zefie_wtvp_minisrv", - "version": "0.9.73", + "version": "0.9.74", "license": "GPL3", "dependencies": { "@serialport/parser-readline": "^13.0.0", diff --git a/zefie_wtvp_minisrv/package.json b/zefie_wtvp_minisrv/package.json index 092f81c4..058f2c1c 100644 --- a/zefie_wtvp_minisrv/package.json +++ b/zefie_wtvp_minisrv/package.json @@ -1,13 +1,12 @@ { "name": "zefie_wtvp_minisrv", - "version": "0.9.73", + "version": "0.9.74", "description": "WebTV Service (WTVP) Emulation Server", "main": "app.js", "homepage": "https://github.com/zefie/zefie_wtvp_minisrv", "license": "GPL3", "scripts": { "start": "node app.js", - "irc": "node irconly.js", "test": "node test.js", "debug": "cross-env DEBUG=* node app.js", "modem-proxy": "node modem_proxy.js" diff --git a/zefie_wtvp_minisrv/tools/configurator.js b/zefie_wtvp_minisrv/tools/configurator.js new file mode 100644 index 00000000..3711328b --- /dev/null +++ b/zefie_wtvp_minisrv/tools/configurator.js @@ -0,0 +1,215 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const readline = require('readline'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const USER_CONFIG_PATH = path.join(ROOT_DIR, 'user_config.json'); + +function printUsage() { + console.log('Usage:'); + console.log(' node tools/configurator.js [--overwrite]'); + console.log(' node tools/configurator.js --delete [--overwrite]'); + console.log('Examples:'); + console.log(' node tools/configurator.js config.keys.user_data_key mynewkey'); + console.log(' node tools/configurator.js config.passwords.enabled true --overwrite'); + console.log(' node tools/configurator.js config.fake.newkey --delete --overwrite'); +} + +function parseJsonWithComments(json) { + if (typeof json !== 'string') json = json ? json.toString() : ''; + let result = ''; + let i = 0; + let isString = false; + let isEscape = false; + let isBlockComment = false; + let isLineComment = false; + + while (i < json.length) { + const char = json[i]; + const nextChar = json[i + 1]; + + if (!isString && !isEscape && char === '/' && nextChar === '*') { + isBlockComment = true; + i += 1; + } else if (isBlockComment && char === '*' && nextChar === '/') { + isBlockComment = false; + i += 1; + } else if (!isString && !isEscape && char === '/' && nextChar === '/') { + isLineComment = true; + i += 1; + } else if (isLineComment && (char === '\n' || char === '\r')) { + isLineComment = false; + } else if (!isBlockComment && !isLineComment) { + if (char === '"' && !isEscape) { + isString = !isString; + } + isEscape = char === '\\' && !isEscape; + result += char; + } + i += 1; + } + + return JSON.parse(result); +} + +function parseInputValue(raw) { + const trimmed = raw.trim(); + const isLiteral = /^(?:-?\d+(?:\.\d+)?|true|false|null)$/i.test(trimmed); + const startsLikeJson = trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"'); + + if (!isLiteral && !startsLikeJson) { + return raw; + } + + try { + return JSON.parse(trimmed); + } catch (_error) { + return raw; + } +} + +function readUserConfig(filePath) { + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, '{}\n', 'utf8'); + return {}; + } + + const content = fs.readFileSync(filePath, 'utf8'); + if (!content.trim()) return {}; + + try { + return parseJsonWithComments(content); + } catch (error) { + throw new Error(`Failed to parse user_config.json: ${error.message}`); + } +} + +function askYesNo(question) { + return new Promise((resolve) => { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + rl.question(question, (answer) => { + rl.close(); + const normalized = String(answer || '').trim().toLowerCase(); + resolve(normalized === 'y' || normalized === 'yes'); + }); + }); +} + +function sortKeys(obj) { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj; + return Object.keys(obj).sort().reduce((sorted, key) => { + sorted[key] = sortKeys(obj[key]); + return sorted; + }, {}); +} + +function getPathInfo(rootObj, parts) { + let current = rootObj; + + for (let i = 0; i < parts.length - 1; i += 1) { + const part = parts[i]; + + if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) { + current[part] = {}; + } + + current = current[part]; + } + + const leafKey = parts[parts.length - 1]; + const exists = Object.prototype.hasOwnProperty.call(current, leafKey); + const oldValue = exists ? current[leafKey] : undefined; + + return { + parent: current, + leafKey, + exists, + oldValue + }; +} + +async function main() { + const args = process.argv.slice(2); + const overwrite = args.includes('--overwrite'); + const deleteMode = args.includes('--delete'); + const positional = args.filter((arg) => arg !== '--overwrite' && arg !== '--delete'); + + if ((deleteMode && positional.length < 1) || (!deleteMode && positional.length < 2)) { + printUsage(); + process.exit(1); + } + + const dotPath = positional[0]; + const valueRaw = deleteMode ? null : positional.slice(1).join(' '); + const pathParts = dotPath.split('.').filter(Boolean); + + if (pathParts.length === 0) { + throw new Error('Invalid path. Provide a dot-separated path like config.keys.user_data_key'); + } + + if (deleteMode && pathParts.length === 1) { + const rootKey = pathParts[0]; + if (rootKey === 'config' || rootKey === 'services') { + throw new Error('Deletion of root keys "config" and "services" is not allowed.'); + } + } + + const config = readUserConfig(USER_CONFIG_PATH); + + if (deleteMode) { + const pathInfo = getPathInfo(config, pathParts); + if (!pathInfo.exists) { + console.log('Key does not exist. No changes made.'); + return; + } + + if (!overwrite) { + const question = `Delete ${dotPath}? [y/N] `; + const approved = await askYesNo(question); + if (!approved) { + console.log('No changes made.'); + return; + } + } + + delete pathInfo.parent[pathInfo.leafKey]; + fs.writeFileSync(USER_CONFIG_PATH, `${JSON.stringify(sortKeys(config), null, 2)}\n`, 'utf8'); + console.log(`Deleted ${dotPath}.`); + return; + } + + const pathInfo = getPathInfo(config, pathParts); + const newValue = parseInputValue(valueRaw); + + if (pathInfo.exists && !overwrite) { + const question = `Key ${dotPath} already exists. Overwrite? [y/N] `; + const approved = await askYesNo(question); + + if (!approved) { + console.log('No changes made.'); + return; + } + } + + pathInfo.parent[pathInfo.leafKey] = newValue; + + fs.writeFileSync(USER_CONFIG_PATH, `${JSON.stringify(sortKeys(config), null, 2)}\n`, 'utf8'); + + if (pathInfo.exists) { + console.log(`Updated ${dotPath}.`); + } else { + console.log(`Created ${dotPath}.`); + } +} + +main().catch((error) => { + console.error(error.message || error); + process.exit(1); +}); \ No newline at end of file diff --git a/zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js b/zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js index 280ab844..410fc606 100644 --- a/zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js +++ b/zefie_wtvp_minisrv/tools/encode_webtv_mpeg.js @@ -4,6 +4,8 @@ /** * WebTV MPEG-1 PS Encoder * + * This tool is incomplete, and may not generate correct WebTV MPEG yet + * * Two-pass pipeline: * 1. ffmpeg encodes input to MPEG-1 PS (codec settings only) * 2. ES extracted via structure-aware pack walk (never naive payload scan) diff --git a/zefie_wtvp_minisrv/tools/update_user_data_key.js b/zefie_wtvp_minisrv/tools/update_user_data_key.js new file mode 100644 index 00000000..56b4ce1f --- /dev/null +++ b/zefie_wtvp_minisrv/tools/update_user_data_key.js @@ -0,0 +1,257 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const CryptoJS = require('crypto-js'); + +const ROOT_DIR = path.resolve(__dirname, '..'); +const DEFAULT_CONFIG_PATH = path.join(ROOT_DIR, 'includes', 'config.json'); +const USER_CONFIG_PATH = path.join(ROOT_DIR, 'user_config.json'); + +function parseJsonWithComments(json) { + if (typeof json !== 'string') json = json ? json.toString() : ''; + let result = ''; + let i = 0; + let isString = false; + let isEscape = false; + let isBlockComment = false; + let isLineComment = false; + + while (i < json.length) { + const char = json[i]; + const nextChar = json[i + 1]; + + if (!isString && !isEscape && char === '/' && nextChar === '*') { + isBlockComment = true; + i += 1; + } else if (isBlockComment && char === '*' && nextChar === '/') { + isBlockComment = false; + i += 1; + } else if (!isString && !isEscape && char === '/' && nextChar === '/') { + isLineComment = true; + i += 1; + } else if (isLineComment && (char === '\n' || char === '\r')) { + isLineComment = false; + } else if (!isBlockComment && !isLineComment) { + if (char === '"' && !isEscape) { + isString = !isString; + } + isEscape = char === '\\' && !isEscape; + result += char; + } + i += 1; + } + + return JSON.parse(result); +} + +function readJsonWithComments(filePath, required) { + if (!fs.existsSync(filePath)) { + if (required) throw new Error(`Required file not found: ${filePath}`); + return {}; + } + + const raw = fs.readFileSync(filePath, 'utf8'); + return parseJsonWithComments(raw); +} + +function integrateConfig(main, user) { + const out = Array.isArray(main) ? main.slice() : { ...main }; + for (const key of Object.keys(user || {})) { + const userVal = user[key]; + if (userVal && typeof userVal === 'object' && !Array.isArray(userVal)) { + out[key] = integrateConfig(out[key] || {}, userVal); + } else { + out[key] = userVal; + } + } + return out; +} + +function isAbsoluteLike(p) { + return /^(?:[a-zA-Z]:)?[\\/]/.test(p); +} + +function resolveFromRoot(p) { + if (isAbsoluteLike(p)) return path.normalize(p); + return path.resolve(ROOT_DIR, p); +} + +function listUserJsonFiles(accountsRoot) { + const files = []; + + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + continue; + } + + if (/^user\d+\.json$/i.test(entry.name)) { + files.push(fullPath); + } + } + } + + if (fs.existsSync(accountsRoot)) walk(accountsRoot); + return files; +} + +function decryptWithKey(value, key) { + return CryptoJS.AES.decrypt(value, key).toString(CryptoJS.enc.Utf8); +} + +function encryptWithKey(value, key) { + return CryptoJS.AES.encrypt(value, key).toString(); +} + +function isPrintableAndSane(str) { + if (typeof str !== 'string') return false; + if (str.length === 0 || str.length > 512) return false; + if (str.includes('\uFFFD')) return false; + if (/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/.test(str)) return false; + return true; +} + +function isSaneDecryptedValue(fieldName, decrypted) { + if (!isPrintableAndSane(decrypted)) return false; + + if (fieldName === 'subscriber_password') { + const isSha512Hex = /^[a-f0-9]{128}$/i.test(decrypted); + if (isSha512Hex) return true; + return decrypted.length <= 128; + } + + return true; +} + +function hasNestedKey(obj, pathParts) { + let cur = obj; + for (const part of pathParts) { + if (!cur || typeof cur !== 'object' || !Object.prototype.hasOwnProperty.call(cur, part)) return false; + cur = cur[part]; + } + return true; +} + +function parseArgs() { + const args = process.argv.slice(2); + let oldKey = null; + + for (let i = 0; i < args.length; i += 1) { + if (args[i] === '--oldkey' && args[i + 1]) { + oldKey = args[i + 1]; + i += 1; + } else if (args[i].startsWith('--oldkey=')) { + oldKey = args[i].slice('--oldkey='.length); + } + } + + return { oldKey }; +} + +function main() { + const { oldKey } = parseArgs(); + + const defaultConfig = readJsonWithComments(DEFAULT_CONFIG_PATH, true); + const userConfig = readJsonWithComments(USER_CONFIG_PATH, false); + + const defaultKey = oldKey || + (defaultConfig && defaultConfig.config && defaultConfig.config.keys + ? defaultConfig.config.keys.user_data_key + : null); + + const userHasKey = hasNestedKey(userConfig, ['config', 'keys', 'user_data_key']); + const userKey = userHasKey ? userConfig.config.keys.user_data_key : null; + + if (oldKey) { + console.log(`Using provided --oldkey for decryption.`); + } + + if (!defaultKey || typeof defaultKey !== 'string') { + throw new Error('Default config key config.keys.user_data_key is missing or invalid.'); + } + + if (!userHasKey) { + console.log('No config.keys.user_data_key found in user_config.json. Nothing to migrate.'); + return; + } + + if (typeof userKey !== 'string' || userKey.length === 0) { + throw new Error('user_config.json config.keys.user_data_key is invalid.'); + } + + if (userKey === defaultKey) { + console.log('user_config.json key matches the default key. Nothing to migrate.'); + return; + } + + const mergedConfig = integrateConfig(defaultConfig, userConfig); + const sessionStore = mergedConfig && mergedConfig.config ? mergedConfig.config.SessionStore : null; + if (!sessionStore || typeof sessionStore !== 'string') { + throw new Error('config.SessionStore is missing or invalid.'); + } + + const accountsRoot = path.join(resolveFromRoot(sessionStore), 'accounts'); + const userFiles = listUserJsonFiles(accountsRoot); + + if (userFiles.length === 0) { + console.log(`No user account files found in ${accountsRoot}. Nothing to migrate.`); + return; + } + + const writableUpdates = []; + let touchedPasswordFields = 0; + + for (const userFile of userFiles) { + let accountData; + try { + accountData = JSON.parse(fs.readFileSync(userFile, 'utf8')); + } catch (error) { + throw new Error(`Failed to parse account file ${userFile}: ${error.message}`); + } + + let fileChanged = false; + + for (const fieldName of ['subscriber_password', 'subscriber_smtp_password']) { + const encryptedValue = accountData[fieldName]; + if (encryptedValue === null || typeof encryptedValue === 'undefined') continue; + if (typeof encryptedValue !== 'string') { + throw new Error(`Suspicious ${fieldName} value in ${userFile}: expected string/null.`); + } + if (encryptedValue.length === 0) continue; + + const decryptedValue = decryptWithKey(encryptedValue, defaultKey); + if (!isSaneDecryptedValue(fieldName, decryptedValue)) { + throw new Error( + `Aborting: decrypted ${fieldName} in ${userFile} appears invalid/binary. No files were updated.\n` + + `If you previously used a different key, re-run with: --oldkey ""` + ); + } + + accountData[fieldName] = encryptWithKey(decryptedValue, userKey); + fileChanged = true; + touchedPasswordFields += 1; + } + + if (fileChanged) { + writableUpdates.push({ filePath: userFile, accountData }); + } + } + + for (const update of writableUpdates) { + fs.writeFileSync(update.filePath, JSON.stringify(update.accountData), 'utf8'); + } + + console.log(`Migrated ${touchedPasswordFields} encrypted password field(s) across ${writableUpdates.length} account file(s).`); +} + +try { + main(); +} catch (error) { + console.error(error.message || error); + process.exit(1); +} \ No newline at end of file diff --git a/zefie_wtvp_minisrv/user_config.example.json b/zefie_wtvp_minisrv/user_config.example.json index 616e2911..c4856eef 100644 --- a/zefie_wtvp_minisrv/user_config.example.json +++ b/zefie_wtvp_minisrv/user_config.example.json @@ -15,6 +15,9 @@ "C:/Users/zefie/webtv/ServiceVault2", "/home/zefie/webtv/ServiceVault" ], + "keys": { + "user_data_key": "SOMETHING_RANDOM_AND_SECRET" // this can be any string, but should be changed to a random value before registering any users. + }, "php_enabled": true, // enables PHP CGI support "php_binpath": "/usr/bin/php-cgi", // path to PHP CGI binary "cgi_enabled": true, // enables CGI Support