remote: establish handshake between client and server
This commit is contained in:
parent
932ded4dce
commit
e2e827bd48
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
"location" : "https://github.com/utmapp/SwiftConnect",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "853f37a2071bf4ea3d225f7b538325cf6342edf2"
|
||||
"revision" : "7d8a5181bb882cf6faffed1cd4dfe3b169eeedfb"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue