remote: establish handshake between client and server

This commit is contained in:
osy 2024-01-26 16:56:02 -08:00
parent 932ded4dce
commit e2e827bd48
7 changed files with 267 additions and 18 deletions

View File

@ -114,7 +114,7 @@ struct AlertMessage: Identifiable {
self.remoteServer = UTMRemoteServer(data: self)
#endif
#if WITH_REMOTE
self.remoteClient = UTMRemoteClient()
self.remoteClient = UTMRemoteClient(data: self)
#endif
listLoadFromDefaults()
}

View File

@ -110,7 +110,10 @@ private struct ServerConnectView: View {
@EnvironmentObject private var data: UTMData
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
@State private var isConnecting: Bool = false
@State private var connectionTask: Task<Void, Error>?
private var isConnecting: Bool {
connectionTask != nil
}
@State private var isPasswordRequired: Bool = false
@State private var willBeSaved: Bool = true
@ -157,7 +160,7 @@ private struct ServerConnectView: View {
if isConnecting {
ProgressView().progressViewStyle(.circular)
Button {
connect()
connectionTask?.cancel()
} label: {
Text("Cancel")
}
@ -183,8 +186,10 @@ private struct ServerConnectView: View {
}
private func connect() {
Task {
isConnecting = true
guard connectionTask == nil else {
return
}
connectionTask = Task {
do {
try await remoteClient.connect(server, shouldSaveDetails: willBeSaved)
} catch {
@ -192,11 +197,13 @@ private struct ServerConnectView: View {
withAnimation {
isPasswordRequired = true
}
} else if error is CancellationError {
// ignore it
} else {
remoteClientState.showErrorAlert(error.localizedDescription)
}
}
isConnecting = false
connectionTask = nil
}
}
}

View File

@ -23,13 +23,17 @@ let service = "_utm_server._tcp"
actor UTMRemoteClient {
let state: State
private let keyManager = UTMRemoteKeyManager(forClient: true)
private var local: Local
private var scanTask: Task<Void, Error>?
private var endpoints: [String: NWEndpoint] = [:]
private var server: Remote!
@MainActor
init() {
init(data: UTMData) {
self.state = State()
self.local = Local(data: data)
}
private func withErrorAlert(_ body: () async throws -> Void) async {
@ -80,6 +84,17 @@ actor UTMRemoteClient {
let connection = try await Connection.init(endpoint: endpoint, identity: keyManager.identity) { certs in
return true
}
try Task.checkCancellation()
let peer = Peer(connection: connection, localInterface: local)
let remote = Remote(peer: peer)
do {
try await remote.handshake()
} catch {
peer.close()
throw error
}
self.server = remote
await state.setConnected(true)
}
}
@ -143,6 +158,64 @@ extension UTMRemoteClient {
func updateFoundServers(_ servers: [Server]) {
foundServers = servers
}
fileprivate func setConnected(_ connected: Bool) {
isConnected = connected
}
}
}
extension UTMRemoteClient {
class Local: LocalInterface {
typealias M = UTMRemoteMessageClient
private let data: UTMData
init(data: UTMData) {
self.data = data
}
func handle(message: M, data: Data) async throws -> Data {
switch message {
case .clientHandshake:
return try await _handshake(parameters: .decode(data)).encode()
}
}
func handle(error: Error) {
Task {
await data.showErrorAlert(message: error.localizedDescription)
}
}
private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply {
return .init(version: UTMRemoteMessageClient.version)
}
}
}
extension UTMRemoteClient {
class Remote {
typealias M = UTMRemoteMessageServer
private let peer: Peer<UTMRemoteMessageClient>
init(peer: Peer<UTMRemoteMessageClient>) {
self.peer = peer
}
func close() {
peer.close()
}
func handshake() async throws {
guard try await _handshake(parameters: .init(version: UTMRemoteMessageServer.version)).version == UTMRemoteMessageServer.version else {
throw ClientError.versionMismatch
}
}
private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
try await M.ServerHandshake.send(parameters, to: peer)
}
}
}
@ -163,4 +236,15 @@ extension UTMRemoteClient {
}
}
}
enum ClientError: LocalizedError {
case versionMismatch
var errorDescription: String? {
switch self {
case .versionMismatch:
return NSLocalizedString("The server interface version does not match the client.", comment: "UTMRemoteClient")
}
}
}
}

View File

@ -0,0 +1,57 @@
//
// Copyright © 2024 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import SwiftConnect
enum UTMRemoteMessageServer: UInt8, MessageID {
static let version = 1
case serverHandshake
}
enum UTMRemoteMessageClient: UInt8, MessageID {
static let version = 1
case clientHandshake
}
extension UTMRemoteMessageServer {
struct ServerHandshake: Message {
static let id = UTMRemoteMessageServer.serverHandshake
struct Request: Serializable, Codable {
let version: Int
}
struct Reply: Serializable, Codable {
let version: Int
}
}
}
extension UTMRemoteMessageClient {
struct ClientHandshake: Message {
static let id = UTMRemoteMessageClient.clientHandshake
struct Request: Serializable, Codable {
let version: Int
}
struct Reply: Serializable, Codable {
let version: Int
}
}
}

View File

@ -22,7 +22,7 @@ import UserNotifications
let service = "_utm_server._tcp"
actor UTMRemoteServer {
private let data: UTMData
fileprivate let data: UTMData
private let keyManager = UTMRemoteKeyManager(forClient: false)
private let center = UNUserNotificationCenter.current()
let state: State
@ -31,7 +31,8 @@ actor UTMRemoteServer {
private var notificationDelegate: NotificationDelegate?
private var listener: Task<Void, Error>?
private var pendingConnections: [State.ClientFingerprint: Connection] = [:]
private var establishedConnections: [State.ClientFingerprint: Connection] = [:]
private var establishedConnections: [State.ClientFingerprint: Remote] = [:]
private var local: Local!
private func _replaceCancellables(with set: Set<AnyCancellable>) {
cancellables = set
@ -109,25 +110,30 @@ actor UTMRemoteServer {
}
try await keyManager.load()
registerNotifications()
local = Local(server: self)
listener = Task {
await withErrorNotification {
for try await connection in Connection.advertise(forServiceType: service, identity: keyManager.identity) {
await withErrorNotification {
let connection = try await Connection(connection: connection)
if let connection = try? await Connection(connection: connection) {
await newRemoteConnection(connection)
}
}
}
await state.setServerActive(false)
await stop()
}
await state.setServerActive(true)
}
}
func stop() async {
await state.disconnectAll()
unregisterNotifications()
listener?.cancel()
listener = nil
if let listener = listener {
self.listener = nil
listener.cancel()
_ = await listener.result
}
local = nil
await state.setServerActive(false)
}
@ -169,8 +175,8 @@ actor UTMRemoteServer {
private func connectedClientsHasChanged(_ connectedClients: Set<State.ClientFingerprint>) {
for connectedClient in connectedClients {
if let connection = establishedConnections.removeValue(forKey: connectedClient) {
connection.close()
if let remote = establishedConnections.removeValue(forKey: connectedClient) {
remote.close()
}
}
}
@ -181,6 +187,15 @@ actor UTMRemoteServer {
return
}
await withErrorNotification {
let peer = Peer(connection: connection, localInterface: local)
let remote = Remote(peer: peer)
do {
try await remote.handshake()
} catch {
peer.close()
throw error
}
establishedConnections.updateValue(remote, forKey: fingerprint)
await state.connect(fingerprint)
}
}
@ -278,12 +293,17 @@ extension UTMRemoteServer {
trigger: nil)
do {
try await center.add(request)
if !isUnknown {
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) {
self.center.removeDeliveredNotifications(withIdentifiers: [fingerprint])
}
}
} catch {
logger.error("Error sending remote connection request: \(error.localizedDescription)")
}
}
private func notifyError(_ error: Error) async {
fileprivate func notifyError(_ error: Error) async {
logger.error("UTM Remote Server error: '\(error)'")
let settings = await center.notificationSettings()
guard settings.authorizationStatus == .authorized else {
@ -430,6 +450,10 @@ extension UTMRemoteServer {
connectedClients.remove(fingerprint)
}
func disconnectAll() {
connectedClients.removeAll()
}
func approve(_ fingerprint: ClientFingerprint) {
let (_, client) = client(forFingerprint: fingerprint)
approvedClients.insert(client)
@ -443,3 +467,74 @@ extension UTMRemoteServer {
}
}
}
extension UTMRemoteServer {
class Local: LocalInterface {
typealias M = UTMRemoteMessageServer
private let server: UTMRemoteServer
private var data: UTMData {
server.data
}
init(server: UTMRemoteServer) {
self.server = server
}
func handle(message: M, data: Data) async throws -> Data {
switch message {
case .serverHandshake:
return try await _handshake(parameters: .decode(data)).encode()
}
}
func handle(error: Error) {
Task {
await server.notifyError(error)
}
}
private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
return .init(version: UTMRemoteMessageServer.version)
}
}
}
extension UTMRemoteServer {
class Remote {
typealias M = UTMRemoteMessageClient
private let peer: Peer<UTMRemoteMessageServer>
init(peer: Peer<UTMRemoteMessageServer>) {
self.peer = peer
}
func close() {
peer.close()
}
func handshake() async throws {
guard try await _handshake(parameters: .init(version: UTMRemoteMessageClient.version)).version == UTMRemoteMessageClient.version else {
throw ServerError.versionMismatch
}
}
private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply {
try await M.ClientHandshake.send(parameters, to: peer)
}
}
}
extension UTMRemoteServer {
enum ServerError: LocalizedError {
case versionMismatch
var errorDescription: String? {
switch self {
case .versionMismatch:
return NSLocalizedString("The client interface version does not match the server.", comment: "UTMRemoteServer")
}
}
}
}

View File

@ -628,6 +628,8 @@
CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */; };
CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
CE772AAC25C8B0F600E4E379 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; };
@ -1897,6 +1899,7 @@
CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = "<group>"; };
CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = "<group>"; };
CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteMessage.swift; sourceTree = "<group>"; };
CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfirmActionModifier.swift; sourceTree = "<group>"; };
CE6EDCDD241C4A6800A719DC /* UTMLegacyViewState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyViewState.h; sourceTree = "<group>"; };
CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyViewState.m; sourceTree = "<group>"; };
@ -2837,6 +2840,7 @@
children = (
CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */,
CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */,
CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */,
CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */,
CE9B15452B12A87E003A32DD /* GenerateKey.h */,
CE9B15462B12A87E003A32DD /* GenerateKey.c */,
@ -3729,6 +3733,7 @@
CE2D958A24AD4F990059923A /* VMConfigSystemView.swift in Sources */,
CEBBF1A724B5730F00C15049 /* UTMDataExtension.swift in Sources */,
84C584E3268F8AE7000FCABF /* VMQEMUSettingsView.swift in Sources */,
CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */,
845F1707289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift in Sources */,
843BF83E2845494C0029D60D /* UTMQemuConfigurationSerial.swift in Sources */,
841E997728AA1191003C6CB6 /* UTMRegistry.swift in Sources */,
@ -4076,6 +4081,7 @@
CEF7F61F2AEEDCC400E34952 /* VMToolbarDisplayMenuView.swift in Sources */,
CEF7F6202AEEDCC400E34952 /* ActivityView.swift in Sources */,
CEF7F6212AEEDCC400E34952 /* UTMPasteboard.swift in Sources */,
CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */,
CEF7F6222AEEDCC400E34952 /* QEMUArgument.swift in Sources */,
CEF7F6232AEEDCC400E34952 /* VMPlaceholderView.swift in Sources */,
CEF7F6242AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.m in Sources */,

View File

@ -78,7 +78,7 @@
"location" : "https://github.com/utmapp/SwiftConnect",
"state" : {
"branch" : "main",
"revision" : "853f37a2071bf4ea3d225f7b538325cf6342edf2"
"revision" : "7d8a5181bb882cf6faffed1cd4dfe3b169eeedfb"
}
},
{