Local route over UDP

This commit is contained in:
Christoph Hagen
2024-04-22 12:58:49 +02:00
parent 877bba56b4
commit 91af68a44b
7 changed files with 250 additions and 25 deletions

View File

@ -3,13 +3,9 @@ import CryptoKit
final class Client {
private let localRequestRoute = "message"
private let urlMessageParameter = "m"
init() {}
func send(_ message: Message, to url: String, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
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
@ -18,7 +14,7 @@ final class Client {
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
case .overLocalWifi:
response = await send(signedMessage, toLocalDeviceUrl: url, verifyUsing: keys.device)
response = await send(signedMessage, toLocalDevice: url, port: port, verifyUsing: keys.device)
}
let receivedTime = Date.now
// Create best guess for creation of challenge.
@ -39,18 +35,19 @@ final class Client {
}
return (response, serverChallenge)
}
private func send(_ message: SignedMessage, toLocalDeviceUrl server: String, verifyUsing deviceKey: SymmetricKey) async -> Message {
let data = message.encoded.hexEncoded
guard let url = URL(string: server)?.appendingPathComponent("\(localRequestRoute)?\(urlMessageParameter)=\(data)") else {
return message.message.with(result: .serverUrlInvalid)
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)
}
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = 10
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
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 {
@ -71,6 +68,10 @@ final class Client {
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)

View File

@ -25,6 +25,9 @@ final class RequestCoordinator: ObservableObject {
@AppStorage("localIP")
var localAddress: String = "192.168.178.104/"
@AppStorage("localPort")
var localPort: UInt16 = 8888
@AppStorage("connectionType")
var connectionType: ConnectionStrategy = .remoteFirst
@ -171,7 +174,7 @@ final class RequestCoordinator: ObservableObject {
return (message.with(result: .noKeyAvailable), nil)
}
let url = url(for: route)
return await client.send(message, to: url, through: route, using: keys)
return await client.send(message, to: url, port: localPort, through: route, using: keys)
}
func resetState() {
@ -196,3 +199,17 @@ final class RequestCoordinator: ObservableObject {
}
}
}
extension UInt16: RawRepresentable {
public var rawValue: String {
"\(self)"
}
public init?(rawValue: String) {
guard let value = UInt16(rawValue) else {
return nil
}
self = value
}
}

View File

@ -0,0 +1,170 @@
import Foundation
import Network
enum UDPState: String {
case initial
case connectionCreated
case preparingConnection
case sending
case waitingForResponse
}
final class UDPClient {
let host: NWEndpoint.Host
let port: NWEndpoint.Port
private var connection: NWConnection?
private var completion: ((Data?) -> Void)?
private var state: UDPState = .initial
init(host: String, port: UInt16) {
self.host = .init("192.168.188.118")
self.port = .init(rawValue: port)!
}
deinit {
print("Destroying UDP Client")
finish()
}
func begin() {
guard state == .initial else {
print("Invalid state for begin(): \(state)")
return
}
connection = NWConnection(host: host, port: port, using: .udp)
state = .connectionCreated
print("Created connection: \(connection != nil)")
}
func send(message: Data, completion: @escaping (Data?) -> Void) {
print("Sending message to \(host) at port \(port)")
guard state == .connectionCreated else {
print("Invalid state preparing for send: \(state)")
return
}
guard let connection else {
print("Failed to send, no connection")
completion(nil)
return
}
self.completion = completion
connection.stateUpdateHandler = { [weak self] (newState) in
switch (newState) {
case .ready:
print("State: Ready\n")
self?.send(message, over: connection)
case .setup:
print("State: Setup\n")
case .cancelled:
print("Cancelled UDP connection")
self?.finish()
case .preparing:
print("Preparing UDP connection")
case .failed(let error):
print("Failed to start UDP connection: \(error)")
self?.finish()
// default:
// print("ERROR! State not defined!\n")
// self?.finish()
case .waiting(_):
print("Waiting for UDP connection path change")
@unknown default:
print("Unknown UDP connection state: \(newState)")
self?.finish()
}
}
print("Preparing connection")
state = .preparingConnection
connection.start(queue: .global())
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
guard self.state == .preparingConnection else {
return
}
print("Timed out preparing connection")
self.finish()
}
}
private func finish(_ data: Data? = nil) {
completion?(data)
completion = nil
connection?.stateUpdateHandler = nil
connection?.cancel()
connection = nil
state = .initial
}
private func send(_ data: Data, over connection: NWConnection) {
guard state == .preparingConnection else {
print("Invalid state for send: \(state)")
return
}
connection.stateUpdateHandler = nil
let completion = NWConnection.SendCompletion.contentProcessed { [weak self] error in
if let error {
print("Failed to send UDP packet: \(error)")
self?.finish()
} else {
print("Finished sending message")
self?.waitForResponse(over: connection)
}
}
state = .sending
connection.send(content: data, completion: completion)
print("Started to send message")
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
guard self.state == .sending else {
return
}
print("Timed out waiting for for send to complete")
self.finish()
}
}
private func waitForResponse(over connection: NWConnection) {
guard state == .sending else {
print("Invalid state for send: \(state)")
return
}
state = .waitingForResponse
connection.receiveMessage { [weak self] (data, context, isComplete, error) in
guard self?.state == .waitingForResponse else {
return
}
guard isComplete else {
if let error {
print("Failed to receive UDP message: \(error)")
} else {
print("Failed to receive complete UDP message without error")
}
self?.finish()
return
}
guard let data else {
print("Received UDP message without data")
self?.finish()
return
}
print("Received \(data.count) bytes")
self?.finish(data)
}
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5)) {
guard self.state == .waitingForResponse else {
return
}
print("Timed out waiting for response")
self.finish()
}
}
}