chore: update Docker Compose image tags to 3.2.12 and enhance documentation for versioning

This commit is contained in:
UNITRONIX
2026-06-11 18:36:48 +02:00
parent a2621b936a
commit 577fc79a97
12 changed files with 145 additions and 36 deletions
+2 -2
View File
@@ -260,8 +260,8 @@ async function downloadFont(family, weights = ['400', '500', '600', '700']) {
if (downloadedFiles.length > 0) {
const cssFaces = downloadedFiles.map(file => {
const weightMatch = file.match(/-(\d+)\.woff2$/);
const weight = (weightMatch && weightMatch[1]) || '400';
const cssFamily = String(family).replace(/['\\]/g, '');
const weight = (weightMatch && /^\d+$/.test(weightMatch[1])) ? weightMatch[1] : '400';
const cssFamily = String(family).replace(/[^a-zA-Z0-9\s-]/g, '').trim() || 'sans-serif';
return [
'@font-face {',
` font-family: '${cssFamily}';`,
+11 -6
View File
@@ -34,6 +34,7 @@ const MONITOR_OPTS = { allowPrivate: true };
const DEFAULT_POLL_INTERVAL_MS = 60_000; // 1 minute
const DEFAULT_TIMEOUT_MS = 5_000; // 5 seconds
const MAX_HISTORY_ROWS = 10_000; // per target
const MAX_TARGETS_PER_POLL = 500; // cap poll workload
const CLEANUP_INTERVAL_MS = 3600_000; // 1 hour
// Security: Strict hostname/IP validation regex
@@ -237,6 +238,7 @@ class NetworkMonitor {
this.db = dbAdapter;
this.running = false;
this.timer = null;
this._pollInFlight = false;
this.pollInterval = DEFAULT_POLL_INTERVAL_MS;
}
@@ -328,7 +330,8 @@ class NetworkMonitor {
if (!targets || targets.length === 0) return [];
const results = [];
for (const target of targets) {
const batch = targets.slice(0, MAX_TARGETS_PER_POLL);
for (const target of batch) {
const result = await this.checkTarget(target);
results.push(result);
}
@@ -342,16 +345,18 @@ class NetworkMonitor {
// --- Internal ---
async _poll() {
if (!this.running) return;
if (!this.running || this._pollInFlight) return;
this._pollInFlight = true;
try {
await this.checkAll();
} catch (err) {
console.error('[NetworkMonitor] Poll error:', err.message);
}
if (this.running) {
this.timer = setTimeout(() => this._poll(), this.pollInterval);
} finally {
this._pollInFlight = false;
if (this.running) {
this.timer = setTimeout(() => this._poll(), this.pollInterval);
}
}
}
}
+38 -6
View File
@@ -535,6 +535,7 @@ let _updateInProgress = false;
/**
* Run a shell command as a promise (non-blocking unlike execSync).
* Prefer spawnPromise() when arguments are known — avoids shell interpolation.
*/
function execPromise(cmd, opts = {}) {
return new Promise((resolve, reject) => {
@@ -550,6 +551,27 @@ function execPromise(cmd, opts = {}) {
});
}
function spawnPromise(command, args, opts = {}) {
return new Promise((resolve, reject) => {
const { spawn } = require('child_process');
const proc = spawn(command, args, { shell: false, ...opts });
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (chunk) => { stdout += chunk; });
proc.stderr?.on('data', (chunk) => { stderr += chunk; });
proc.on('error', reject);
proc.on('close', (code) => {
if (code !== 0) {
const err = new Error(`${command} exited with code ${code}`);
err.stderr = stderr;
err.stdout = stdout;
return reject(err);
}
resolve({ stdout, stderr });
});
});
}
/**
* Copy directory recursively.
*/
@@ -1125,18 +1147,17 @@ async function buildGoServer(preferredGoBinPath = null) {
const start = Date.now();
const goBin = goCheck.binPath || 'go';
const buildEnv = buildEnvWithGo(goBin);
// Quote when path contains spaces (Windows "Program Files")
const goCmd = /\s/.test(goBin) ? `"${goBin}"` : goBin;
try {
await execPromise(`${goCmd} mod download`, {
await spawnPromise(goBin, ['mod', 'download'], {
cwd: serverDir,
timeout: 120000,
env: buildEnv
});
await execPromise(
`${goCmd} build -trimpath -ldflags="-s -w" -o "${binaryName}" .`,
await spawnPromise(
goBin,
['build', '-trimpath', '-ldflags=-s -w', '-o', binaryName, '.'],
{ cwd: serverDir, timeout: 600000, env: buildEnv }
);
@@ -1380,7 +1401,7 @@ async function _installGoToolchainBody(onProgress, opts = {}) {
{ timeout: 300000 }
);
} else {
await execPromise(`tar -xzf "${archivePath}" -C "${GO_TOOLCHAIN_DIR}"`, { timeout: 300000 });
await spawnPromise('tar', ['-xzf', archivePath, '-C', GO_TOOLCHAIN_DIR], { timeout: 300000 });
}
try { fs.unlinkSync(archivePath); } catch (_e) { /* ignore */ }
@@ -2578,6 +2599,14 @@ function isValidBackupName(name) {
return typeof name === 'string' && /^pre-update-[\d\-T]+$/.test(name);
}
function isValidManifestRelativePath(relPath) {
if (typeof relPath !== 'string' || relPath.length === 0 || relPath.includes('\0')) return false;
const normalized = relPath.replace(/\\/g, '/');
if (path.isAbsolute(normalized) || normalized.startsWith('/') || normalized.startsWith('..')) return false;
if (normalized.split('/').some((seg) => seg === '..')) return false;
return true;
}
/**
* Recursively delete a directory. Refuses to delete anything outside
* BACKUP_DIR to defend against path-traversal bugs upstream.
@@ -2635,6 +2664,9 @@ function restoreFromBackup(backupName) {
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
let restored = 0;
for (const filePath of (manifest.files || [])) {
if (!isValidManifestRelativePath(filePath)) {
throw new Error(`Invalid path in backup manifest: ${filePath}`);
}
const src = resolvePathUnderRoot(backupPath, filePath);
const dest = resolvePathUnderRoot(ROOT_DIR, filePath);
if (fs.existsSync(src)) {