- VoiceSession: token->connect with fresh-room retry on timeout, agent discovery, agent_ready wait, mic enable/mute, text send over lk.chat - ControlChannel: outbound set_voice/set_model/get_models/set_mode/ media_state; inbound agent_ready/models/model_changed/task_event - TranscriptHandler: transcription segments + transcript topic finals, partial/final dedupe per role - ConversationView/OrbView: status orb, mic/text-mode toggles, composer - ServerConfig: plain ws:// signaling for private IPs (self-signed TLS not injectable into SDK WebSocket); Info.plist local networking
54 lines
2.2 KiB
Swift
54 lines
2.2 KiB
Swift
import Foundation
|
|
import CommonCrypto
|
|
import Security
|
|
|
|
/// Pins the self-signed cert of the LAN deployment (CN=192.168.86.2, regenerated
|
|
/// at container start — re-export the hash after server cert rotation, see README).
|
|
/// Extract with:
|
|
/// openssl s_client -connect 192.168.86.2:9178 </dev/null 2>/dev/null \
|
|
/// | openssl x509 -outform der | openssl dgst -sha256
|
|
enum TLSPinning {
|
|
static let pinnedCertSHA256Hex: Set<String> = [
|
|
"07907abde3bb9c685265909927627d9ea4705d7e7b27056eefe06e10b192af46"
|
|
]
|
|
|
|
static let host = "192.168.86.2"
|
|
|
|
static func session() -> URLSession {
|
|
let config = URLSessionConfiguration.default
|
|
config.timeoutIntervalForRequest = 15
|
|
return URLSession(configuration: config, delegate: PinnedTrustDelegate(), delegateQueue: nil)
|
|
}
|
|
|
|
static func sha256Hex(_ data: Data) -> String {
|
|
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
data.withUnsafeBytes { buffer in
|
|
guard let base = buffer.baseAddress else { return }
|
|
CC_SHA256(base, CC_LONG(data.count), &hash)
|
|
}
|
|
return hash.map { String(format: "%02x", $0) }.joined()
|
|
}
|
|
|
|
static func matchesPinnedCert(_ trust: SecTrust) -> Bool {
|
|
guard let chain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
|
|
let leaf = chain.first,
|
|
let der = SecCertificateCopyData(leaf) as Data? else { return false }
|
|
return pinnedCertSHA256Hex.contains(sha256Hex(der))
|
|
}
|
|
}
|
|
|
|
final class PinnedTrustDelegate: NSObject, URLSessionDelegate {
|
|
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
|
|
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
let trust = challenge.protectionSpace.serverTrust else {
|
|
completionHandler(.performDefaultHandling, nil)
|
|
return
|
|
}
|
|
if TLSPinning.matchesPinnedCert(trust) {
|
|
completionHandler(.useCredential, URLCredential(trust: trust))
|
|
} else {
|
|
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
}
|
|
}
|
|
}
|