diff --git a/.gitignore b/.gitignore index 4029b5d..aff3bf3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ backend/target/ !backend/.mvn/wrapper/maven-wrapper.properties # Frontend -frontend/node_modules/ +node_modules/ frontend/dist/ frontend/.env frontend/.env.local diff --git a/.vscode/launch.json b/.vscode/launch.json index a686855..72c9d63 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,7 +14,7 @@ { // Wie oben, aber der eingebaute SwyxTray-Mock wird über das Profil "mock" // eingeschaltet (ohne das Profil gibt es ihn nicht) und /api/config zeigt - // auf ihn statt auf 192.168.180.135:17654 – so lässt sich die Startseite + // auf ihn statt auf 127.0.0.1:17654 – so lässt sich die Startseite // ohne echte Telefonanlage ausprobieren (eingehenden Anruf auslösen: // siehe README). "type": "java", diff --git a/Dockerfile b/Dockerfile index 872c779..39f3228 100644 --- a/Dockerfile +++ b/Dockerfile @@ -62,7 +62,7 @@ USER swyx EXPOSE 8080 # Adresse der SwyxTray-App zur Laufzeit überschreibbar (Spring Relaxed Binding): -# -e APP_WEBSOCKET_HOST=192.168.180.135 -e APP_WEBSOCKET_PORT=17654 +# -e APP_WEBSOCKET_HOST=127.0.0.1 -e APP_WEBSOCKET_PORT=17654 # -e APP_WEBSOCKET_PATH=/ws -e APP_WEBSOCKET_SECURE=false # # SPRING_PROFILES_ACTIVE bleibt bewusst leer – der SwyxTray-Mock bliebe sonst diff --git a/README.md b/README.md index a180d77..ba8f35d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # SwyxWeb React-Frontend mit Spring-Boot-Backend. Die Startseite verbindet sich **aus dem Browser heraus** -per WebSocket mit der **SwyxTray-App** (`ws://192.168.180.135:17654/ws`), meldet eingehende +per WebSocket mit der **SwyxTray-App** (`ws://127.0.0.1:17654/ws`), meldet eingehende Anrufe und startet ausgehende Anrufe. ``` @@ -25,7 +25,7 @@ Das Backend baut **keine** eigene WebSocket-Verbindung auf; es hat zwei Aufgaben `mock`**; im Normalbetrieb – und damit auch im Container – gibt es ihn nicht. **Zwei getrennte Ports:** `8080` ist der Port dieser Anwendung (REST-API und, mit Profil `mock`, der Mock), -`17654` der Port der SwyxTray-App auf 192.168.180.135. Sie sind unabhängig voneinander. +`17654` der Port der SwyxTray-App auf dem Arbeitsplatz des Anwenders. Sie sind unabhängig voneinander. ## Protokoll der SwyxTray-App @@ -488,14 +488,14 @@ Es gilt die erste Quelle, die etwas liefert: | 1 | Eingabefeld auf der Startseite | zur Laufzeit änderbar | | 2 | `GET /api/config` vom Backend | `app.websocket.host` in `application.properties` | | 3 | `VITE_WS_URL` aus `frontend/.env` | siehe [.env.example](frontend/.env.example) | -| 4 | Default im Code | `ws://192.168.180.135:17654/ws` | +| 4 | Default im Code | `ws://127.0.0.1:17654/ws` | Backend-seitig: ```properties server.port=8080 # Port dieser Anwendung -app.websocket.host=192.168.180.135 # SwyxTray-App +app.websocket.host=127.0.0.1 # SwyxTray-App app.websocket.port=17654 app.websocket.path=/ws app.websocket.secure=false # true ⇒ wss:// @@ -522,7 +522,7 @@ als `root`, mit einem Healthcheck gegen `/api/config`. ```bash docker build -t swyxweb . docker run -d -p 8080:8080 \ - -e APP_WEBSOCKET_HOST=192.168.180.135 \ + -e APP_WEBSOCKET_HOST=127.0.0.1 \ -e APP_WEBSOCKET_PORT=17654 \ --name swyxweb swyxweb ``` @@ -571,7 +571,9 @@ Zwei Dinge, die der Container nicht lösen kann: Vite-Dev-Server auf Port 5173 sich verbinden kann. Für die Produktion einschränken – siehe [WebSocketConfig.java](backend/src/main/java/de/appcreation/swyxweb/config/WebSocketConfig.java) und [CorsConfig.java](backend/src/main/java/de/appcreation/swyxweb/config/CorsConfig.java). -- **Firewall**: Port 17654 muss auf 192.168.180.135 eingehend freigegeben sein, sonst - scheitert der Verbindungsaufbau ohne aussagekräftige Browser-Fehlermeldung. +- **Firewall**: Die Verbindung geht per Vorgabe an `127.0.0.1`, bleibt also auf dem Rechner + und braucht keine Freigabe. Zeigt die Adresse auf einen anderen Rechner, muss dort Port + 17654 eingehend freigegeben sein – sonst scheitert der Verbindungsaufbau ohne + aussagekräftige Browser-Fehlermeldung. - **System-Benachrichtigungen** verlangen eine Benutzerinteraktion zur Erlaubniserteilung und funktionieren nur über `https://` oder `localhost`. diff --git a/backend/src/main/java/de/appcreation/swyxweb/config/WebSocketProperties.java b/backend/src/main/java/de/appcreation/swyxweb/config/WebSocketProperties.java index 1d74249..d07cbb3 100644 --- a/backend/src/main/java/de/appcreation/swyxweb/config/WebSocketProperties.java +++ b/backend/src/main/java/de/appcreation/swyxweb/config/WebSocketProperties.java @@ -8,7 +8,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "app.websocket") public record WebSocketProperties(String host, int port, String path, boolean secure) { - /** Fertige URL, z. B. ws://192.168.180.135:8080/ws */ + /** Fertige URL, z. B. ws://127.0.0.1:8080/ws */ public String url() { return (secure ? "wss" : "ws") + "://" + host + ":" + port + path; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 5188c2d..51a5a0f 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -13,7 +13,7 @@ server.port=8080 # Adresse des WebSocket-Servers, die das Frontend über /api/config als # Verbindungsziel bekommt. Unabhängig vom Port dieser Anwendung. -app.websocket.host=192.168.180.135 +app.websocket.host=127.0.0.1 app.websocket.port=17654 app.websocket.path=/ws app.websocket.secure=false diff --git a/frontend/.env.example b/frontend/.env.example index 589400a..f3e6d0a 100644 --- a/frontend/.env.example +++ b/frontend/.env.example @@ -1,5 +1,5 @@ # Adresse des WebSocket-Servers; überschreibt den Wert aus /api/config. -VITE_WS_URL=ws://192.168.180.135:17654/ws +VITE_WS_URL=ws://127.0.0.1:17654/ws # Ziel des Dev-Proxys für /api, also der Port dieser Anwendung # (Standard: http://localhost:8080) diff --git a/frontend/src/config.ts b/frontend/src/config.ts index 065b2ab..d460ec6 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,5 +1,5 @@ /** Fallback-Adresse des WebSocket-Servers, falls weder .env noch /api/config etwas liefern. */ -export const DEFAULT_WS_HOST = '192.168.180.135' +export const DEFAULT_WS_HOST = '127.0.0.1' export const DEFAULT_WS_PORT = 17654 export const DEFAULT_WS_PATH = '/ws' diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx index 22eb094..06d0a5c 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/HomePage.tsx @@ -210,7 +210,7 @@ export default function HomePage() { value={url} spellCheck={false} autoComplete="off" - placeholder="ws://192.168.180.135:17654/ws" + placeholder="ws://127.0.0.1:17654/ws" onChange={(e) => { urlTouched.current = true setUrl(e.target.value) diff --git a/frontend/src/swyx/SwyxTrayClient.ts b/frontend/src/swyx/SwyxTrayClient.ts index f4b9d83..d78aa2d 100644 --- a/frontend/src/swyx/SwyxTrayClient.ts +++ b/frontend/src/swyx/SwyxTrayClient.ts @@ -41,6 +41,48 @@ const DEFAULT_TIMEOUT_MS = 10000 // selbst fünf Sekunden, bevor sie mit einem Fehler antwortet. const TAB_TIMEOUT_MS = 15000 +/** Loopback gilt dem Browser als vertrauenswürdig – dorthin ist `ws://` auch von HTTPS aus erlaubt. */ +function isLoopback(hostname: string): boolean { + const host = hostname.replace(/^\[|\]$/g, '').toLowerCase() + return ( + host === 'localhost' || + host.endsWith('.localhost') || + host === '::1' || + /^127\./.test(host) + ) +} + +/** + * Eine über HTTPS ausgelieferte Seite darf keine unverschlüsselte `ws://`-Verbindung + * öffnen. Der Browser wirft dafür nur ein nichtssagendes SecurityError + * ("The operation is insecure."), deshalb wird der Grund hier selbst benannt. + */ +export function describeConnectFailure(url: string, message: string): string { + let target: URL | null = null + try { + target = new URL(url) + } catch { + // Wirklich kaputte Adresse – dann bleibt es bei der Browser-Meldung. + } + + if ( + target && + target.protocol === 'ws:' && + typeof window !== 'undefined' && + window.location.protocol === 'https:' && + !isLoopback(target.hostname) + ) { + return ( + `Diese Seite läuft über HTTPS; der Browser blockiert deshalb die unverschlüsselte ` + + `Verbindung zu ${url} (Mixed Content). Die SwyxTray-App muss dafür selbst TLS ` + + `anbieten (wss://). Läuft sie auf demselben Rechner wie der Browser, ist ` + + `ws://127.0.0.1:${target.port || '17654'}${target.pathname} erlaubt.` + ) + } + + return `Ungültige WebSocket-Adresse: ${message}` +} + /** * WebSocket-Client für die SwyxTray-App. Die Verbindung wird im Browser * aufgebaut; Quittungen werden über die numerische `id` zugeordnet, @@ -235,7 +277,7 @@ export class SwyxTrayClient { socket = new WebSocket(url) } catch (e) { const message = e instanceof Error ? e.message : String(e) - this.emit('error', `Ungültige WebSocket-Adresse: ${message}`) + this.emit('error', describeConnectFailure(url, message)) this.emit('status', 'closed') return }