176 lines
5.9 KiB
Swift
176 lines
5.9 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
import SwiftData
|
|
|
|
final class RequestCoordinator: ObservableObject {
|
|
|
|
@Published
|
|
var state: MessageResult = .noKeyAvailable
|
|
|
|
@Published
|
|
var pendingRequests: [PendingOperation] = []
|
|
|
|
@Published
|
|
var activeRequest: PendingOperation?
|
|
|
|
@Published
|
|
var keyManager = KeyManagement()
|
|
|
|
@Published
|
|
var isPerformingRequest: Bool = false
|
|
|
|
@AppStorage("server")
|
|
var serverPath: String = "https://christophhagen.de/sesame/"
|
|
|
|
@AppStorage("localIP")
|
|
var localAddress: String = "192.168.178.104/"
|
|
|
|
@AppStorage("connectionType")
|
|
var connectionType: ConnectionStrategy = .remoteFirst
|
|
|
|
private let modelContext: ModelContext
|
|
|
|
private let client = Client()
|
|
|
|
private var timer: Timer?
|
|
|
|
init(modelContext: ModelContext) {
|
|
self.modelContext = modelContext
|
|
if keyManager.hasAllKeys {
|
|
self.state = .notChecked
|
|
}
|
|
}
|
|
|
|
func startUnlock() {
|
|
guard !isPerformingRequest else {
|
|
return
|
|
}
|
|
isPerformingRequest = true
|
|
Task {
|
|
let finalResult = await performFullChallengeResponse()
|
|
DispatchQueue.main.async {
|
|
self.state = finalResult
|
|
self.isPerformingRequest = false
|
|
}
|
|
scheduleReturnToReadyState()
|
|
}
|
|
}
|
|
|
|
private func performFullChallengeResponse() async -> MessageResult {
|
|
let transmissionTypes = connectionType.transmissionTypes
|
|
for route in transmissionTypes.dropLast() {
|
|
if await performUnlockAndSaveItem(route: route) == .unlocked {
|
|
return .unlocked
|
|
}
|
|
}
|
|
guard let route = transmissionTypes.last else {
|
|
// No transmission types at all
|
|
return keyManager.hasAllKeys ? .notChecked : .noKeyAvailable
|
|
}
|
|
return await performUnlockAndSaveItem(route: route)
|
|
}
|
|
|
|
private func performUnlockAndSaveItem(route: TransmissionType) async -> MessageResult {
|
|
let startTime = Date.now
|
|
let result = await performFullChallengeResponse(route: route)
|
|
let endTime = Date.now
|
|
let roundTripTime = endTime.timeIntervalSince(startTime)
|
|
print("Unlock took \(Int(roundTripTime * 1000)) ms (\(result.result))")
|
|
let item = HistoryItem(message: result, startDate: startTime, route: route, finishDate: endTime)
|
|
modelContext.insert(item)
|
|
return result.result
|
|
}
|
|
|
|
private func performFullChallengeResponse(route: TransmissionType) async -> Message {
|
|
let (challengeResponse, challenge) = await performChallenge(route: route)
|
|
guard let challenge else {
|
|
return challengeResponse
|
|
}
|
|
|
|
let (unlockResponse, secondaryChallenge) = await performUnlock(with: challenge.message, route: route)
|
|
guard let secondaryChallenge else {
|
|
return unlockResponse
|
|
}
|
|
let (secondUnlockResponse, _) = await performUnlock(with: secondaryChallenge.message, route: route)
|
|
return secondUnlockResponse
|
|
}
|
|
|
|
private func performChallenge(route: TransmissionType) async -> ServerResponse {
|
|
let initialMessage = Message.initial()
|
|
let (result, challenge) = await send(initialMessage, route: route)
|
|
guard let message = challenge?.message else {
|
|
return (result, nil)
|
|
}
|
|
// Can't get here without the message being accepted
|
|
guard message.messageType == .challenge else {
|
|
print("Invalid message type for challenge: \(message)")
|
|
return (result.with(result: .invalidMessageTypeFromDevice), nil)
|
|
}
|
|
return (result.with(result: .deviceAvailable), challenge)
|
|
}
|
|
|
|
private func performUnlock(with challenge: Message, route: TransmissionType) async -> ServerResponse {
|
|
let request = challenge.requestMessage()
|
|
let (unlockState, responseData) = await send(request, route: route)
|
|
|
|
guard let response = responseData?.message else {
|
|
return (unlockState, nil)
|
|
}
|
|
switch response.messageType {
|
|
case .initial, .request:
|
|
print("Invalid message type for response: \(response)")
|
|
return (response.with(result: .invalidMessageTypeFromDevice), nil)
|
|
case .challenge:
|
|
// New challenge received, challenge was expired
|
|
return (unlockState, responseData)
|
|
case .response:
|
|
break
|
|
}
|
|
|
|
guard response.serverChallenge == request.serverChallenge else {
|
|
print("Invalid server challenge for unlock: \(response)")
|
|
return (response.with(result: .invalidServerChallengeFromDevice), nil)
|
|
}
|
|
return (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 resetState() {
|
|
let hasKeys = keyManager.hasAllKeys
|
|
DispatchQueue.main.async {
|
|
self.state = hasKeys ? .notChecked : .noKeyAvailable
|
|
}
|
|
}
|
|
|
|
func scheduleReturnToReadyState() {
|
|
timer?.invalidate()
|
|
|
|
DispatchQueue.main.async {
|
|
self.timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { [weak self] timer in
|
|
defer { timer.invalidate() }
|
|
guard let self else {
|
|
return
|
|
}
|
|
self.resetState()
|
|
self.timer = nil
|
|
}
|
|
}
|
|
}
|
|
}
|