Files
swyx/README.md
T
Sven 78b17a1350 SwyxTray und Firefox-Plugin in das Repository aufnehmen
Windows-Tray-Anwendung (CLMgr-Anbindung an SwyxIt!) samt WebSocket-Zugang,
Firefox-Erweiterung und Browser-Beispielclient.
2026-08-21 11:33:52 +02:00

604 lines
29 KiB
Markdown

# SwyxTray
Windows-Anwendung **ohne Fenster**, die nur ein Symbol im Infobereich der
Taskleiste (unten rechts) anzeigt und sich per Swyx Client SDK mit dem lokal
laufenden SwyxIt!-Client verbindet.
## Was die App tut
- Verbindet sich beim Start mit dem **Client Line Manager** (`CLMgr.exe`) der
aktuellen Windows-Sitzung — es wird kein zweiter Client gestartet, sondern der
bereits laufende mitbenutzt.
- Zeigt den Leitungszustand über die Farbe des Tray-Symbols:
| Farbe | Zustand |
|-----------|----------------------------------|
| grau | SwyxIt! nicht verbunden |
| rot | SwyxServer nicht erreichbar |
| grün | bereit, keine belegte Leitung |
| blau | eingehender Ruf |
| türkis | Verbindungsaufbau (ausgehend) |
| orange | Gespräch aktiv |
| violett | Gespräch gehalten |
- Blendet bei eingehenden Rufen eine Sprechblase mit Name und Rufnummer ein.
- Stellt einen **WebSocket-Zugang auf 127.0.0.1** bereit, über den eine im
Browser laufende Seite wählen, annehmen und auflegen kann und im Gegenzug
jede Zustandsänderung zugestellt bekommt (siehe unten).
- Kontextmenü (Rechtsklick): Status, belegte Leitungen, *WebSocket-Zugang*,
*Lese Tabs*, *Tab öffnen*, *Tab schliessen*, *Neu verbinden*,
*Protokoll öffnen*, *Beenden*. Doppelklick zeigt den Status als Sprechblase.
Die drei Tab-Punkte laufen über den Tab-Verwaltungskanal zum Firefox-Plugin
(siehe unten); ohne verbundenes Plugin (oder mit `PluginPort: 0`) bleiben
sie ohne Ergebnis bzw. grau. *Lese Tabs* holt die Titel der offenen Tabs
und zeigt sie als Sprechblase — die vollständige Liste samt Adressen landet
im Protokoll. *Tab öffnen* öffnet einen Tab mit `https://www.assecutor.de`,
*Tab schliessen* schließt alle Tabs, deren Titel „Assecutor Data Service
GmbH" enthält — als Komposition aus `list` und `close` je Treffer, wie beim
`focus`-Kommando (beide Werte sind Konstanten in
[TrayApplicationContext.cs](src/SwyxTray/TrayApplicationContext.cs)).
## Verwendetes SDK
Offizielles NuGet-Paket von Enreach:
```xml
<PackageReference Include="Swyx.Client.ClmgrAPI" Version="14.21.0" />
```
Das Paket enthält die Interop-Wrapper der ClientLineManager-COM-API
(`IpPbx.CLMgrLib`) und wählt über `$(Platform)` automatisch `Interop.CLMgr.dll`
(x86) bzw. `Interop64.CLMgr.dll` (x64). **Deshalb muss `Platform` gesetzt sein
— bei `AnyCPU` bricht der Build mit einem Fehler des Pakets ab.**
Die Version 14.21.0 passt zur hier installierten SwyxIt!-Version 14.21.4345.0.
Bei einem Client-Update die Paketversion mitziehen.
Genutzte Schnittstellen:
- `IClientLineMgrDisp``DispInit`, `DispRegisterUser`, `DispNumberOfLines`,
`DispGetLine`, `DispSelectedLineNumber`, `DispIsServerUp`,
`DispGetCurrentUser`, `DispGetCurrentServer`, `DispReleaseUser`
- `IClientLineDisp``DispState`, `DispPeerNumber`, `DispPeerName`
- `IClientLineMgrEventsPub_Event``PubOnLineMgrNotification(msg, param)`
- `CLMgrLineStates` — die Leitungszustände als typisierte Aufzählung
## Aufbau
| Datei | Zweck |
|---|---|
| [Program.cs](src/SwyxTray/Program.cs) | Einstieg, STA-Thread, Einzelinstanz-Sperre, Fehlerprotokollierung |
| [TrayApplicationContext.cs](src/SwyxTray/TrayApplicationContext.cs) | `NotifyIcon`, Kontextmenü, Sprechblasen |
| [TrayIcons.cs](src/SwyxTray/TrayIcons.cs) | erzeugt die Symbole zur Laufzeit (keine .ico-Dateien nötig) |
| [Swyx/SwyxClient.cs](src/SwyxTray/Swyx/SwyxClient.cs) | CLMgr-Verbindung, Ereignisbehandlung, Zustandsabfrage |
| [Swyx/SwyxModels.cs](src/SwyxTray/Swyx/SwyxModels.cs) | `SwyxSnapshot`, `SwyxLineInfo`, Gesamtzustand |
| [Swyx/LineStateExtensions.cs](src/SwyxTray/Swyx/LineStateExtensions.cs) | Einordnung und deutscher Klartext der `CLMgrLineStates` |
| [Swyx/PubCLMgrMessages.cs](src/SwyxTray/Swyx/PubCLMgrMessages.cs) | Meldungsnummern aus `CLMgrPubTypes.h` |
| [Web/LocalWebSocketServer.cs](src/SwyxTray/Web/LocalWebSocketServer.cs) | Listener auf 127.0.0.1 und ::1, Verbindungsverwaltung, Verteilen der Zustände |
| [Web/WebSocketHandshake.cs](src/SwyxTray/Web/WebSocketHandshake.cs) | HTTP-Upgrade, Prüfung von Token, Origin und Host |
| [Web/WebSocketSession.cs](src/SwyxTray/Web/WebSocketSession.cs) | eine Verbindung: Empfangsschleife, serialisiertes Senden |
| [Web/CommandExecutor.cs](src/SwyxTray/Web/CommandExecutor.cs) | Kommandos → `SwyxClient`, mit Zeitgrenze |
| [Web/UiDispatcher.cs](src/SwyxTray/Web/UiDispatcher.cs) | Sprung aus den Netzwerk-Threads in den UI-Thread |
| [Web/Protocol.cs](src/SwyxTray/Web/Protocol.cs) | die JSON-Nachrichten |
| [Web/WebSocketConfig.cs](src/SwyxTray/Web/WebSocketConfig.cs) | Port, Token, erlaubte Origins |
| [examples/browser-client.html](examples/browser-client.html) | Beispielseite zum Erproben des Zugangs |
| [src/SwyxFFPlugin/](src/SwyxFFPlugin/) | die Firefox-Erweiterung: [manifest.json](src/SwyxFFPlugin/manifest.json) und [background.js](src/SwyxFFPlugin/background.js) |
| [Log.cs](src/SwyxTray/Log.cs) | Dateiprotokoll unter `%LOCALAPPDATA%\SwyxTray\swyxtray.log` |
### Zwei Entwurfsentscheidungen
**Ereignisgesteuert statt Polling.** `PubOnLineMgrNotification` liefert jede
Zustandsänderung sofort. Bei *jeder* Meldung wird der komplette Zustand neu
gelesen — das kostet nur wenige COM-Zugriffe und macht die Anzeige unabhängig
davon, ob eine Meldungsnummer richtig zugeordnet ist. `PubCLMgrMessages` steckt
nämlich nur im C-Header des SDK, nicht in der TypeLib. Zusätzlich läuft alle
5 Sekunden ein Sicherheitszyklus, der einen beendeten CLMgr erkennt und die
Verbindung wiederherstellt.
**Alles im UI-Thread.** CLMgr ist ein STA-COM-Server; der WinForms-Thread ist
STA und hat bereits eine Nachrichtenschleife. Dadurch werden die COM-Ereignisse
automatisch dorthin gemarshallt — kein manuelles Marshalling, keine Sperren.
### `CoInitializeSecurity` ist nicht optional
`CLMgr.exe` ist ein eigener Prozess. Beim Anmelden der Ereignissenke ruft er in
diesen Prozess **zurück** — und ohne eigene Sicherheitsvorgabe weist COM diesen
Rückruf mit `E_ACCESSDENIED` ab. [NativeMethods.InitializeComSecurity](src/SwyxTray/NativeMethods.cs)
setzt deshalb vor dem ersten COM-Aufruf `RPC_C_AUTHN_LEVEL_NONE` /
`RPC_C_IMP_LEVEL_IDENTIFY`. Das betrifft nur diesen Prozess, der außer der
Ereignissenke nichts nach außen anbietet. Wer es enger fassen will, probiert
`RPC_C_AUTHN_LEVEL_CONNECT` (2) — das genügt, solange CLMgr in derselben Sitzung
unter demselben Konto läuft.
Schlägt das Anmelden trotzdem fehl, ist das **kein** Abbruchgrund: die App bleibt
verbunden und schaltet den Abfragezyklus von 5 s auf 1 s herunter.
## Zugriff aus dem Browser (WebSocket)
Die App hört auf `ws://127.0.0.1:17654/` und spricht dort JSON — eine
Nachricht je Frame. Über dieselbe Verbindung laufen beide Richtungen: die Seite
schickt Kommandos, die App schickt unaufgefordert jede Zustandsänderung.
### Einrichten
Beim ersten Start entsteht `%LOCALAPPDATA%\SwyxTray\websocket.json`:
```json
{
"Enabled": true,
"Port": 17654,
"PluginPort": 17655,
"Token": "E59BF4…",
"AllowedOrigins": [],
"MaxConnections": 8,
"AllowRemoteAccess": false
}
```
Solange die Prüfungen abgeschaltet sind (siehe unten), sind `Token` und
`AllowedOrigins` ohne Wirkung — für einen ersten Test ist an dieser Datei nichts
zu tun. Der Rest dieses Abschnitts gilt für den eingeschalteten Zustand.
Das Token wird zufällig erzeugt, die Liste der Origins bleibt leer — **ohne
Eintrag wird jede Verbindung aus einem Browser abgewiesen.** Das ist Absicht,
denn ein offener Port ohne diese Schranke hieße: jede beliebige Seite, die
jemand im Browser öffnet, kann Anrufe auslösen. Einzutragen ist der Ursprung
der aufrufenden Seite, also z. B. `https://crm.example.local` — ohne Pfad. Nach
einer Änderung muss die App neu gestartet werden.
*WebSocket-Zugang → Verbindungsdaten kopieren* im Kontextmenü legt Adresse und
Token in die Zwischenablage.
### Verbinden
```js
const ws = new WebSocket("ws://127.0.0.1:17654/?token=…", ["swyxtray.v1"]);
```
Das Token darf statt in der URL auch als zweites Unterprotokoll stehen
(`["swyxtray.v1", "token.…"]`) — nützlich, wenn das Geheimnis nicht in
Adresszeilen und Verlaufslisten auftauchen soll. Die WebSocket-API des Browsers
kennt keine eigenen Header, deshalb diese beiden Wege.
Ein vollständiges Beispiel mit Wiederverbindung steht in
[examples/browser-client.html](examples/browser-client.html).
### Firefox-Plugin (SwyxFFPlugin): der Tab-Verwaltungskanal
Für das Firefox-Plugin hört die App zusätzlich auf einem **zweiten Port**
voreingestellt **17655** (`PluginPort` in `websocket.json`, `0` schaltet ihn
ab; in einer bestehenden Datei ohne diesen Eintrag gilt automatisch 17655).
Auf diesem Port läuft **ausschließlich die Tab-Verwaltung** — keine
Snapshots, keine Anruf-Ereignisse, keine Telefonie-Kommandos (sie werden mit
`ok: false` abgewiesen). Nach dem `hello` ist die Richtung umgekehrt zum
Hauptport: **SwyxTray fragt, das Plugin antwortet.** Es gibt drei Aufträge,
jeder wird mit einem `tabresult` samt unveränderter `id` beantwortet:
**Offene Tabs auslesen:**
```json
an das Plugin: {"type":"tab","id":1,"action":"list"}
vom Plugin: {"cmd":"tabresult","id":1,"ok":true,
"tabs":[{"id":42,"title":"Kundenakte Muster GmbH",
"url":"https://crm.example.local/kunden/4711","active":false}]}
```
**Tab mit einer URL öffnen** (der neue Tab ist in Firefox von selbst vorn):
```json
an das Plugin: {"type":"tab","id":2,"action":"open","url":"https://crm.example.local/kunden/4711"}
vom Plugin: {"cmd":"tabresult","id":2,"ok":true,"tabId":43}
```
**Tab schließen:**
```json
an das Plugin: {"type":"tab","id":3,"action":"close","tabId":42}
vom Plugin: {"cmd":"tabresult","id":3,"ok":true}
```
Antwortet das Plugin binnen fünf Sekunden nicht (oder ist keines verbunden),
gilt der Auftrag als gescheitert; die darauf aufbauenden Kommandos des
Hauptports (`focus`, `tabs`, `opentab`, `closetab`) enden dann mit
`ok: false`. Vom Plugin aus sind nur `tabresult` und `ping` zulässig. Ein
„Tab aktivieren“ kennt der Kanal bewusst nicht — beim `focus`-Kommando holt
SwyxTray einen vorhandenen Tab als Komposition nach vorn: `list`, passenden
Tab per `close` schließen und die URL per `open` neu öffnen.
Das Plugin selbst liegt in [src/SwyxFFPlugin/](src/SwyxFFPlugin/) —
[background.js](src/SwyxFFPlugin/background.js) setzt genau diese drei
Aufträge um und verbindet sich selbsttätig neu (1 s Abstand, je Fehlversuch
verdoppelt bis 30 s), falls SwyxTray noch nicht oder nicht mehr läuft.
Installation zum Erproben: `about:debugging#/runtime/this-firefox`
*Temporäres Add-on laden…* → die `manifest.json` des Ordners wählen (Näheres
im [README des Plugins](src/SwyxFFPlugin/README.md)).
Sind die Prüfungen wieder eingeschaltet, braucht die Adresse zusätzlich das
Token (`ws://127.0.0.1:17655/?token=…`), und in `AllowedOrigins` muss der
Origin des Plugins stehen. Firefox sendet dafür `moz-extension://<UUID>`,
wobei die UUID je Installation zufällig vergeben wird — sie steht in
`about:debugging` unter „Interne UUID“ der Erweiterung.
### Kommandos (Seite → App)
| Kommando | Felder | Wirkung |
|---|---|---|
| `call` | `number` | wählt auf einer freien Leitung |
| `answer` | `line` (optional) | nimmt an; ohne Angabe die klingelnde Leitung |
| `hangup` | `line` (optional) | legt auf; ohne Angabe die belegte Leitung |
| `hold` | `line` (optional) | stellt das Gespräch in die Warteschleife |
| `focus` | `url`, `title` (optional) | aktiviert oder öffnet den passenden Firefox-Tab über das Plugin |
| `tabs` | — | liest die offenen Firefox-Tabs über das Plugin aus |
| `opentab` | `url` | öffnet über das Plugin einen neuen Firefox-Tab |
| `closetab` | `tabId` | schließt über das Plugin den Tab mit dieser Id |
| `status` | — | fordert den vollen Zustand an |
| `reconnect` | — | verwirft die CLMgr-Verbindung und baut sie neu auf |
| `ping` | — | Lebenszeichen |
`id` ist frei wählbar und steht in der Antwort wieder da — anders als bei HTTP
gibt es keine eingebaute Zuordnung von Anfrage und Antwort.
```json
{"id":7,"cmd":"call","number":"+49 30 1234567"}
{"type":"result","id":7,"ok":true,"line":2}
```
Leitungen werden nach außen **1-basiert** gezählt, so wie sie im Kontextmenü und
in SwyxIt! erscheinen. Rufnummern dürfen Leerzeichen, Bindestriche, Punkte und
Klammern enthalten; alles andere außer Ziffern, führendem `+`, `*` und `#` wird
abgewiesen, statt es an CLMgr weiterzureichen.
Ein `ok: true` auf `call` heißt nur, dass CLMgr den Auftrag angenommen hat. Das
Wählen läuft asynchron — ob der Ruf zustande kommt, steht erst in den folgenden
Zustandsmeldungen.
`focus` (seit Protokollversion 3, ab Version 5 ausschließlich über das
**Firefox-Plugin**, siehe unten): war ein passender Tab schon offen — sein
Titel enthält den `title` oder seine Adresse ist die `url` —, wird er
geschlossen und die `url` neu geöffnet (`"focused": true`), sonst nur neu
geöffnet (`"focused": false`). Ist kein Plugin verbunden oder antwortet es
nicht, endet das Kommando mit `ok: false` — einen anderen Weg, eine Seite
anzuzeigen, gibt es nicht. Die `url` ist Pflicht, `title` dient nur dem
Tab-Abgleich; erlaubt sind nur absolute `http(s)`-Adressen.
```json
{"id":9,"cmd":"focus","title":"Kundenakte Muster GmbH","url":"https://crm.example.local/kunden/4711"}
{"type":"result","id":9,"ok":true,"focused":false}
```
`tabs`, `opentab`, `closetab` (seit Protokollversion 6): die drei Aufträge des
Tab-Verwaltungskanals stehen damit auch Webseiten offen — SwyxTray reicht sie
unverändert an das Firefox-Plugin durch. Ist kein Plugin verbunden oder
antwortet es nicht binnen fünf Sekunden, endet das Kommando mit `ok: false`.
Für `opentab` gilt dieselbe Regel wie bei `focus`: nur absolute
`http(s)`-Adressen. Die `tabId` für `closetab` stammt aus einer vorherigen
`tabs`-Antwort; nach Öffnen oder Schließen verschickt die App **keine** neue
Tab-Liste von selbst — wer sie aktuell braucht, fragt mit `tabs` nach:
```json
{"id":10,"cmd":"tabs"}
{"type":"result","id":10,"ok":true,
"tabs":[{"id":42,"title":"Kundenakte Muster GmbH",
"url":"https://crm.example.local/kunden/4711","active":false}]}
{"id":11,"cmd":"opentab","url":"https://crm.example.local/kunden/4711"}
{"type":"result","id":11,"ok":true,"tabId":43}
{"id":12,"cmd":"closetab","tabId":43}
{"type":"result","id":12,"ok":true}
```
### Meldungen (App → Seite)
Direkt nach dem Verbinden kommen `hello` und ein `snapshot`, danach ein
`snapshot` bei **jeder** Zustandsänderung. Die Seite muss also nie pollen und
nie selbst Zustand mitführen:
```json
{"type":"snapshot","connected":true,"serverUp":true,"overall":"Ringing",
"statusText":"Eingehender Ruf","user":"…","server":"…",
"lines":[{"line":1,"state":"LSRinging","stateCode":3,"stateText":"klingelt",
"peer":"Muster GmbH (+493012345)","peerNumber":"+493012345",
"peerName":"Muster GmbH","busy":true,"selected":true}]}
```
`overall` ist derselbe Gesamtzustand, den auch die Farbe des Tray-Symbols zeigt.
Zusätzlich zum `snapshot` meldet die App **Anruf-Ereignisse** als eigene
Nachricht vom Typ `call` — damit muss eine Seite keine zwei Zustände
vergleichen, um einen eingehenden Ruf zu erkennen:
| `event` | Bedeutung |
|---|---|
| `incoming` | Leitung beginnt zu klingeln (auch Anklopfen) |
| `outgoing` | Wahl eines ausgehenden Rufs beginnt |
| `connected` | Gespräch steht; `direction` sagt `incoming`/`outgoing`, sofern erkennbar |
| `ended` | Leitung ist wieder frei; Peer-Angaben aus dem letzten belegten Zustand |
```json
{"type":"call","event":"incoming","line":1,
"peer":"Muster GmbH (+493012345)","peerNumber":"+493012345","peerName":"Muster GmbH"}
```
Die Reihenfolge ist garantiert: erst der `snapshot`, unmittelbar danach die
daraus abgeleiteten `call`-Ereignisse. Ein typischer Ablauf für ein
angenommenes Gespräch ist also `incoming``connected` (mit
`"direction":"incoming"`) → `ended`, jeweils mit einem `snapshot` davor.
Wählt die Seite selbst per Kommando `call`, folgen `outgoing``connected`
`ended` auf demselben Weg. Diese Ereignisse gibt es seit Protokollversion 2
(Feld `protocol` im `hello`).
### Was den Zugang absichert
> **Zur Zeit greift davon nur die Bindung.** Token-, Origin- und Host-Prüfung
> sind für die Erprobung abgeschaltet, und `AllowRemoteAccess` steht auf `true`
> — siehe die beiden folgenden Abschnitte. Der Rest dieses Abschnitts
> beschreibt den eingeschalteten Zustand.
Vier Schranken, die zusammen wirken:
1. **Gebunden nur an 127.0.0.1 und ::1.** Von außen ist der Port nicht
erreichbar, und die Windows-Firewall fragt beim Start nichts nach. Diese
Schranke fällt mit `AllowRemoteAccess` (siehe unten).
2. **Token** in jedem Handshake, zeitkonstant verglichen.
3. **Origin-Allowlist.** Für WebSockets gilt die Same-Origin-Policy *nicht*
der Browser blockiert nichts, es gibt keinen Preflight. Diese Prüfung ist
damit nicht Härtung, sondern die eigentliche Schranke gegen fremde Seiten.
Fehlt der Origin-Header ganz, ist der Aufrufer kein Browser; das ist
zulässig, weil das Token bereits nachgewiesen wurde. Eine Webseite kann den
Header nicht weglassen.
4. **Host-Prüfung** auf `127.0.0.1`, `localhost` bzw. `[::1]`. Ohne sie könnte
eine Seite einen eigenen Namen auf 127.0.0.1 zeigen lassen (DNS-Rebinding).
Entfällt ebenfalls mit `AllowRemoteAccess`.
### Zugriff von einem anderen Rechner
Mit `"AllowRemoteAccess": true` bindet die App statt an die Loopback-Adressen an
**alle Schnittstellen** (`0.0.0.0` und `::`) und ist damit im lokalen Netz
erreichbar:
```js
new WebSocket("ws://192.168.180.135:17654/?token=…");
```
Die erreichbaren Adressen stehen beim Start im Protokoll, und
*Verbindungsdaten kopieren* liefert dann ebenfalls die LAN-Adresse statt
127.0.0.1.
Drei Dinge ändern sich damit:
- **Die Host-Prüfung entfällt.** Unter welchem Namen oder welcher Adresse ein
entfernter Rechner die App anspricht, kann sie nicht wissen. Der Schutz gegen
DNS-Rebinding ruht damit allein auf Token und Origin-Liste.
- **Die Windows-Firewall redet mit.** Ist sie aktiv, braucht es eine
eingehende Regel, sonst kommt vom anderen Rechner nichts an:
```powershell
New-NetFirewallRule -DisplayName "SwyxTray WebSocket" -Direction Inbound `
-Protocol TCP -LocalPort 17654 -Action Allow
```
Das erfordert Administratorrechte — die einzige Stelle des Vorhabens, an der
sie nötig sind, und ein Grund, warum Loopback die Voreinstellung bleibt.
- **`ws://` gilt aus der Ferne nicht mehr als vertrauenswürdig.** Die
Ausnahme für Mixed Content gilt nur für Loopback. Eine **HTTPS**-Seite auf dem
anderen Rechner kann `ws://192.168.…` also nicht öffnen — für den Test muss
die Seite über `http://` kommen, oder es braucht `wss://` mit einem
Zertifikat, das die App derzeit nicht anbietet.
Wer den Zugang nicht nur zum Testen aus dem Netz erreichbar macht, sollte sich
bewusst sein: Token und Origin-Liste sind dann das Einzige, was zwischen dem
Netz und dem Telefon des Benutzers steht.
### Zur Zeit sind die Prüfungen abgeschaltet
**Für die Erprobung entfallen Token-, Origin- und Host-Prüfung — in jedem
Build.** Eine Seite verbindet sich ohne jede Einrichtung, auch direkt aus dem
Dateisystem, wo der Browser den Origin `null` sendet:
```js
new WebSocket("ws://127.0.0.1:17654/");
```
Die Konfigurationsdatei wird nur noch für Port, Verbindungsgrenze und
`AllowRemoteAccess` gelesen; ein fehlendes Token verhindert den Start nicht.
Zusammen mit dem eingeschalteten Zugriff aus dem Netz heißt das: **jeder im
selben Netz kann über diesen Anschluss telefonieren und mitlesen, wer anruft.**
In diesem Zustand nicht ausliefern.
Erkennbar ist das am Protokoll und am Kontextmenü, das dann
*WebSocket: Port 17654, 1 Verbindung(en) — im Netz — OHNE PRUEFUNG* zeigt:
```
[INF] WebSocket-Zugang aus dem Netz erreichbar unter ws://192.168.180.135:17654/
— OHNE PRUEFUNG: Token, Origin und Host werden nicht geprueft. Jeder im
selben Netz kann telefonieren und mitlesen. Nicht in diesem Zustand
ausliefern.
```
Wieder einschalten lässt sich alles mit **einer Zeile** in
[Web/WebSocketConfig.cs](src/SwyxTray/Web/WebSocketConfig.cs) — die Prüfungen
selbst stehen unverändert in
[Web/WebSocketHandshake.cs](src/SwyxTray/Web/WebSocketHandshake.cs):
```csharp
public static readonly bool SecurityDisabled = true; // <- zur Zeit
```
Der dort im Kommentar hinterlegte Ersatz macht daraus wieder ein
`#if DEBUG` — dann prüft der Release-Build alles, während sich der Debug-Build
weiterhin ohne Einrichtung erproben lässt.
### Was im Browser noch dazwischenkommen kann
`ws://127.0.0.1` gilt als vertrauenswürdiger Ursprung und wird auch aus einer
HTTPS-Seite heraus nicht als Mixed Content blockiert. Neuere Chrome-Versionen
beschränken allerdings Zugriffe aus dem öffentlichen ins lokale Netz (*Local
Network Access*) und fragen den Benutzer — das lässt sich per
Unternehmensrichtlinie freigeben, aber nicht durch einen Antwort-Header lösen.
**Vor dem Rollout mit der IT klären.**
Browser lösen `localhost` häufig zuerst nach `::1` auf. Die App hört deshalb auf
beiden Loopback-Adressen; sonst funktionierte `ws://127.0.0.1:…`, während
`ws://localhost:…` ohne erkennbaren Grund scheiterte.
### Zwei Entwurfsentscheidungen
**Kein `HttpListener`.** Der läuft über http.sys und braucht auch für
127.0.0.1 eine einmalige URL-Reservierung mit Administratorrechten
(`netsh http add urlacl`). Die App soll ohne solche Rechte auskommen, deshalb
ein schlichter `TcpListener`. Vom WebSocket-Protokoll bleibt dabei nur der
HTTP-Upgrade von Hand zu schreiben — Rahmen, Maskierung, Ping/Pong und der
Schließvorgang kommen von `WebSocket.CreateFromStream` aus der
Klassenbibliothek.
**Kommandos laufen über den UI-Thread.** CLMgr ist ein STA-COM-Server, und die
App bündelt alle COM-Zugriffe im WinForms-Thread. Der
[UiDispatcher](src/SwyxTray/Web/UiDispatcher.cs) schleust jedes Kommando aus dem
Netzwerk-Thread dorthin, mit einer Zeitgrenze von 10 Sekunden: bliebe die
Nachrichtenschleife hängen, fröre die Verbindung sonst stumm ein.
## Bauen und Starten
```powershell
cd src\SwyxTray
dotnet build -c Release
.\bin\x64\Release\net10.0-windows\win-x64\SwyxTray.exe --verbose
```
`--verbose` schreibt zusätzlich jede CLMgr-Meldung ins Protokoll — für die
erste Inbetriebnahme empfehlenswert.
### Aus VS Code (F5)
Die Startkonfiguration **„SwyxTray debuggen (Desktop-Sitzung)"** in
[.vscode/launch.json](.vscode/launch.json) baut, startet und hängt den Debugger
an. Sie startet die App über eine geplante Aufgabe
([.vscode/start-desktop.ps1](.vscode/start-desktop.ps1)) — nötig, weil bei
einer Remote-Verbindung (SSH, Tunnel) der Debugger Prozesse sonst in der
Sitzung des VS-Code-Servers startet, meist Sitzung 0. Dort gibt es keine
Taskleiste, die App läuft dann ohne sichtbares Tray-Symbol. Die geplante
Aufgabe läuft dagegen mit dem interaktiven Token des Benutzers und landet auf
dem Desktop; danach hängt sich der Debugger per Prozessname an. Eine bereits
laufende Instanz wird vorher beendet, sonst griffe die Einzelinstanz-Sperre.
Autostart: eine Verknüpfung auf die EXE in
`shell:startup` (`%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup`)
ablegen. Kein Dienst, keine Administratorrechte.
### Voraussetzungen
- SwyxIt! muss laufen (getestet gegen 14.21.4345.0).
- **.NET-10-Desktop-Laufzeit.** Auf diesem Rechner ist nur .NET 10 installiert,
deshalb steht `net10.0-windows` in der csproj. Für .NET 8 einfach
`<TargetFramework>net8.0-windows</TargetFramework>` setzen. Alternativ ohne
Laufzeitabhängigkeit ausliefern:
```powershell
dotnet publish -c Release -r win-x64 --self-contained true
```
- Die App läuft **in derselben Windows-Sitzung wie SwyxIt!** — als normale
Benutzeranwendung, nicht als Dienst (siehe unten).
## Warum kein Windows-Dienst
Ursprünglich war ein Windows-Dienst vorgesehen. Das funktioniert für diesen
Zweck nicht: ein Dienst läuft in **Session 0**, der SwyxIt!-Client und sein
`CLMgr.exe` in der Benutzersitzung (1, 2, …). Über die Sitzungsgrenze hinweg
lässt sich der laufende Client nicht ansprechen. Ein Dienst müsste dafür einen
Hilfsprozess per `CreateProcessAsUser` in die Benutzersitzung starten und mit
ihm über eine Named Pipe reden. Die Tray-Anwendung umgeht das Problem
vollständig, weil sie von vornherein in der richtigen Sitzung läuft.
## Stand der Prüfung
- **Übersetzung gegen das echte SDK: erfolgreich.** Alle verwendeten Typen und
Signaturen stammen aus `Interop64.CLMgr.dll` (Version 2.0.0.0) des Pakets und
wurden gegen die installierte TypeLib `CLMgr 2.0`
(`{F8E552F7-4C00-11D3-80BC-00105A653379}`, registriert auf
`C:\Program Files (x86)\Swyx\SwyxIt!\CLMgr.exe`) verifiziert.
**Gegen den echten, laufenden Client verifiziert** (SwyxIt! 14.21.4345.0):
```
[INF] SwyxTray gestartet (PID 3348, Sitzung 0, x64).
[INF] Mit CLMgr verbunden (UserId 0, Benutzer 'Assecutor, G.Timm',
Server '127.0.0.1', 4 Leitungen).
[DBG] Zustand: Idle | L1=LSInactive(0) * | L2=LSInactive(0)
| L3=LSInactive(0) | L4=LSInactive(0)
```
Bestätigt sind damit: COM-Aktivierung, `DispInit`, `DispRegisterUser`, Anmelden
der Ereignissenke, Auslesen aller vier Leitungen samt Zustand und ausgewählter
Leitung, Ableitung des Gesamtzustands, Symbolerzeugung, Änderungserkennung
(keine Doppelmeldungen über mehrere Zyklen) und das Dateiprotokoll.
**WebSocket-Zugang gegen die laufende App geprüft** (mit einem
`ClientWebSocket` als Gegenstelle):
```
[INF] WebSocket-Zugang bereit auf ws://127.0.0.1:17654/
[INF] WebSocket-Anfrage von 127.0.0.1:51312 abgewiesen (403): Token fehlt oder ist falsch.
[INF] WebSocket-Anfrage von 127.0.0.1:51313 abgewiesen (403): Origin 'https://boese.example'
ist nicht freigegeben.
[INF] WebSocket 1 verbunden (127.0.0.1:51314, Origin (ohne Origin)).
[INF] WebSocket 2 verbunden ([::1]:64293, Origin https://test.example).
```
Bestätigt sind damit: Handshake samt Unterprotokoll-Aushandlung, Token- und
Origin-Prüfung in beide Richtungen, Erreichbarkeit über `127.0.0.1` *und*
`localhost`/`::1`, `hello` und `snapshot` beim Verbinden, die Kommandos `ping`,
`status`, `hangup`, das Abweisen ungültiger Rufnummern, unbekannter Kommandos
und kaputten JSONs sowie der Schließvorgang.
Geprüft wurde außerdem der jetzige Zustand mit abgeschalteten Prüfungen: der
**Release-Build** nimmt eine Verbindung über die LAN-Adresse ohne Token und mit
fremdem Origin an. Zuvor, mit eingeschalteten Prüfungen, wies er dieselbe
Anfrage mit 403 ab, während der Debug-Build sie annahm — beide Zweige sind also
erprobt.
Mit `"AllowRemoteAccess": true` lauscht die App auf `0.0.0.0:17654` und
`[::]:17654` (bestätigt über `Get-NetTCPConnection`); eine Verbindung über die
LAN-Adresse `ws://192.168.180.135:17654/` wird angenommen, dieselbe mit falschem
Token weiterhin abgewiesen. **Der Test lief vom selben Rechner aus** — dass ein
entfernter Rechner durchkommt, hängt zusätzlich an der Firewall. Auf dem
Testrechner ist sie in allen Profilen abgeschaltet, dort war keine Regel nötig.
**Noch nicht am realen Verkehr geprüft:** Zustandswechsel während eines echten
Gesprächs — Klingeln, Annehmen, Halten, Auflegen — und damit die Sprechblase bei
eingehenden Rufen, der Farbwechsel des Symbols und das Verteilen der
Zustandsmeldungen an verbundene Seiten. Dafür ist ein Testanruf nötig; `call`
wurde bewusst nur mit ungültigen Nummern erprobt, um keinen echten Ruf
auszulösen. Ebenso ungeprüft ist das Symbol im Infobereich selbst, weil die
verwendete Konsole in Session 0 lief und dort keine Taskleiste existiert — und
damit auch die Beispielseite in einem echten Browser.
### Zwei Hürden auf dem Weg dorthin
Beide betrafen die Umgebung, nicht den Code, sind aber für die Auslieferung
relevant:
1. **Smart App Control blockierte den COM-Proxy.** Jedes `QueryInterface` über
die Prozessgrenze lädt `C:\Program Files\Common Files\Swyx\CLMgrPs64.dll` in
den eigenen Prozess; genau diese Swyx-Datei wurde abgewiesen (`0x800711C7`,
CodeIntegrity 3033/3077/3118, *Enterprise signing level requirements*).
Das Abschalten von Smart App Control wirkt erst **nach einem Neustart** —
der Registry-Wert steht sofort auf `0`, `Win32_DeviceGuard` meldet aber bis
zum Reboot weiter Erzwingung. **Läuft beim Kunden WDAC, muss `CLMgrPs64.dll`
im Regelwerk freigegeben sein** — das trifft jede Anwendung, die den
Swyx-Proxy lädt.
2. **`IConnectionPoint::Advise` scheiterte mit `E_ACCESSDENIED`** — gelöst durch
`CoInitializeSecurity` (siehe oben).
## Quellen
- [INFO: SwyxIt! Client SDK — Enreach Help-Center](https://service.swyx.net/hc/en-gb/articles/360011686580-INFO-SwyxIt-Client-SDK)
- [SwyxIt! Client SDK — API-Referenz](https://clientsdk.swyx.engineering/)
- [IClientLineMgrEventsPub](https://clientsdk.swyx.engineering/interface_i_client_line_mgr_events_pub.html)
- [IClientLineMgrPub](https://clientsdk.swyx.engineering/interface_i_client_line_mgr_pub.html)
- [CLMgrPubTypes.h — PubCLMgrMessages](https://clientsdk.swyx.engineering/_c_l_mgr_pub_types_8h.html)
- [NuGet: Swyx.Client.ClmgrAPI](https://www.nuget.org/packages/Swyx.Client.ClmgrAPI)