Server-Adresse: das Schema richtet sich nach dem Ziel

Beim Neuverbinden stand "localhost:5173" im Feld, die App meldete "nicht
erreichbar". Ursache war basisUrlNormalisieren(): ohne Schema setzte es
blind https:// davor - womit die eigene Vorlage der App unbrauchbar war,
denn im Feld steht als Beispiel 192.168.1.20:8123, und im Heimnetz gibt
es kein Zertifikat.

Jetzt nach dem Ziel: localhost, 127.x, 10.x, 192.168.x, 172.16-31.x, ::1
und .local/.home.arpa/.localhost bekommen http, alles andere weiter https
(datametric360.de laeuft ueber Cloudflare). Steht ein Schema da, wird
nichts geraten.

Vier Regressionstests, darunter der gemeldete Fall, die Vorlage aus dem
Feld und 172.32.0.1 als Gegenprobe zum privaten Bereich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:15:31 +02:00
parent 9652a436e0
commit e237bf1e8b
6 changed files with 119 additions and 4 deletions
+48
View File
@@ -0,0 +1,48 @@
/**
* Das Schema der Server-Adresse.
*
* Anlass: beim Neuverbinden stand „localhost:5173" im Feld — die App machte
* daraus `https://localhost:5173` und meldete „Der Server ist unter dieser
* Adresse nicht erreichbar". Das Beispiel im Feld selbst
* (`192.168.1.20:8123`) hatte dasselbe Problem: im Heimnetz gibt es kein
* Zertifikat, Home Assistant spricht dort `http`.
*/
import { describe, expect, it } from "vitest"
import { basisUrlNormalisieren } from "./umgebung"
describe("basisUrlNormalisieren", () => {
it("nimmt http fuer das eigene Netz", () => {
// Genau die Vorlage aus dem Eingabefeld, und der gemeldete Fall.
expect(basisUrlNormalisieren("192.168.1.20:8123")).toBe("http://192.168.1.20:8123")
expect(basisUrlNormalisieren("localhost:5173")).toBe("http://localhost:5173")
expect(basisUrlNormalisieren("127.0.0.1:8123")).toBe("http://127.0.0.1:8123")
expect(basisUrlNormalisieren("10.0.0.5:8123")).toBe("http://10.0.0.5:8123")
expect(basisUrlNormalisieren("172.16.3.4:8123")).toBe("http://172.16.3.4:8123")
expect(basisUrlNormalisieren("homeassistant.local:8123")).toBe(
"http://homeassistant.local:8123",
)
})
it("bleibt bei https fuer alles von aussen Erreichbare", () => {
// Der echte Betriebsfall: die Domain laeuft ueber Cloudflare.
expect(basisUrlNormalisieren("datametric360.de")).toBe("https://datametric360.de")
expect(basisUrlNormalisieren("ha.example.com:8123")).toBe("https://ha.example.com:8123")
// 172.32 liegt bereits AUSSERHALB des privaten Bereichs (nur 16-31).
expect(basisUrlNormalisieren("172.32.0.1")).toBe("https://172.32.0.1")
})
it("raet gar nicht, wenn ein Schema dasteht", () => {
expect(basisUrlNormalisieren("https://192.168.1.20:8123")).toBe("https://192.168.1.20:8123")
expect(basisUrlNormalisieren("http://datametric360.de")).toBe("http://datametric360.de")
})
it("schneidet abschliessende Schraegstriche ab", () => {
// Sonst entstehen Adressen wie "https://host//api/states".
expect(basisUrlNormalisieren(" https://datametric360.de/// ")).toBe(
"https://datametric360.de",
)
expect(basisUrlNormalisieren("192.168.1.20:8123/")).toBe("http://192.168.1.20:8123")
})
})
+34 -1
View File
@@ -129,9 +129,42 @@ export async function zugangVerwerfen(): Promise<void> {
/** Schneidet abschließende Schrägstriche ab und ergänzt fehlendes Schema.
Ohne das entstehen sonst Adressen wie "https://host//api/states". */
/**
* Adressen, die per Definition im eigenen Netz liegen - dort gibt es kein
* Zertifikat, also spricht der Server http.
*
* Home Assistant im Heimnetz ist genau dieser Fall: "192.168.1.20:8123" steht
* sogar als Beispiel im Eingabefeld. Bis zum 04.09.2026 setzte die App davor
* blind https:// - die eigene Vorlage, so eingetippt, ergab damit eine
* Adresse, unter der nichts antwortet ("Der Server ist unter dieser Adresse
* nicht erreichbar", gemeldet beim Neuverbinden).
*
* Umgekehrt bleibt https richtig fuer alles, was von aussen erreichbar ist -
* datametric360.de laeuft ueber Cloudflare, dort waere http ein Rueckschritt.
* Deshalb nicht generell umgestellt, sondern nach dem Ziel unterschieden.
*/
function imEigenenNetz(host: string): boolean {
const h = host.toLowerCase().replace(/:[0-9]+$/, "").replace(/^\[|\]$/g, "");
if (h === "localhost" || h === "::1") return true;
if (h.endsWith(".local") || h.endsWith(".home.arpa") || h.endsWith(".localhost")) return true;
const v4 = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/.exec(h);
if (!v4) return false;
const a = Number(v4[1]);
const b = Number(v4[2]);
return a === 127 || a === 10 || (a === 192 && b === 168) || (a === 172 && b >= 16 && b <= 31);
}
/** Schneidet abschliessende Schraegstriche ab und ergaenzt fehlendes Schema.
Ohne das entstehen sonst Adressen wie "https://host//api/states".
Geraten wird nur, wenn kein Schema dasteht - wer http:// oder https://
tippt, bekommt genau das. */
export function basisUrlNormalisieren(eingabe: string): string {
let url = eingabe.trim();
if (!/^https?:\/\//i.test(url)) url = `https://${url}`;
if (!/^https?:\/\//i.test(url)) {
const ziel = url.split("/")[0] ?? "";
url = `${imEigenenNetz(ziel) ? "http" : "https"}://${url}`;
}
return url.replace(/\/+$/, "");
}