Loopback als Vorgabe, verständliche Meldung bei Mixed Content
Die feste Adresse 192.168.180.135 der SwyxTray-App wird überall durch 127.0.0.1 ersetzt: Die App läuft auf dem Arbeitsplatz des Anwenders, die Verbindung bleibt damit auf dem Rechner und braucht keine Firewall-Freigabe. Scheitert der Verbindungsaufbau, weil eine über HTTPS ausgelieferte Seite kein unverschlüsseltes ws:// öffnen darf, meldet der Browser nur "The operation is insecure." – describeConnectFailure() benennt stattdessen den Grund und den Ausweg (wss:// oder Loopback). node_modules/ wird nun auf jeder Ebene ignoriert, nicht nur unter frontend/. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -3,7 +3,7 @@ backend/target/
|
|||||||
!backend/.mvn/wrapper/maven-wrapper.properties
|
!backend/.mvn/wrapper/maven-wrapper.properties
|
||||||
|
|
||||||
# Frontend
|
# Frontend
|
||||||
frontend/node_modules/
|
node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/.env
|
frontend/.env
|
||||||
frontend/.env.local
|
frontend/.env.local
|
||||||
|
|||||||
Vendored
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
{
|
{
|
||||||
// Wie oben, aber der eingebaute SwyxTray-Mock wird über das Profil "mock"
|
// Wie oben, aber der eingebaute SwyxTray-Mock wird über das Profil "mock"
|
||||||
// eingeschaltet (ohne das Profil gibt es ihn nicht) und /api/config zeigt
|
// 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:
|
// ohne echte Telefonanlage ausprobieren (eingehenden Anruf auslösen:
|
||||||
// siehe README).
|
// siehe README).
|
||||||
"type": "java",
|
"type": "java",
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ USER swyx
|
|||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
# Adresse der SwyxTray-App zur Laufzeit überschreibbar (Spring Relaxed Binding):
|
# 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
|
# -e APP_WEBSOCKET_PATH=/ws -e APP_WEBSOCKET_SECURE=false
|
||||||
#
|
#
|
||||||
# SPRING_PROFILES_ACTIVE bleibt bewusst leer – der SwyxTray-Mock bliebe sonst
|
# SPRING_PROFILES_ACTIVE bleibt bewusst leer – der SwyxTray-Mock bliebe sonst
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# SwyxWeb
|
# SwyxWeb
|
||||||
|
|
||||||
React-Frontend mit Spring-Boot-Backend. Die Startseite verbindet sich **aus dem Browser heraus**
|
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.
|
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.
|
`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),
|
**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
|
## Protokoll der SwyxTray-App
|
||||||
|
|
||||||
@@ -488,14 +488,14 @@ Es gilt die erste Quelle, die etwas liefert:
|
|||||||
| 1 | Eingabefeld auf der Startseite | zur Laufzeit änderbar |
|
| 1 | Eingabefeld auf der Startseite | zur Laufzeit änderbar |
|
||||||
| 2 | `GET /api/config` vom Backend | `app.websocket.host` in `application.properties` |
|
| 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) |
|
| 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:
|
Backend-seitig:
|
||||||
|
|
||||||
```properties
|
```properties
|
||||||
server.port=8080 # Port dieser Anwendung
|
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.port=17654
|
||||||
app.websocket.path=/ws
|
app.websocket.path=/ws
|
||||||
app.websocket.secure=false # true ⇒ wss://
|
app.websocket.secure=false # true ⇒ wss://
|
||||||
@@ -522,7 +522,7 @@ als `root`, mit einem Healthcheck gegen `/api/config`.
|
|||||||
```bash
|
```bash
|
||||||
docker build -t swyxweb .
|
docker build -t swyxweb .
|
||||||
docker run -d -p 8080:8080 \
|
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 \
|
-e APP_WEBSOCKET_PORT=17654 \
|
||||||
--name swyxweb swyxweb
|
--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
|
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)
|
[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).
|
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
|
- **Firewall**: Die Verbindung geht per Vorgabe an `127.0.0.1`, bleibt also auf dem Rechner
|
||||||
scheitert der Verbindungsaufbau ohne aussagekräftige Browser-Fehlermeldung.
|
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
|
- **System-Benachrichtigungen** verlangen eine Benutzerinteraktion zur Erlaubniserteilung und
|
||||||
funktionieren nur über `https://` oder `localhost`.
|
funktionieren nur über `https://` oder `localhost`.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
@ConfigurationProperties(prefix = "app.websocket")
|
@ConfigurationProperties(prefix = "app.websocket")
|
||||||
public record WebSocketProperties(String host, int port, String path, boolean secure) {
|
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() {
|
public String url() {
|
||||||
return (secure ? "wss" : "ws") + "://" + host + ":" + port + path;
|
return (secure ? "wss" : "ws") + "://" + host + ":" + port + path;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ server.port=8080
|
|||||||
|
|
||||||
# Adresse des WebSocket-Servers, die das Frontend über /api/config als
|
# Adresse des WebSocket-Servers, die das Frontend über /api/config als
|
||||||
# Verbindungsziel bekommt. Unabhängig vom Port dieser Anwendung.
|
# 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.port=17654
|
||||||
app.websocket.path=/ws
|
app.websocket.path=/ws
|
||||||
app.websocket.secure=false
|
app.websocket.secure=false
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Adresse des WebSocket-Servers; überschreibt den Wert aus /api/config.
|
# 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
|
# Ziel des Dev-Proxys für /api, also der Port dieser Anwendung
|
||||||
# (Standard: http://localhost:8080)
|
# (Standard: http://localhost:8080)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/** Fallback-Adresse des WebSocket-Servers, falls weder .env noch /api/config etwas liefern. */
|
/** 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_PORT = 17654
|
||||||
export const DEFAULT_WS_PATH = '/ws'
|
export const DEFAULT_WS_PATH = '/ws'
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ export default function HomePage() {
|
|||||||
value={url}
|
value={url}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
placeholder="ws://192.168.180.135:17654/ws"
|
placeholder="ws://127.0.0.1:17654/ws"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
urlTouched.current = true
|
urlTouched.current = true
|
||||||
setUrl(e.target.value)
|
setUrl(e.target.value)
|
||||||
|
|||||||
@@ -41,6 +41,48 @@ const DEFAULT_TIMEOUT_MS = 10000
|
|||||||
// selbst fünf Sekunden, bevor sie mit einem Fehler antwortet.
|
// selbst fünf Sekunden, bevor sie mit einem Fehler antwortet.
|
||||||
const TAB_TIMEOUT_MS = 15000
|
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
|
* WebSocket-Client für die SwyxTray-App. Die Verbindung wird im Browser
|
||||||
* aufgebaut; Quittungen werden über die numerische `id` zugeordnet,
|
* aufgebaut; Quittungen werden über die numerische `id` zugeordnet,
|
||||||
@@ -235,7 +277,7 @@ export class SwyxTrayClient {
|
|||||||
socket = new WebSocket(url)
|
socket = new WebSocket(url)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = e instanceof Error ? e.message : String(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')
|
this.emit('status', 'closed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user