buncha stuff, v0.9.74
This commit is contained in:
57
QuickSetup.md
Normal file
57
QuickSetup.md
Normal file
@@ -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 <dot.path.key> <value> [--overwrite]
|
||||||
|
node tools/configurator.js <dot.path.key> --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
|
||||||
|
```
|
||||||
@@ -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 (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(", ") + "...");
|
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";
|
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);
|
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.");
|
||||||
|
}
|
||||||
@@ -129,7 +129,7 @@ class WTVMSNTV2 {
|
|||||||
socket.rawDataListener = (chunk) => this.handleData(socket, chunk);
|
socket.rawDataListener = (chunk) => this.handleData(socket, chunk);
|
||||||
socket.on('data', socket.rawDataListener);
|
socket.on('data', socket.rawDataListener);
|
||||||
socket.on('error', (err) => {
|
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'] || '';
|
const userAgent = request_headers['User-Agent'] || request_headers['user-agent'] || '';
|
||||||
if (!this.isAllowedUserAgent(userAgent)) {
|
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 || '<none>');
|
console.warn('[WTV-MSNTV2] unsupported User-Agent rejected:', userAgent || '<none>');
|
||||||
}
|
}
|
||||||
this.writeError(socket, 403, 'Forbidden', request_headers);
|
this.writeError(socket, 403, 'Forbidden', request_headers);
|
||||||
return;
|
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) {
|
if (verbose) {
|
||||||
console.log('[WTV-MSNTV2] incoming request:', requestLine);
|
console.log('[WTV-MSNTV2] incoming request:', requestLine);
|
||||||
if (body.length) {
|
if (body.length) {
|
||||||
@@ -250,7 +250,7 @@ class WTVMSNTV2 {
|
|||||||
handleConnect(socket, requestUrl) {
|
handleConnect(socket, requestUrl) {
|
||||||
const [host, portString] = requestUrl.split(':');
|
const [host, portString] = requestUrl.split(':');
|
||||||
const port = parseInt(portString, 10) || 443;
|
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);
|
const connectIntercept = this.getConnectIntercept(host);
|
||||||
if (verbose) console.log('[WTV-MSNTV2] CONNECT request:', requestUrl, 'intercept:', !!connectIntercept);
|
if (verbose) console.log('[WTV-MSNTV2] CONNECT request:', requestUrl, 'intercept:', !!connectIntercept);
|
||||||
|
|
||||||
@@ -276,13 +276,13 @@ class WTVMSNTV2 {
|
|||||||
});
|
});
|
||||||
|
|
||||||
remote.on('error', (err) => {
|
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');
|
this.writeError(socket, 502, 'Bad Gateway');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupTlsSocket(tlsSocket) {
|
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;
|
const sslDebug = this.sslv2Debug;
|
||||||
tlsSocket.on('secureConnect', () => {
|
tlsSocket.on('secureConnect', () => {
|
||||||
if (sslDebug) console.log('[WTV-MSNTV2] TLS handshake complete for intercepted CONNECT', tlsSocket.connectIntercept.match);
|
if (sslDebug) console.log('[WTV-MSNTV2] TLS handshake complete for intercepted CONNECT', tlsSocket.connectIntercept.match);
|
||||||
@@ -302,7 +302,7 @@ class WTVMSNTV2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setupForgeTls(socket, connectIntercept) {
|
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 sslDebug = this.sslv2Debug;
|
||||||
const creds = this.forgeTlsCredentials;
|
const creds = this.forgeTlsCredentials;
|
||||||
if (!creds) {
|
if (!creds) {
|
||||||
@@ -1234,7 +1234,7 @@ class WTVMSNTV2 {
|
|||||||
const requestLine = headerLines.shift();
|
const requestLine = headerLines.shift();
|
||||||
const requestParts = requestLine.split(' ');
|
const requestParts = requestLine.split(' ');
|
||||||
if (requestParts.length < 3) {
|
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) {
|
if (verbose) {
|
||||||
console.warn('[WTV-MSNTV2] TLS invalid request line:', requestLine);
|
console.warn('[WTV-MSNTV2] TLS invalid request line:', requestLine);
|
||||||
console.warn('[WTV-MSNTV2] TLS raw header block:', headerBlock);
|
console.warn('[WTV-MSNTV2] TLS raw header block:', headerBlock);
|
||||||
@@ -1280,7 +1280,7 @@ class WTVMSNTV2 {
|
|||||||
};
|
};
|
||||||
Object.assign(request_headers, headers);
|
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) {
|
if (verbose) {
|
||||||
console.log('[WTV-MSNTV2] decrypted request:', requestLine);
|
console.log('[WTV-MSNTV2] decrypted request:', requestLine);
|
||||||
console.log('[WTV-MSNTV2] decrypted headers:\n' + rawHeaders.join('\r\n'));
|
console.log('[WTV-MSNTV2] decrypted headers:\n' + rawHeaders.join('\r\n'));
|
||||||
@@ -1440,14 +1440,14 @@ class WTVMSNTV2 {
|
|||||||
const candidate = this.wtvshared.makeSafePath(base, '');
|
const candidate = this.wtvshared.makeSafePath(base, '');
|
||||||
// Exact match first
|
// Exact match first
|
||||||
if (candidate && fs.existsSync(candidate) && fs.lstatSync(candidate).isFile()) {
|
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;
|
return candidate;
|
||||||
}
|
}
|
||||||
// Dynamic suffixes: e.g. kickstart.aspx -> kickstart.aspx.js
|
// Dynamic suffixes: e.g. kickstart.aspx -> kickstart.aspx.js
|
||||||
for (const suffix of dynSuffixes) {
|
for (const suffix of dynSuffixes) {
|
||||||
const dynCandidate = this.wtvshared.makeSafePath(base + suffix, '');
|
const dynCandidate = this.wtvshared.makeSafePath(base + suffix, '');
|
||||||
if (dynCandidate && fs.existsSync(dynCandidate) && fs.lstatSync(dynCandidate).isFile()) {
|
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;
|
return dynCandidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1753,14 +1753,14 @@ class WTVMSNTV2 {
|
|||||||
target = new url.URL(requestUrl);
|
target = new url.URL(requestUrl);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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);
|
console.error('[WTV-MSNTV2] invalid URL:', requestUrl, err.message);
|
||||||
}
|
}
|
||||||
this.writeError(socket, 400, 'Bad Request', request_headers);
|
this.writeError(socket, 400, 'Bad Request', request_headers);
|
||||||
return;
|
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 isHttps = target.protocol === 'https:';
|
||||||
const agent = isHttps ? https : http;
|
const agent = isHttps ? https : http;
|
||||||
const requestPath = target.pathname + (target.search || '');
|
const requestPath = target.pathname + (target.search || '');
|
||||||
@@ -1793,7 +1793,7 @@ class WTVMSNTV2 {
|
|||||||
|
|
||||||
const maxResponseBytes = this.maxProxyResponseBytes;
|
const maxResponseBytes = this.maxProxyResponseBytes;
|
||||||
const proxyReq = agent.request(options, (res) => {
|
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) {
|
if (verbose) {
|
||||||
console.log('[WTV-MSNTV2] upstream response:', res.statusCode, res.statusMessage);
|
console.log('[WTV-MSNTV2] upstream response:', res.statusCode, res.statusMessage);
|
||||||
console.log('[WTV-MSNTV2] upstream response headers:', JSON.stringify(res.headers));
|
console.log('[WTV-MSNTV2] upstream response headers:', JSON.stringify(res.headers));
|
||||||
|
|||||||
4
zefie_wtvp_minisrv/package-lock.json
generated
4
zefie_wtvp_minisrv/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "zefie_wtvp_minisrv",
|
"name": "zefie_wtvp_minisrv",
|
||||||
"version": "0.9.73",
|
"version": "0.9.74",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "zefie_wtvp_minisrv",
|
"name": "zefie_wtvp_minisrv",
|
||||||
"version": "0.9.73",
|
"version": "0.9.74",
|
||||||
"license": "GPL3",
|
"license": "GPL3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@serialport/parser-readline": "^13.0.0",
|
"@serialport/parser-readline": "^13.0.0",
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "zefie_wtvp_minisrv",
|
"name": "zefie_wtvp_minisrv",
|
||||||
"version": "0.9.73",
|
"version": "0.9.74",
|
||||||
"description": "WebTV Service (WTVP) Emulation Server",
|
"description": "WebTV Service (WTVP) Emulation Server",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"homepage": "https://github.com/zefie/zefie_wtvp_minisrv",
|
"homepage": "https://github.com/zefie/zefie_wtvp_minisrv",
|
||||||
"license": "GPL3",
|
"license": "GPL3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node app.js",
|
"start": "node app.js",
|
||||||
"irc": "node irconly.js",
|
|
||||||
"test": "node test.js",
|
"test": "node test.js",
|
||||||
"debug": "cross-env DEBUG=* node app.js",
|
"debug": "cross-env DEBUG=* node app.js",
|
||||||
"modem-proxy": "node modem_proxy.js"
|
"modem-proxy": "node modem_proxy.js"
|
||||||
|
|||||||
215
zefie_wtvp_minisrv/tools/configurator.js
Normal file
215
zefie_wtvp_minisrv/tools/configurator.js
Normal file
@@ -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 <dot.path.key> <value> [--overwrite]');
|
||||||
|
console.log(' node tools/configurator.js <dot.path.key> --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);
|
||||||
|
});
|
||||||
@@ -4,6 +4,8 @@
|
|||||||
/**
|
/**
|
||||||
* WebTV MPEG-1 PS Encoder
|
* WebTV MPEG-1 PS Encoder
|
||||||
*
|
*
|
||||||
|
* This tool is incomplete, and may not generate correct WebTV MPEG yet
|
||||||
|
*
|
||||||
* Two-pass pipeline:
|
* Two-pass pipeline:
|
||||||
* 1. ffmpeg encodes input to MPEG-1 PS (codec settings only)
|
* 1. ffmpeg encodes input to MPEG-1 PS (codec settings only)
|
||||||
* 2. ES extracted via structure-aware pack walk (never naive payload scan)
|
* 2. ES extracted via structure-aware pack walk (never naive payload scan)
|
||||||
|
|||||||
257
zefie_wtvp_minisrv/tools/update_user_data_key.js
Normal file
257
zefie_wtvp_minisrv/tools/update_user_data_key.js
Normal file
@@ -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 "<your previous key>"`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -15,6 +15,9 @@
|
|||||||
"C:/Users/zefie/webtv/ServiceVault2",
|
"C:/Users/zefie/webtv/ServiceVault2",
|
||||||
"/home/zefie/webtv/ServiceVault"
|
"/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_enabled": true, // enables PHP CGI support
|
||||||
"php_binpath": "/usr/bin/php-cgi", // path to PHP CGI binary
|
"php_binpath": "/usr/bin/php-cgi", // path to PHP CGI binary
|
||||||
"cgi_enabled": true, // enables CGI Support
|
"cgi_enabled": true, // enables CGI Support
|
||||||
|
|||||||
Reference in New Issue
Block a user