Files
BetterDesk/web-nodejs/lib/linuxSystemdUnitPrivileged.js
T
UNITRONIX b787ad9996 fix: rdclient file transfer JS getters and Linux systemd unit updates
Replace invalid prototype getter syntax in local-files.js and mesh-files.js
so the file transfer modal loads in browsers. Panel updates patch
betterdesk-server.service via a passwordless-sudo helper instead of
interactive sudo tee.

Refs #217
2026-06-24 20:19:02 +02:00

121 lines
3.5 KiB
JavaScript

'use strict';
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const ALLOWED_SYSTEMD_UNITS = new Set([
'/etc/systemd/system/betterdesk-server.service',
'/etc/systemd/system/betterdesk-console.service',
]);
function resolveSystemdUnitScriptPath(consoleRoot) {
return path.join(consoleRoot, 'scripts/linux-write-systemd-unit.js');
}
function normalizeUnitPath(filePath) {
if (!filePath || typeof filePath !== 'string') return null;
const trimmed = filePath.trim();
if (!path.isAbsolute(trimmed)) return null;
return path.resolve(trimmed);
}
function isAllowedSystemdUnitPath(filePath) {
const normalized = normalizeUnitPath(filePath);
if (!normalized) return false;
try {
const real = fs.existsSync(normalized) ? fs.realpathSync(normalized) : normalized;
return ALLOWED_SYSTEMD_UNITS.has(real);
} catch (_) {
return ALLOWED_SYSTEMD_UNITS.has(normalized);
}
}
function isRoot() {
return typeof process.getuid === 'function' && process.getuid() === 0;
}
function canUsePasswordlessSudo() {
if (isRoot()) return true;
try {
execFileSync('sudo', ['-n', 'true'], { stdio: 'pipe', timeout: 3000 });
return true;
} catch (_) {
return false;
}
}
function invokeSystemdUnitScript(payload, consoleRoot) {
const scriptPath = resolveSystemdUnitScriptPath(consoleRoot);
if (!fs.existsSync(scriptPath)) {
throw new Error('Systemd unit helper not installed');
}
const runOpts = {
input: JSON.stringify(payload),
encoding: 'utf8',
timeout: 15000,
stdio: ['pipe', 'pipe', 'pipe'],
};
let out;
if (isRoot()) {
out = execFileSync(process.execPath, [scriptPath], runOpts);
} else {
out = execFileSync('sudo', ['-n', process.execPath, scriptPath], runOpts);
}
const parsed = JSON.parse(String(out || '').trim() || '{}');
if (!parsed.success) {
throw new Error(parsed.error || 'Privileged systemd unit operation failed');
}
return parsed;
}
function readSystemdUnitPrivileged(filePath, consoleRoot) {
if (!isAllowedSystemdUnitPath(filePath)) {
throw new Error(`Systemd unit path not allowed: ${filePath}`);
}
const normalized = normalizeUnitPath(filePath);
try {
return fs.readFileSync(normalized, 'utf8');
} catch (err) {
if (err.code !== 'EACCES' && err.code !== 'EPERM') throw err;
}
const result = invokeSystemdUnitScript({ action: 'read', path: normalized }, consoleRoot);
return result.content || '';
}
function writeSystemdUnitPrivileged(filePath, content, consoleRoot) {
if (!isAllowedSystemdUnitPath(filePath)) {
throw new Error(`Systemd unit path not allowed: ${filePath}`);
}
const normalized = normalizeUnitPath(filePath);
try {
fs.writeFileSync(normalized, content, { encoding: 'utf8', mode: 0o644 });
return;
} catch (err) {
if (err.code !== 'EACCES' && err.code !== 'EPERM') throw err;
}
invokeSystemdUnitScript({ action: 'write', path: normalized, content }, consoleRoot);
}
function privilegedSystemdUnitHint() {
return 'Run once as root: sudo node web-nodejs/scripts/linux-ensure-console-user.js';
}
module.exports = {
ALLOWED_SYSTEMD_UNITS,
resolveSystemdUnitScriptPath,
normalizeUnitPath,
isAllowedSystemdUnitPath,
canUsePasswordlessSudo,
readSystemdUnitPrivileged,
writeSystemdUnitPrivileged,
privilegedSystemdUnitHint,
};