import Foundation import SwiftUI import SwiftData final class RequestCoordinator: ObservableObject { @Published var serverChallenge: ServerChallenge? = nil @Published var state: MessageResult = .noKeyAvailable @Published private var timer: Timer? @Published var pendingRequests: [PendingOperation] = [] @Published var activeRequest: PendingOperation? @Published var keyManager = KeyManagement() @AppStorage("server") var serverPath: String = "https://christophhagen.de/sesame/" @AppStorage("localIP") var localAddress: String = "192.168.178.104/" @AppStorage("connectionType") var connectionType: ConnectionStrategy = .remoteFirst let modelContext: ModelContext init(modelContext: ModelContext) { self.modelContext = modelContext if keyManager.hasAllKeys { self.state = .notChecked } } let client = Client() var needsNewServerChallenge: Bool { serverChallenge?.isExpired ?? true } @Published var isPerformingRequest: Bool = false func startUnlock() { addOperations(.challenge, .unlock) } func startChallenge() { addOperations(.challenge) } private func addOperations(_ operations: RequestType...) { #warning("Only perform challenge when doing unlock? Remove code complexity") // Just add all operations for an unlock // For every completed operation, the unnecessary ones will be removed without executing them let operations = connectionType.transmissionTypes.map { route in operations.map { PendingOperation(route: route, operation: $0) } }.joined() pendingRequests.append(contentsOf: operations) continueRequests() } private func continueRequests() { guard activeRequest == nil else { return } guard !pendingRequests.isEmpty else { self.isPerformingRequest = false return } let activeRequest = pendingRequests.removeFirst() self.activeRequest = activeRequest self.isPerformingRequest = true Task { await process(request: activeRequest) } } private func process(request: PendingOperation) async { let startTime = Date.now let (success, response, challenge) = await self.start(request) let endTime = Date.now let roundTripTime = endTime.timeIntervalSince(startTime) if let s = challenge?.message { print("\(s) took \(Int(roundTripTime * 1000)) ms") } else { print("\(request.operation.description) took \(Int(roundTripTime * 1000)) ms") } if request.operation == .unlock, let response { print("Saving history item") let item = HistoryItem(message: response, startDate: startTime, route: request.route, finishDate: endTime) modelContext.insert(item) } DispatchQueue.main.async { self.filterPendingRequests(after: request, success: success, hasChallenge: challenge != nil) if let response { self.state = response.result } if let challenge { self.serverChallenge = challenge } self.activeRequest = nil self.continueRequests() } } private func filterPendingRequests(after operation: PendingOperation, success: Bool, hasChallenge: Bool) { if success { // Filter all unlocks if operation.operation == .unlock { // Successful unlock means no need for next challenge or unlocks, so remove all self.pendingRequests = [] } else { // Successful challenge means no need for additional challenges, but keep unlocks self.pendingRequests = pendingRequests.filter { $0.operation != .challenge } } } else { // Filter all operations with the same route for connection errors // And with type, depending on error? } } private func start(_ operation: PendingOperation) async -> OptionalServerResponse { switch operation.operation { case .challenge: if let serverChallenge, !serverChallenge.isExpired { return (true, serverChallenge.message, serverChallenge) } return await performChallenge(route: operation.route) case .unlock: guard let serverChallenge, !serverChallenge.isExpired else { return (false, nil, nil) } return await performUnlock(with: serverChallenge.message, route: operation.route) } } private func performChallenge(route: TransmissionType) async -> OptionalServerResponse { let initialMessage = Message.initial() let (result, challenge) = await send(initialMessage, route: route) guard let message = challenge?.message else { return (false, result, nil) } // Can't get here without the message being accepted guard message.messageType == .challenge else { print("Invalid message type for challenge: \(message)") return (false, result.with(result: .invalidMessageTypeFromDevice), nil) } return (true, result.with(result: .deviceAvailable), challenge) } private func performUnlock(with challenge: Message, route: TransmissionType) async -> OptionalServerResponse { let request = challenge.requestMessage() let (unlockState, responseData) = await send(request, route: route) guard let response = responseData?.message else { return (false, unlockState, nil) } switch response.messageType { case .initial, .request: print("Invalid message type for response: \(response)") return (false, response.with(result: .invalidMessageTypeFromDevice), nil) case .challenge: // New challenge received, challenge was expired return (true, unlockState, responseData) case .response: break } guard response.serverChallenge == request.serverChallenge else { print("Invalid server challenge for unlock: \(response)") return (false, response.with(result: .invalidServerChallengeFromDevice), nil) } return (true, response.with(result: .unlocked), nil) } private func url(for route: TransmissionType) -> String { switch route { case .throughServer: return serverPath case .overLocalWifi: return localAddress } } private func send(_ message: Message, route: TransmissionType) async -> ServerResponse { guard let keys = keyManager.getAllKeys() else { return (message.with(result: .noKeyAvailable), nil) } let url = url(for: route) return await client.send(message, to: url, through: route, using: keys) } func startUpdatingServerChallenge() { guard timer == nil else { return } DispatchQueue.main.async { self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] timer in guard let self else { timer.invalidate() return } DispatchQueue.main.async { self.startChallenge() } } self.timer!.fire() } } func endUpdatingServerChallenge() { timer?.invalidate() timer = nil } }