Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
430c18b84e | ||
|
|
98c7763c47 | ||
|
|
e4396db3c8 | ||
|
|
98d8cfa993 |
@@ -4,6 +4,8 @@ A modern SSH, SFTP and RDP client for Windows and Arch Linux / CachyOS. All your
|
|||||||
|
|
||||||
MrTerm is available in **English** and **German**. It follows your system language by default, and you can change it under **Settings → Language**.
|
MrTerm is available in **English** and **German**. It follows your system language by default, and you can change it under **Settings → Language**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Download & installation
|
## Download & installation
|
||||||
|
|
||||||
Get the latest version from the [releases page](https://git.mrblake.cc/MrBlake/MrTerm/releases).
|
Get the latest version from the [releases page](https://git.mrblake.cc/MrBlake/MrTerm/releases).
|
||||||
@@ -48,6 +50,19 @@ sudo pacman -S freerdp gnome-keyring # use kwallet instead of gnome-keyring on
|
|||||||
- **App lock** with a password and/or a FIDO2 security key such as a YubiKey. The vault is then additionally encrypted, and it can lock automatically when you're inactive
|
- **App lock** with a password and/or a FIDO2 security key such as a YubiKey. The vault is then additionally encrypted, and it can lock automatically when you're inactive
|
||||||
- **Automatic updates**: MrTerm checks for new versions on startup and can install them for you
|
- **Automatic updates**: MrTerm checks for new versions on startup and can install them for you
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
|  |  |
|
||||||
|
| **SSH terminal** with tabs and quick access to SFTP, Docker, Firewall and Network | **Host settings**: authentication, keys, jump hosts and VPN |
|
||||||
|
|  |  |
|
||||||
|
| **Docker & Podman** containers with CPU and memory usage | **Firewall** rules for UFW and iptables |
|
||||||
|
|  |  |
|
||||||
|
| **Network**: interfaces, bonds (LACP), bridges, gateway and DNS | **VPN**: WireGuard and OpenVPN, connected automatically per host |
|
||||||
|
|  | |
|
||||||
|
| **Settings**: language, app lock, LAN sync and themes | |
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
1. Click **New host**, enter the address, username and password or key, and click **Save**.
|
1. Click **New host**, enter the address, username and password or key, and click **Save**.
|
||||||
|
|||||||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 64 KiB |
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@xterm/addon-fit": "^0.11.0",
|
"@xterm/addon-fit": "^0.11.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mrterm",
|
"name": "mrterm",
|
||||||
"productName": "MrTerm",
|
"productName": "MrTerm",
|
||||||
"version": "0.10.1",
|
"version": "0.10.3",
|
||||||
"description": "Moderner SSH-, SFTP- und RDP-Client",
|
"description": "Moderner SSH-, SFTP- und RDP-Client",
|
||||||
"main": "src/main/main.js",
|
"main": "src/main/main.js",
|
||||||
"author": "MrBlake",
|
"author": "MrBlake",
|
||||||
|
|||||||
@@ -149,8 +149,10 @@ class NetworkConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(id, s) {
|
watch(id, s) {
|
||||||
s.conn.on('close', () => { if (this.sessions.get(id) === s && !s.replacing) { this.close(id); s.onClose(); } });
|
const conn = s.conn;
|
||||||
s.conn.on('error', () => {});
|
// Nur die aktuelle Verbindung darf die Sitzung beenden (nach apply wird sie ersetzt)
|
||||||
|
conn.on('close', () => { if (this.sessions.get(id) === s && s.conn === conn) { this.close(id); s.onClose(); } });
|
||||||
|
conn.on('error', () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async read(id) {
|
async read(id) {
|
||||||
@@ -226,7 +228,7 @@ class NetworkConfigManager {
|
|||||||
const be = BACKENDS[backendName || s.backend];
|
const be = BACKENDS[backendName || s.backend];
|
||||||
if (!be) throw new Error(i18n.t('Editing is not supported for this network configuration ({backend}).', { backend: s.backend }));
|
if (!be) throw new Error(i18n.t('Editing is not supported for this network configuration ({backend}).', { backend: s.backend }));
|
||||||
const script = `export PATH=$PATH:/usr/sbin:/sbin
|
const script = `export PATH=$PATH:/usr/sbin:/sbin
|
||||||
mkdir -p ${STATE} || exit 1; rm -f ${STATE}/pending ${STATE}/rolled-back
|
mkdir -p ${STATE} || exit 1; rm -f ${STATE}/pending ${STATE}/applied ${STATE}/rolled-back
|
||||||
cd / && tar czf ${STATE}/backup.tgz $(for p in ${be.paths.join(' ')}; do [ -e "$p" ] && echo "$p"; done) || exit 1
|
cd / && tar czf ${STATE}/backup.tgz $(for p in ${be.paths.join(' ')}; do [ -e "$p" ] && echo "$p"; done) || exit 1
|
||||||
# Schreiben in einer Subshell: bei einem Fehler sofort den alten Stand wiederherstellen
|
# Schreiben in einer Subshell: bei einem Fehler sofort den alten Stand wiederherstellen
|
||||||
if ! ( set -e
|
if ! ( set -e
|
||||||
@@ -234,7 +236,7 @@ ${writes.join('\n')}
|
|||||||
) >${STATE}/write.log 2>&1; then ${be.restore}; cat ${STATE}/write.log >&2; exit 5; fi
|
) >${STATE}/write.log 2>&1; then ${be.restore}; cat ${STATE}/write.log >&2; exit 5; fi
|
||||||
if ! (${be.validate}) >${STATE}/validate.log 2>&1; then ${be.restore}; cat ${STATE}/validate.log >&2; exit 4; fi
|
if ! (${be.validate}) >${STATE}/validate.log 2>&1; then ${be.restore}; cat ${STATE}/validate.log >&2; exit 4; fi
|
||||||
touch ${STATE}/pending
|
touch ${STATE}/pending
|
||||||
nohup setsid sh -c 'export PATH=$PATH:/usr/sbin:/sbin; sleep 2; (${be.apply}) >${STATE}/apply.log 2>&1; sleep ${ROLLBACK_SECONDS}; if [ -f ${STATE}/pending ]; then ${be.restore}; (${be.apply}) >>${STATE}/apply.log 2>&1; rm -f ${STATE}/pending; date "+%Y-%m-%d %H:%M:%S" > ${STATE}/rolled-back; fi' >/dev/null 2>&1 &
|
nohup setsid sh -c 'export PATH=$PATH:/usr/sbin:/sbin; sleep 2; (${be.apply}) >${STATE}/apply.log 2>&1; touch ${STATE}/applied; sleep ${ROLLBACK_SECONDS}; if [ -f ${STATE}/pending ]; then ${be.restore}; (${be.apply}) >>${STATE}/apply.log 2>&1; rm -f ${STATE}/pending; date "+%Y-%m-%d %H:%M:%S" > ${STATE}/rolled-back; fi' >/dev/null 2>&1 &
|
||||||
echo started`;
|
echo started`;
|
||||||
const r = await this.root(s, script);
|
const r = await this.root(s, script);
|
||||||
if (r.code === 4) throw new Error(i18n.t('The new configuration is invalid and was not applied: {err}', { err: lastLine(r.err) }));
|
if (r.code === 4) throw new Error(i18n.t('The new configuration is invalid and was not applied: {err}', { err: lastLine(r.err) }));
|
||||||
@@ -245,13 +247,14 @@ echo started`;
|
|||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
try {
|
try {
|
||||||
const { conn, jumps } = await this.ssh.connect(target, id);
|
const { conn, jumps } = await this.ssh.connect(target, id);
|
||||||
const c = await this.root(s, `rm -f ${STATE}/pending`, conn);
|
// Erst bestätigen, wenn das Anwenden fertig ist: sonst reißt z. B. ein Bond-Neustart die neue Verbindung wieder ab
|
||||||
if (c.code) { conn.end(); throw new Error(lastLine(c.err)); }
|
const c = await this.root(s, `[ -f ${STATE}/applied ] || exit 7; rm -f ${STATE}/pending`, conn);
|
||||||
|
if (c.code) { conn.on('error', () => {}); conn.end(); jumps?.forEach((x) => { try { x.end(); } catch {} }); throw new Error(lastLine(c.err) || 'not applied yet'); }
|
||||||
// Neue Verbindung übernimmt die Sitzung
|
// Neue Verbindung übernimmt die Sitzung
|
||||||
s.replacing = true;
|
const old = { conn: s.conn, jumps: s.jumps };
|
||||||
try { s.conn.end(); } catch {}
|
Object.assign(s, { conn, jumps });
|
||||||
s.jumps?.forEach((x) => { try { x.end(); } catch {} });
|
// Alte Verbindung ist nach dem IP-Wechsel meist tot: Fehler (z. B. Keepalive-Timeout) still verwerfen
|
||||||
Object.assign(s, { conn, jumps, replacing: false });
|
for (const c of [old.conn, ...(old.jumps || [])]) { c.on('error', () => {}); try { c.end(); } catch {} }
|
||||||
if (verifyAddress) s.host = target;
|
if (verifyAddress) s.host = target;
|
||||||
this.watch(id, s);
|
this.watch(id, s);
|
||||||
return { confirmed: true };
|
return { confirmed: true };
|
||||||
|
|||||||
@@ -2185,8 +2185,13 @@ class NetworkSession {
|
|||||||
this.draw();
|
this.draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh(tries = 1) {
|
||||||
try { this.apply(await api.call('network:read', this.id)); } catch (e) { toast(e.message, 'error'); }
|
for (let i = 1; ; i++) {
|
||||||
|
try { return this.apply(await api.call('network:read', this.id)); } catch (e) {
|
||||||
|
if (i >= tries) return toast(e.message, 'error');
|
||||||
|
await new Promise((r) => setTimeout(r, 3000));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg(name) { return this.data.config.find((c) => c.name === name); }
|
cfg(name) { return this.data.config.find((c) => c.name === name); }
|
||||||
@@ -2258,7 +2263,7 @@ class NetworkSession {
|
|||||||
try {
|
try {
|
||||||
const r = await fn();
|
const r = await fn();
|
||||||
ov.remove();
|
ov.remove();
|
||||||
if (r?.confirmed) { toast(r.direct ? T('Saved') : T('Network change applied and confirmed'), 'ok'); await this.refresh(); }
|
if (r?.confirmed) { toast(r.direct ? T('Saved') : T('Network change applied and confirmed'), 'ok'); await this.refresh(4); }
|
||||||
else {
|
else {
|
||||||
setTabState(this, 'err');
|
setTabState(this, 'err');
|
||||||
this.body.innerHTML = `<div class="pane-empty"><div style="color:var(--orange);max-width:560px;text-align:center">${T('MrTerm could not reconnect after the change. The server restores the previous configuration automatically within 90 seconds.')}</div></div>`;
|
this.body.innerHTML = `<div class="pane-empty"><div style="color:var(--orange);max-width:560px;text-align:center">${T('MrTerm could not reconnect after the change. The server restores the previous configuration automatically within 90 seconds.')}</div></div>`;
|
||||||
|
|||||||