3629dbdaaa
The app shipped with certificate validation bypassed globally and on by default — InsecureTrustDelegate trusted any cert from any host. That was the evaluation's P1: anyone on the LAN could MITM call audio, transcripts, and voiceprints. The backend's Start9 cert already validates under normal system trust when the StartOS Root CA is installed in the keychain (confirmed: URLSession default validation returns 200 against the backend and its fallback), so the bypass is unnecessary: - skip-TLS now defaults to off - when explicitly enabled, the bypass is scoped to the configured host via InsecureTrustDelegate.allowsTrustOverride, never "trust any server" - the host gate is pure and unit-tested (InsecureTrustDelegateTests) Docs reconciled: AGENTS.md backend/TLS line and Current state.
69 lines
2.4 KiB
Swift
69 lines
2.4 KiB
Swift
import Foundation
|
|
import Combine
|
|
|
|
/// Performs the Phase 0 backend reachability check: `GET {baseURL}/api/status`.
|
|
///
|
|
/// This is a thin slice — the full `SparkControlClient` (label-merge, multipart,
|
|
/// sequential queueing, retries) arrives in Phase 5.
|
|
@MainActor
|
|
final class SparkControlHealth: ObservableObject {
|
|
|
|
enum Status: Equatable {
|
|
case unknown
|
|
case checking
|
|
case online(String)
|
|
case offline(String)
|
|
}
|
|
|
|
@Published private(set) var status: Status = .unknown
|
|
@Published private(set) var lastChecked: Date?
|
|
|
|
func check(baseURL: String, skipTLS: Bool) async {
|
|
status = .checking
|
|
|
|
let trimmed = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
let base = trimmed.hasSuffix("/") ? String(trimmed.dropLast()) : trimmed
|
|
guard !base.isEmpty, let url = URL(string: base + "/api/status") else {
|
|
status = .offline("Invalid host URL")
|
|
return
|
|
}
|
|
|
|
let config = URLSessionConfiguration.ephemeral
|
|
config.timeoutIntervalForRequest = 8
|
|
config.waitsForConnectivity = false
|
|
|
|
let delegate: URLSessionDelegate? = skipTLS
|
|
? InsecureTrustDelegate(allowedHost: url.host)
|
|
: nil
|
|
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
|
|
defer { session.finishTasksAndInvalidate() }
|
|
|
|
do {
|
|
let (data, response) = try await session.data(from: url)
|
|
lastChecked = Date()
|
|
guard let http = response as? HTTPURLResponse else {
|
|
status = .offline("No HTTP response")
|
|
return
|
|
}
|
|
if (200..<300).contains(http.statusCode) {
|
|
status = .online(Self.summarize(data) ?? "Reachable")
|
|
} else {
|
|
status = .offline("HTTP \(http.statusCode)")
|
|
}
|
|
} catch {
|
|
lastChecked = Date()
|
|
status = .offline(error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
/// Best-effort one-line summary of the `/api/status` body, if it's JSON.
|
|
private static func summarize(_ data: Data) -> String? {
|
|
guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
|
|
return nil
|
|
}
|
|
if let s = object["status"] as? String { return s }
|
|
if let s = object["state"] as? String { return s }
|
|
return "Reachable"
|
|
}
|
|
}
|