MrTerm: SSH/SFTP/RDP-Client im Termius-Stil (Electron)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 14:43:32 +02:00
co-authored by Claude Opus 5.5
commit a5eccc7764
13 changed files with 6133 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// RDP: Windows nutzt das eingebaute mstsc.exe, Linux (Arch/CachyOS) FreeRDP (xfreerdp3 / xfreerdp / wlfreerdp).
const { spawn, execFile, execFileSync } = require('child_process');
const fs = require('fs');
const os = require('os');
const path = require('path');
function which(bin) {
try {
execFileSync(process.platform === 'win32' ? 'where' : 'which', [bin], { stdio: 'ignore' });
return true;
} catch { return false; }
}
function linuxClient(pref) {
const candidates = pref && pref !== 'auto'
? [pref]
: process.env.WAYLAND_DISPLAY
? ['sdl-freerdp3', 'wlfreerdp3', 'xfreerdp3', 'wlfreerdp', 'xfreerdp']
: ['xfreerdp3', 'sdl-freerdp3', 'xfreerdp'];
return candidates.find(which);
}
function detect(settings) {
if (process.platform === 'win32') return { available: true, client: 'mstsc' };
const c = linuxClient(settings.rdpClientLinux);
return c ? { available: true, client: c } : { available: false, hint: 'sudo pacman -S freerdp' };
}
function launch(host, settings) {
const port = Number(host.port) || 3389;
const user = host.domain ? `${host.domain}\\${host.username || ''}` : host.username || '';
if (process.platform === 'win32') {
const target = port === 3389 ? host.address : `${host.address}:${port}`;
const lines = [
`full address:s:${target}`,
user && `username:s:${user}`,
`screen mode id:i:${host.rdpFullscreen ? 2 : 1}`,
host.rdpWidth && `desktopwidth:i:${host.rdpWidth}`,
host.rdpHeight && `desktopheight:i:${host.rdpHeight}`,
'dynamic resolution:i:1',
'smart sizing:i:1',
`redirectclipboard:i:${host.rdpClipboard === false ? 0 : 1}`,
`redirectdrives:i:${host.rdpDrives ? 1 : 0}`,
`audiomode:i:${host.rdpAudio === false ? 2 : 0}`,
'prompt for credentials:i:0',
'authentication level:i:2',
].filter(Boolean);
const file = path.join(os.tmpdir(), `mrterm-${host.id}.rdp`);
fs.writeFileSync(file, lines.join('\r\n'), 'utf16le');
const run = () => {
const p = spawn('mstsc.exe', [file], { detached: true, stdio: 'ignore' });
p.unref();
};
if (host.password && user) {
// Anmeldedaten temporär im Windows-Anmeldeinformationsspeicher ablegen, danach wieder entfernen.
execFile('cmdkey', [`/generic:TERMSRV/${host.address}`, `/user:${user}`, `/pass:${host.password}`], () => {
run();
setTimeout(() => execFile('cmdkey', [`/delete:TERMSRV/${host.address}`], () => {}), 20000);
});
} else run();
return { client: 'mstsc' };
}
const client = linuxClient(settings.rdpClientLinux);
if (!client) throw new Error('Kein FreeRDP gefunden. Installieren mit: sudo pacman -S freerdp');
const args = [`/v:${host.address}:${port}`, '/cert:tofu', '+auto-reconnect'];
if (host.username) args.push(`/u:${host.username}`);
if (host.domain) args.push(`/d:${host.domain}`);
if (host.rdpFullscreen) args.push('/f');
else if (host.rdpWidth && host.rdpHeight) args.push(`/size:${host.rdpWidth}x${host.rdpHeight}`);
else args.push('/dynamic-resolution');
if (host.rdpClipboard !== false) args.push('+clipboard');
if (host.rdpDrives) args.push(`/drive:home,${os.homedir()}`);
if (host.rdpAudio !== false) args.push('/sound');
args.push(`/title:${host.label || host.address}`);
// Passwort über stdin statt Kommandozeile (nicht in `ps` sichtbar)
if (host.password) args.push('/from-stdin');
const p = spawn(client, args, { detached: true, stdio: [host.password ? 'pipe' : 'ignore', 'ignore', 'pipe'] });
if (host.password) { p.stdin.write(host.password + '\n'); p.stdin.end(); }
let err = '';
p.stderr.on('data', (d) => { err += d; });
p.unref();
return { client, process: p, getErr: () => err };
}
module.exports = { detect, launch };