Fix: SSH-Terminal-Login (No auth methods), kaputtes DOM in Bearbeiten-Modals

Quelltext-Huerde wieder entfernt (auf Wunsch) - siehe vorherigen Commit,
hier nur der Revert von initSourceProtection() und der README-Erwaehnung.

SSH-Terminal-Login war komplett kaputt (von dir gemeldet + Screenshot):
- Root Cause: das Init-Payload vom Browser enthielt nie ein Passwort,
  SSHClient.connect() bekam also weder Passwort noch Key noch Agent und
  scheiterte sofort mit "No authentication methods available" - noch
  bevor ueberhaupt eine interaktive Passwortabfrage moeglich gewesen
  waere (die High-Level-API erledigt Host-Key-Pruefung UND
  Authentifizierung in einem blockierenden Aufruf).
- Fix: Umstieg auf die Low-Level paramiko.Transport-API. Nach
  Host-Key-Bestaetigung wird aktiv erfragt, welche Auth-Methoden der
  Server anbietet (auth_none), und bei Bedarf interaktiv ueber das
  Browser-Terminal nach Passwort/keyboard-interactive-Prompts gefragt
  -- genau das Verhalten, das der bestehende Hinweistext im Modal schon
  immer versprach, aber nie tatsaechlich implementiert war.
  Host-Key-Verifikation dabei manuell nachgebaut (_verify_host_key_interactive)
  inkl. hartem Ablehnen bei GEAENDERTEM (nicht nur unbekanntem) Host-Key,
  wie ein echtes ssh-CLI bei einer moeglichen MITM-Situation.
- Waehrend der Live-Verifikation gegen ein echtes Geraet zwei weitere
  Bugs gefunden und gefixt: ws.receive() wirft in diesem Setup
  ConnectionClosed statt None zurueckzugeben (crashte
  _terminal_read_line unbehandelt -> "Invalid frame header" beim
  Client); _send_and_close() crashte ebenso, wenn der Client bereits weg
  war. Beide jetzt defensiv abgefangen.
- Live gegen ein echtes Zielgeraet verifiziert (Host-Key-Bestaetigung,
  Passwort-Prompt, erfolgreicher Login) sowie manuell von dir bestaetigt.

Kaputtes DOM in zwei Bearbeiten-Modals (von dir gemeldet: "Verbindung
testen" oeffnete beim Switch bearbeiten kein Fenster, obwohl es beim
Neuanlegen funktionierte):
- Root Cause: <div class="modal-overlay">...</div> stand direkt in
  <tbody> (nur <tr> ist dort gueltig). Browser "foster-parenten"
  ungueltigen Tbody-Inhalt aus der Tabelle heraus und zerreissen dabei
  teils die Eltern-Kind-Beziehung zwischen <form> und seinen Buttons --
  this.closest("form") lieferte dadurch null statt des Formulars.
  Betroffen: editSwitchModal (switches.html), deviceOptionsModal
  (settings_dhcp.html). Fix: beide Modal-Bloecke aus der Tabelle heraus
  in eine eigene Schleife direkt danach verschoben (gleiches Muster wie
  die bereits korrekten Neuanlegen-Modals).
- Per DOM-Inspektion verifiziert: this.closest("form") lieferte vorher
  null, danach das korrekte Formular fuer alle Zeilen; End-to-End-Test
  bestaetigt, dass sich das Terminal-Modal jetzt oeffnet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 12:00:22 +02:00
co-authored by Claude Sonnet 5
parent 6803929140
commit 4d7433e832
5 changed files with 273 additions and 208 deletions
+34 -28
View File
@@ -236,39 +236,45 @@
{% endif %}
</td>
</tr>
{% if option_defs and can_edit %}
<div class="modal-overlay" id="deviceOptionsModal{{ loop.index }}">
<div class="modal">
<form method="post">
<input type="hidden" name="save_dhcp_device_options" value="1">
<input type="hidden" name="mac" value="{{ r.mac }}">
<div class="modal-header">
<h3>DHCP-Optionen — {{ r.name }}</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<p class="text-faint" style="font-size:11.5px; margin:0 0 14px;">Leer lassen, um den globalen Wert zu übernehmen.</p>
{% for d in option_defs %}
<div class="field">
<label>{{ d.name }} <span class="text-faint">(Code {{ d.code }})</span></label>
<input type="text" name="opt_{{ d.id }}" value="{{ option_values.get(d.id, {}).get(r.mac, '') }}"
placeholder="{{ option_values.get(d.id, {}).get('', '(kein globaler Wert)') }}">
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Modals außerhalb der Tabelle: ein <div> direkt in <tbody> ist
ungültiges HTML und wird vom Browser aus der Tabelle heraus
"foster-parented" — dabei kann die Eltern-Kind-Beziehung zwischen
Formular und Feldern zerrissen werden. -->
{% if option_defs and can_edit %}
{% for r in reservations %}
<div class="modal-overlay" id="deviceOptionsModal{{ loop.index }}">
<div class="modal">
<form method="post">
<input type="hidden" name="save_dhcp_device_options" value="1">
<input type="hidden" name="mac" value="{{ r.mac }}">
<div class="modal-header">
<h3>DHCP-Optionen — {{ r.name }}</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<p class="text-faint" style="font-size:11.5px; margin:0 0 14px;">Leer lassen, um den globalen Wert zu übernehmen.</p>
{% for d in option_defs %}
<div class="field">
<label>{{ d.name }} <span class="text-faint">(Code {{ d.code }})</span></label>
<input type="text" name="opt_{{ d.id }}" value="{{ option_values.get(d.id, {}).get(r.mac, '') }}"
placeholder="{{ option_values.get(d.id, {}).get('', '(kein globaler Wert)') }}">
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% endif %}
{% else %}
<p class="text-faint" style="font-size:12.5px; margin-bottom:18px;">Keine aktiven Clients mit MAC + IP vorhanden.</p>
{% endif %}
+67 -60
View File
@@ -75,66 +75,6 @@
</div>
</td>
</tr>
{% if can_edit %}
<div class="modal-overlay" id="editSwitchModal{{ loop.index }}">
<div class="modal">
<form method="post" onsubmit="return validateSwitchForm(this, 'edit{{ loop.index }}');">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<div class="modal-header">
<h3>Switch bearbeiten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Hostname</label>
<input type="text" name="hostname" value="{{ s['hostname'] }}" required>
</div>
<div class="field"><label>IP-Adresse</label>
<input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div>
<div class="field"><label>SSH-Port</label>
<input type="number" name="ssh_port" value="{{ s['ssh_port'] or '' }}" min="1" max="65535" placeholder="22 (Standard)">
<div class="field-hint">Leer lassen, wenn der Switch den Standard-Port 22 verwendet.</div>
</div>
<div class="field">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
{% for c in all_credentials %}
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}" {% if c['id'] == s['credential_id'] %}selected{% endif %}>{{ c['name'] }}</option>
{% endfor %}
<option value="new">+ Neue Zugangsdaten anlegen</option>
</select>
</div>
<div class="new-credential-fields hidden">
<div class="field"><label>Name der Zugangsdaten</label>
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
</div>
<div class="field"><label>Username</label>
<input type="text" name="new_credential_username" placeholder="z.B. admin">
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_edit{{ loop.index }}" name="new_credential_password">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_edit{{ loop.index }}" name="new_credential_password_confirm">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="margin-right:auto;" onclick="openTerminal(this.closest('form'))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 9l4 3-4 3M13 15h5"/></svg>
Verbindung testen
</button>
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endif %}
{% else %}
<tr class="empty-row"><td colspan="5">Noch keine Switche vorhanden.</td></tr>
{% endfor %}
@@ -143,6 +83,73 @@
</div>
</div>
<!-- Bearbeiten-Modals außerhalb der Tabelle: ein <div> direkt in <tbody>
ist ungültiges HTML — Browser "foster-parenten" es dann aus der
Tabelle heraus und zerreißen dabei mitunter die Eltern-Kind-Beziehung
zwischen Formular und Buttons (this.closest('form') lieferte dadurch
null, "Verbindung testen" öffnete kein Terminal mehr). -->
{% if can_edit %}
{% for s in switches %}
<div class="modal-overlay" id="editSwitchModal{{ loop.index }}">
<div class="modal" style="max-width:1000px;">
<form method="post" onsubmit="return validateSwitchForm(this, 'edit{{ loop.index }}');">
<input type="hidden" name="edit_switch" value="1">
<input type="hidden" name="old_hostname" value="{{ s['hostname'] }}">
<div class="modal-header">
<h3>Switch bearbeiten</h3>
<button type="button" class="modal-close" data-close-modal>&times;</button>
</div>
<div class="modal-body">
<div class="field"><label>Hostname</label>
<input type="text" name="hostname" value="{{ s['hostname'] }}" required>
</div>
<div class="field"><label>IP-Adresse</label>
<input type="text" name="ip" value="{{ s['ip'] }}" required placeholder="z.B. 192.168.1.100">
<div class="invalid-feedback">Bitte eine gültige IP-Adresse eingeben.</div>
</div>
<div class="field"><label>SSH-Port</label>
<input type="number" name="ssh_port" value="{{ s['ssh_port'] or '' }}" min="1" max="65535" placeholder="22 (Standard)">
<div class="field-hint">Leer lassen, wenn der Switch den Standard-Port 22 verwendet.</div>
</div>
<div class="field">
<label>Zugangsdaten</label>
<select name="credential_choice" class="credential-select" onchange="toggleNewCredentialFields(this)">
{% for c in all_credentials %}
<option value="{{ c['id'] }}" data-username="{{ c['username'] }}" {% if c['id'] == s['credential_id'] %}selected{% endif %}>{{ c['name'] }}</option>
{% endfor %}
<option value="new">+ Neue Zugangsdaten anlegen</option>
</select>
</div>
<div class="new-credential-fields hidden">
<div class="field"><label>Name der Zugangsdaten</label>
<input type="text" name="new_credential_name" placeholder="z.B. Lager-Switche">
</div>
<div class="field"><label>Username</label>
<input type="text" name="new_credential_username" placeholder="z.B. admin">
</div>
<div class="field"><label>Passwort</label>
<input type="password" id="password_edit{{ loop.index }}" name="new_credential_password">
</div>
<div class="field"><label>Passwort bestätigen</label>
<input type="password" id="password_confirm_edit{{ loop.index }}" name="new_credential_password_confirm">
<div class="invalid-feedback">Passwörter stimmen nicht überein!</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" style="margin-right:auto;" onclick="openTerminal(this.closest('form'))">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M6 9l4 3-4 3M13 15h5"/></svg>
Verbindung testen
</button>
<button type="button" class="btn btn-secondary" data-close-modal>Abbrechen</button>
<button type="submit" class="btn btn-primary">Speichern</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% endif %}
{% if can_create %}
<!-- Modal: Neuer Switch -->
<div class="modal-overlay" id="addSwitchModal">