fix(ui): launch apps on the page's scheme over HTTPS

New-tab apps and the companion WebView got hardcoded http:// URLs, so a
node reached over HTTPS opened Vaultwarden, BTCPay, Grafana et al in
cleartext. Every app port is gate-owned and serves TLS on the same port
(appgate/tls.rs), so directAppUrl(), the legacy open() path, and
resolveRuntimeLaunchUrl() now follow the page's scheme. HTTP pages (the
kiosk, LAN) are unchanged; netbird keeps its unconditional https.
This commit is contained in:
archipelago
2026-08-31 17:08:26 -04:00
parent 3089624969
commit 7c0a492c43
5 changed files with 159 additions and 45 deletions
@@ -166,6 +166,49 @@ function pageScheme(): string {
return p === 'https:' || p === 'http:' ? p : 'http:'
}
/** Apps served over HTTPS (self-signed) rather than plain HTTP, regardless of
* the page's scheme. */
export const HTTPS_APP_IDS = new Set(['netbird'])
/** App ID -> direct launch port for the paths that bypass the in-app session:
* new-tab apps and the companion's native WebView. Every port here is owned
* by the app gate (manifest `auth: gated`/`open` + `bind: 127.0.0.1`), which
* serves TLS on the same port whenever the node has a certificate. */
export const DIRECT_APP_PORTS: Record<string, string> = {
'btcpay-server': '23000',
grafana: '3000',
photoprism: '2342',
homeassistant: '8123',
vaultwarden: '8082',
nextcloud: '8085',
portainer: '9000',
tailscale: '8240',
'nginx-proxy-manager': '8081',
'uptime-kuma': '3002',
gitea: '3001',
// Without this, directAppUrl('netbird') returns null and netbird falls
// through to the iframe (and never gets its https URL) — issue #15.
netbird: '8087',
}
/** Direct-port launch URL for an app, on the page's scheme.
*
* These are the apps that open OUTSIDE the dashboard's own origin — a new
* browser tab on the desktop, or the companion's in-app WebView on a phone.
* The URL is handed to a context with no dashboard chrome, so it must carry
* the scheme the remote browser actually reached the node on: on an HTTPS
* connection, `http://host:port` is at best a silent downgrade to cleartext
* and at worst blocked outright as mixed content. Every port in
* DIRECT_APP_PORTS is served by the app gate with TLS on the same port
* (see appgate/tls.rs), so following the page scheme is always answerable.
* Plain-HTTP dashboards keep today's behaviour exactly. */
export function directAppUrl(appId: string): string | null {
const port = DIRECT_APP_PORTS[appId]
if (!port || typeof window === 'undefined') return null
const scheme = HTTPS_APP_IDS.has(appId) || pageScheme() === 'https:' ? 'https' : 'http'
return `${scheme}://${window.location.hostname}:${port}`
}
/** Resolve a human-readable title for an app */
export function resolveAppTitle(id: string): string {
return APP_TITLES[id] || id.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())