import Foundation import CryptoKit final class Client { init() {} func send(_ message: Message, to url: String, port: UInt16, through route: TransmissionType, using keys: KeySet) async -> ServerResponse { let sentTime = Date.now let signedMessage = message.authenticate(using: keys.remote) let response: Message switch route { case .throughServer: response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device) case .overLocalWifi: response = await send(signedMessage, toLocalDevice: url, port: port, verifyUsing: keys.device) } let receivedTime = Date.now // Create best guess for creation of challenge. let roundTripTime = receivedTime.timeIntervalSince(sentTime) let serverChallenge = ServerChallenge( creationDate: sentTime.addingTimeInterval(roundTripTime / 2), message: response) // Validate message content guard response.result == .messageAccepted else { print("Failure: \(response)") return (response, nil) } guard response.clientChallenge == message.clientChallenge else { print("Invalid client challenge: \(response)") return (response.with(result: .invalidClientChallengeFromDevice), nil) } return (response, serverChallenge) } private func send(_ message: SignedMessage, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message { let client = UDPClient(host: host, port: port) let response: Data? = await withCheckedContinuation { continuation in client.begin() client.send(message: message.encoded) { res in continuation.resume(returning: res) } } guard let data = response else { return message.message.with(result: .deviceNotConnected) } return decode(data, inResponseTo: message.message, verifyUsing: deviceKey) } private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message { guard let url = URL(string: server)?.appendingPathComponent(SesameRoute.postMessage.rawValue) else { return message.message.with(result: .serverUrlInvalid) } var request = URLRequest(url: url) request.httpBody = message.encoded request.httpMethod = "POST" request.timeoutInterval = 10 request.addValue(authToken.hexEncoded, forHTTPHeaderField: SesameHeader.authenticationHeader) return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey) } private func perform(_ request: URLRequest, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) async -> Message { let (response, responseData) = await fulfill(request) guard response == .messageAccepted, let data = responseData else { return message.with(result: response) } return decode(data, inResponseTo: message, verifyUsing: deviceKey) } private func decode(_ data: Data, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) -> Message { guard data.count == SignedMessage.size else { print("[WARN] Received message with \(data.count) bytes (\(Array(data)))") return message.with(result: .invalidMessageSizeFromDevice) } let decodedMessage: SignedMessage do { decodedMessage = try SignedMessage(decodeFrom: data) } catch { return message.with(result: error as! MessageResult) } guard decodedMessage.isValid(using: deviceKey) else { return message.with(result: .invalidSignatureFromDevice) } return decodedMessage.message } private func fulfill(_ request: URLRequest) async -> (response: MessageResult, data: Data?) { do { let (data, response) = try await URLSession.shared.data(for: request) guard let code = (response as? HTTPURLResponse)?.statusCode else { return (.unexpectedUrlResponseType, nil) } return (.init(httpCode: code), data) } catch { print("Request failed: \(error)") return (.deviceTimedOut, nil) } } }