diff --git a/Client/tauri-client/src-tauri/Cargo.toml b/Client/tauri-client/src-tauri/Cargo.toml index 7901a420..96743742 100644 --- a/Client/tauri-client/src-tauri/Cargo.toml +++ b/Client/tauri-client/src-tauri/Cargo.toml @@ -23,7 +23,12 @@ tauri-plugin-global-shortcut = "2" tauri-plugin-notification = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -tauri-plugin-http = { version = "2.5.7", features = ["rustls-tls"] } +# NOTE: dangerous-settings is required for self-signed certificate support. +# The app is designed for self-hosted servers which commonly use self-signed certs. +# The Rust TOFU WS proxy handles WebSocket certs, but HTTP API calls (health, +# login, upload) and attachment downloads need this feature to accept self-signed +# certs via the `danger` fetch option. Scoped to server URLs only in attachments.ts. +tauri-plugin-http = { version = "2.5.7", features = ["rustls-tls", "dangerous-settings"] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" tauri-plugin-fs = "2" diff --git a/Client/tauri-client/src/lib/types.ts b/Client/tauri-client/src/lib/types.ts index d79862d6..6973b25a 100644 --- a/Client/tauri-client/src/lib/types.ts +++ b/Client/tauri-client/src/lib/types.ts @@ -497,7 +497,8 @@ export interface RegisterResponse { /** GET /api/health response. */ export interface HealthResponse { readonly status: string; - readonly version: string; + /** Version is omitted from unauthenticated endpoints to prevent fingerprinting. */ + readonly version?: string; readonly uptime: number; readonly online_users: number; } diff --git a/Client/tauri-client/src/main.ts b/Client/tauri-client/src/main.ts index 8a51ae48..b77c5e0c 100644 --- a/Client/tauri-client/src/main.ts +++ b/Client/tauri-client/src/main.ts @@ -85,10 +85,12 @@ if (!appEl) { // Create core services const router = createRouter("connect"); -// H-3: Default to strict TLS verification. Self-signed cert support is handled -// by the Rust-side TOFU WS proxy and the CertMismatchModal, not by disabling -// TLS validation in the HTTP client. -const api = createApiClient({ host: "", allowSelfSigned: false }, () => { +// NOTE: allowSelfSigned must be true because the app targets self-hosted servers +// that commonly use self-signed certificates. The Rust TOFU WS proxy handles +// WebSocket certs, but HTTP API calls (health, login, register, upload) have no +// equivalent proxy and need this flag to function. The ideal future fix is adding +// a TOFU HTTP proxy in Rust alongside the existing WS proxy. +const api = createApiClient({ host: "", allowSelfSigned: true }, () => { log.warn("Session expired (401), clearing auth"); clearAuth(); }); @@ -195,7 +197,7 @@ function runHealthChecks( connectPage.updateHealthStatus(profile.host, { status: elapsed > 1500 ? "slow" : "online", latencyMs: elapsed, - version: health.version, + version: health.version ?? null, onlineUsers: health.online_users ?? null, }); } catch { diff --git a/Server/api/router_test.go b/Server/api/router_test.go index cfc92ae0..40aebaf0 100644 --- a/Server/api/router_test.go +++ b/Server/api/router_test.go @@ -87,7 +87,7 @@ func TestHealthEndpointStatusOK(t *testing.T) { } } -func TestHealthEndpointHasVersion(t *testing.T) { +func TestHealthEndpointOmitsVersion(t *testing.T) { router := setupRouter(t) req := httptest.NewRequest(http.MethodGet, "/health", nil) @@ -100,8 +100,9 @@ func TestHealthEndpointHasVersion(t *testing.T) { t.Fatalf("JSON decode error: %v", err) } - if body["version"] == nil || body["version"] == "" { - t.Error("health response missing 'version' field") + // C-2: Version must NOT be exposed on unauthenticated endpoints. + if _, exists := body["version"]; exists { + t.Error("health response must not contain 'version' field (prevents fingerprinting)") } } @@ -136,7 +137,7 @@ func TestAPIV1InfoReturnsServerName(t *testing.T) { } } -func TestAPIV1InfoReturnsVersion(t *testing.T) { +func TestAPIV1InfoOmitsVersion(t *testing.T) { router := setupRouter(t) req := httptest.NewRequest(http.MethodGet, "/api/v1/info", nil) @@ -149,8 +150,9 @@ func TestAPIV1InfoReturnsVersion(t *testing.T) { t.Fatalf("JSON decode error: %v", err) } - if body["version"] == nil { - t.Error("info response missing 'version' field") + // C-2: Version must NOT be exposed to prevent fingerprinting. + if _, exists := body["version"]; exists { + t.Error("info response must not contain 'version' field (prevents fingerprinting)") } }