Sync: warn CachyOS/UFW/firewalld users to open the sync ports with copyable commands; ask before sharing newly created SSH keys, passwords and VPN configurations
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+54
-6
@@ -556,7 +556,8 @@ function editHost(hst = {}) {
|
||||
tags: v.tags.split(',').map((t) => t.trim()).filter(Boolean),
|
||||
groupId: v.groupId || null, keyId: v.keyId || null, jumpHostId: v.jumpHostId || null, vpnId: v.vpnId || null,
|
||||
};
|
||||
await call('vault:upsert', 'hosts', item);
|
||||
const savedHost = await call('vault:upsert', 'hosts', item);
|
||||
if (item.password) askShareSecret('hosts', savedHost, item.label || item.address);
|
||||
reload();
|
||||
}, isNew ? null : (() => { const b = h(`<button class="btn danger">${ICONS.trash}</button>`); b.title = T('Delete'); b.onclick = async () => { if (await confirmBox(T('Delete host?'), T('“{name}” will be permanently removed.', { name: hst.label || hst.address }))) { await call('vault:remove', 'hosts', hst.id); closeDrawer(); reload(); } }; return b; })());
|
||||
setProto(proto);
|
||||
@@ -617,7 +618,8 @@ async function generateKey() {
|
||||
});
|
||||
if (!r) return;
|
||||
const k = await call('key:generate', { type: r.type, passphrase: r.passphrase, comment: r.label });
|
||||
await call('vault:upsert', 'keys', { label: r.label, privateKey: k.privateKey, publicKey: k.publicKey, passphrase: r.passphrase || undefined });
|
||||
const newKey = await call('vault:upsert', 'keys', { label: r.label, privateKey: k.privateKey, publicKey: k.publicKey, passphrase: r.passphrase || undefined });
|
||||
askShareSecret('keys', newKey, r.label);
|
||||
await call('clipboard:write', k.publicKey);
|
||||
toast(T('Key generated – public key copied to clipboard'), 'ok');
|
||||
reload();
|
||||
@@ -633,7 +635,8 @@ function editKey(k = {}, pick = false) {
|
||||
const v = formValues(form);
|
||||
if (!v.privateKey.trim()) throw new Error(T('Private key is missing.'));
|
||||
const parsed = await call('key:parse', { privateKey: v.privateKey, passphrase: v.passphrase });
|
||||
await call('vault:upsert', 'keys', { ...k, ...v, label: v.label || parsed.type, publicKey: v.publicKey.trim() || parsed.publicKey });
|
||||
const savedKey = await call('vault:upsert', 'keys', { ...k, ...v, label: v.label || parsed.type, publicKey: v.publicKey.trim() || parsed.publicKey });
|
||||
askShareSecret('keys', savedKey, savedKey.label);
|
||||
reload();
|
||||
}, (() => { const b = h(`<button class="btn">${ICONS.folder}${T('File…')}</button>`); b.onclick = loadFile; return b; })());
|
||||
async function loadFile() {
|
||||
@@ -811,6 +814,7 @@ function editVpn(v = {}, pick = false) {
|
||||
const item = { ...v, type, label: f.label.trim() || VPN_TYPES[type], config: f.config, disconnectOnQuit: f.disconnectOnQuit,
|
||||
username: type === 'openvpn' ? f.username : '', password: type === 'openvpn' ? f.password : '' };
|
||||
const saved = await call('vault:upsert', 'vpns', item);
|
||||
askShareSecret('vpns', saved, saved.label);
|
||||
for (const cb of $$('[data-host]', hostBox)) {
|
||||
const hst = S.vault.hosts.find((x) => x.id === cb.dataset.host);
|
||||
const want = cb.checked ? saved.id : (hst.vpnId === saved.id ? null : hst.vpnId || null);
|
||||
@@ -1103,9 +1107,14 @@ async function renderSyncCard(card) {
|
||||
const st = S.syncStatus;
|
||||
card.innerHTML = `<h3>${T('Synchronization')}</h3><p style="color:var(--muted);margin-top:0">${T('Synchronize hosts, groups, snippets, port forwards, VPNs and known hosts directly between MrTerm devices in your local network. The connection is end-to-end encrypted; no server or cloud is involved.')}</p>`;
|
||||
const en = check(T('Enable LAN synchronization'), 'syncOn', st.enabled);
|
||||
$('input', en).onchange = async (e) => { S.syncStatus = await call('sync:enable', e.target.checked); renderSyncCard(card); };
|
||||
$('input', en).onchange = async (e) => {
|
||||
S.syncStatus = await call('sync:enable', e.target.checked);
|
||||
renderSyncCard(card);
|
||||
if (e.target.checked && S.syncStatus.firewall) showFirewallHint();
|
||||
};
|
||||
card.append(en);
|
||||
if (!st.enabled) return;
|
||||
if (st.firewall && !syncFwDismissed()) card.append(firewallHintBox(() => renderSyncCard(card)));
|
||||
const name = field(T('Device name'), 'devName', st.deviceName);
|
||||
$('input', name).onchange = async (e) => { S.syncStatus = await call('sync:enable', true, e.target.value.trim()); };
|
||||
card.append(name);
|
||||
@@ -1128,7 +1137,46 @@ async function renderSyncCard(card) {
|
||||
const sec = h(`<button class="btn">${ICONS.key}${T('Choose shared secrets')}</button>`); sec.onclick = chooseSharedSecrets;
|
||||
acts.append(pair, now, sec);
|
||||
card.append(acts);
|
||||
card.append(h(`<div class="hint" style="color:var(--faint);font-size:11px;margin-top:10px">${T('Devices find each other via UDP port 47811 and synchronize via TCP port 47812. If a firewall is active, allow these ports in your local network (with UFW: sudo ufw allow 47811/udp and sudo ufw allow 47812/tcp).')}</div>`));
|
||||
if (!st.firewall || syncFwDismissed()) card.append(h(`<div class="hint" style="color:var(--faint);font-size:11px;margin-top:10px">${T('Devices find each other via UDP port 47811 and synchronize via TCP port 47812. If a firewall is active, allow these ports in your local network (with UFW: sudo ufw allow 47811/udp and sudo ufw allow 47812/tcp).')}</div>`));
|
||||
}
|
||||
|
||||
// Neues Geheimnis bei aktiver Synchronisation: fragen, ob es geteilt werden soll (Antwort wird gemerkt)
|
||||
async function askShareSecret(c, item, label) {
|
||||
try {
|
||||
const st = S.syncStatus || (S.syncStatus = await api.call('sync:status'));
|
||||
if (!st.enabled || !st.peers.length || !item?.id) return;
|
||||
if ((st.share[c] || []).includes(item.id) || (st.share.declined?.[c] || []).includes(item.id)) return;
|
||||
const what = { keys: T('SSH key'), hosts: T('Host password'), vpns: T('VPN configuration') }[c];
|
||||
const r = await modal({ title: T('Share with paired devices?'), text: `${what}: ${label || ''}`,
|
||||
body: `<p style="margin-top:0">${T('Should this secret be synchronized to your paired devices ({names})? You can change this later under Settings → Synchronization.', { names: st.peers.map((p) => p.name).join(', ') })}</p>`,
|
||||
buttons: [{ label: T("Don't share"), value: 'no', cls: 'ghost' }, { label: T('Share'), value: 'yes', cls: 'primary' }], noEnter: true });
|
||||
if (!r) return; // Dialog geschlossen: beim nächsten Speichern erneut fragen
|
||||
S.syncStatus = await api.call('sync:shareItem', c, item.id, r === 'yes');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Firewall-Hinweis (CachyOS / aktive UFW bzw. firewalld): Ports für die Synchronisation freigeben
|
||||
const FW_CMDS = { ufw: 'sudo ufw allow 47811/udp\nsudo ufw allow 47812/tcp', firewalld: 'sudo firewall-cmd --permanent --add-port=47811/udp --add-port=47812/tcp\nsudo firewall-cmd --reload' };
|
||||
const fwCmds = (f) => (f.firewalld && !f.ufw ? FW_CMDS.firewalld : FW_CMDS.ufw);
|
||||
const syncFwDismissed = () => { try { return localStorage.getItem('mrterm.sync.fwHint') === '1'; } catch { return false; } };
|
||||
function firewallHintBox(onDismiss) {
|
||||
const f = S.syncStatus.firewall;
|
||||
const box = h(`<div class="net-banner warn fw-hint"><div>${ICONS.wall}</div><div class="grow"><b>${f.os === 'cachyos' ? T('CachyOS: allow the sync ports in the firewall') : T('Firewall active: allow the sync ports')}</b>
|
||||
<p>${T('Otherwise other devices cannot find or reach this device. Run these commands once in a terminal:')}</p><pre class="mono">${esc(fwCmds(f))}</pre>
|
||||
<div class="row"><button class="btn sm" data-f="copy">${ICONS.copy}${T('Copy commands')}</button><button class="btn sm ghost" data-f="hide">${T('Already done, hide')}</button></div></div></div>`);
|
||||
box.onclick = (e) => {
|
||||
const a = e.target.closest('[data-f]')?.dataset.f;
|
||||
if (a === 'copy') { api.call('clipboard:write', fwCmds(f)); toast(T('Copied'), 'ok'); }
|
||||
if (a === 'hide') { try { localStorage.setItem('mrterm.sync.fwHint', '1'); } catch {} onDismiss?.(); }
|
||||
};
|
||||
return box;
|
||||
}
|
||||
async function showFirewallHint() {
|
||||
const f = S.syncStatus.firewall;
|
||||
const r = await modal({ title: f.os === 'cachyos' ? T('CachyOS: allow the sync ports in the firewall') : T('Firewall active: allow the sync ports'),
|
||||
body: `<p style="margin-top:0">${T('CachyOS enables the UFW firewall by default. It blocks the ports MrTerm needs to find and reach other devices (UDP 47811, TCP 47812). Run these commands once in a terminal:')}</p><pre class="mono fw-pre">${esc(fwCmds(f))}</pre>`,
|
||||
buttons: [{ label: T('Close'), value: null, cls: 'ghost' }, { label: T('Copy commands'), value: 'copy', cls: 'primary' }] });
|
||||
if (r === 'copy') { await api.call('clipboard:write', fwCmds(f)); toast(T('Copied'), 'ok'); }
|
||||
}
|
||||
|
||||
// Kopplungsdialog: dieses Gerät ist sichtbar, gefundene Geräte können gekoppelt werden
|
||||
@@ -1141,7 +1189,7 @@ async function openPairing() {
|
||||
const box = $('.pair-list');
|
||||
if (!box) return clearInterval(timer);
|
||||
const near = S.syncStatus?.nearby || [];
|
||||
box.innerHTML = near.length ? '' : `<div class="hint" style="color:var(--muted);display:flex;gap:10px;align-items:center"><span class="spinner sm"></span>${T('Searching for devices …')}</div>`;
|
||||
box.innerHTML = near.length ? '' : `<div class="hint" style="color:var(--muted);display:flex;gap:10px;align-items:center"><span class="spinner sm"></span>${T('Searching for devices …')}</div>${S.syncStatus?.firewall ? `<div class="hint" style="color:var(--orange);margin-top:10px;font-size:12px">${T('No devices found? The firewall on this device may block them:')}<pre class="mono fw-pre">${esc(fwCmds(S.syncStatus.firewall))}</pre></div>` : ''}`;
|
||||
for (const d of near) {
|
||||
const row = h(`<div class="card" style="padding:10px 12px;cursor:default"><div class="avatar" style="background:var(--icon-bg);color:var(--accent)">${ICONS.screen}</div><div class="meta"><div class="title">${esc(d.name)}</div><div class="sub">${esc(d.address)}${d.pairable ? '' : ` · ${T('not ready for pairing')}`}</div></div></div>`);
|
||||
const b = h(`<button class="btn sm primary" ${d.pairable ? '' : 'disabled'}>${T('Pair')}</button>`);
|
||||
|
||||
@@ -437,3 +437,9 @@ kbd { background: var(--card); border: 1px solid var(--border); border-bottom-wi
|
||||
.pair-list { max-height: 260px; overflow: auto; }
|
||||
.share-list { max-height: 380px; overflow: auto; }
|
||||
.share-list h4 { margin: 12px 0 6px; }
|
||||
.fw-hint { align-items: flex-start; margin: 4px 0 14px; }
|
||||
.fw-hint svg { width: 22px; height: 22px; }
|
||||
.fw-hint .grow { flex: 1; min-width: 0; }
|
||||
.fw-hint b { color: var(--text); }
|
||||
.fw-hint p { margin: 4px 0 8px; color: var(--muted); }
|
||||
.fw-hint pre, .fw-pre { background: var(--bg); border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; margin: 0 0 10px; color: var(--text); white-space: pre-wrap; user-select: text; }
|
||||
|
||||
Reference in New Issue
Block a user