remote: add remote server
This commit is contained in:
parent
7985cdee0d
commit
1bd1b5fda2
|
@ -88,7 +88,12 @@ struct AlertMessage: Identifiable {
|
|||
nonisolated private var documentsURL: URL {
|
||||
UTMData.defaultStorageUrl
|
||||
}
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
/// Remote access server
|
||||
private(set) var remoteServer: UTMRemoteServer!
|
||||
#endif
|
||||
|
||||
/// Queue to run `busyWork` tasks
|
||||
private var busyQueue: DispatchQueue
|
||||
|
||||
|
@ -100,6 +105,9 @@ struct AlertMessage: Identifiable {
|
|||
self.virtualMachines = []
|
||||
self.pendingVMs = []
|
||||
self.selectedVM = nil
|
||||
#if os(macOS)
|
||||
self.remoteServer = UTMRemoteServer(data: self)
|
||||
#endif
|
||||
listLoadFromDefaults()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,445 @@
|
|||
//
|
||||
// Copyright © 2023 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 Combine
|
||||
import SwiftConnect
|
||||
import UserNotifications
|
||||
|
||||
let service = "_utm_server._tcp"
|
||||
|
||||
actor UTMRemoteServer {
|
||||
private let data: UTMData
|
||||
private let keyManager = UTMRemoteKeyManager(forClient: false)
|
||||
private let center = UNUserNotificationCenter.current()
|
||||
let state: State
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private var notificationDelegate: NotificationDelegate?
|
||||
private var listener: Task<Void, Error>?
|
||||
private var pendingConnections: [State.ClientFingerprint: Connection] = [:]
|
||||
private var establishedConnections: [State.ClientFingerprint: Connection] = [:]
|
||||
|
||||
private func _replaceCancellables(with set: Set<AnyCancellable>) {
|
||||
cancellables = set
|
||||
}
|
||||
|
||||
@MainActor
|
||||
init(data: UTMData) {
|
||||
let _state = State()
|
||||
var _cancellables = Set<AnyCancellable>()
|
||||
self.data = data
|
||||
self.state = _state
|
||||
|
||||
_cancellables.insert(_state.$approvedClients.sink { approved in
|
||||
Task {
|
||||
await self.approvedClientsHasChanged(approved)
|
||||
}
|
||||
})
|
||||
_cancellables.insert(_state.$blockedClients.sink { blocked in
|
||||
Task {
|
||||
await self.blockedClientsHasChanged(blocked)
|
||||
}
|
||||
})
|
||||
_cancellables.insert(_state.$connectedClients.sink { connected in
|
||||
Task {
|
||||
await self.connectedClientsHasChanged(connected)
|
||||
}
|
||||
})
|
||||
_cancellables.insert(_state.$serverAction.sink { action in
|
||||
guard action != .none else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
switch action {
|
||||
case .stop:
|
||||
await self.stop()
|
||||
break
|
||||
case .start:
|
||||
await self.start()
|
||||
break
|
||||
case .reset:
|
||||
await self.resetServer()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
self.state.requestServerAction(.none)
|
||||
}
|
||||
})
|
||||
// this is a really ugly way to make sure that we keep a reference to the AnyCancellables even though
|
||||
// we cannot access self._cancellables from init() due to it being associated with @MainActor.
|
||||
// it should be fine because we only need to make sure the references are not dropped, we will never
|
||||
// actually read from _cancellables
|
||||
Task {
|
||||
await self._replaceCancellables(with: _cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
private func withErrorNotification(_ body: () async throws -> Void) async {
|
||||
do {
|
||||
try await body()
|
||||
} catch {
|
||||
await notifyError(error)
|
||||
}
|
||||
}
|
||||
|
||||
func start() async {
|
||||
do {
|
||||
try await center.requestAuthorization(options: .alert)
|
||||
} catch {
|
||||
logger.error("Failed to authorize notifications.")
|
||||
}
|
||||
await withErrorNotification {
|
||||
guard await !state.isServerActive else {
|
||||
return
|
||||
}
|
||||
try await keyManager.load()
|
||||
registerNotifications()
|
||||
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)
|
||||
await newRemoteConnection(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
await state.setServerActive(false)
|
||||
}
|
||||
await state.setServerActive(true)
|
||||
}
|
||||
}
|
||||
|
||||
func stop() async {
|
||||
unregisterNotifications()
|
||||
listener?.cancel()
|
||||
listener = nil
|
||||
await state.setServerActive(false)
|
||||
}
|
||||
|
||||
private func newRemoteConnection(_ connection: Connection) async {
|
||||
let remoteAddress = connection.connection.endpoint.debugDescription
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint().hexString() else {
|
||||
connection.close()
|
||||
return
|
||||
}
|
||||
guard await !state.isBlocked(fingerprint) else {
|
||||
connection.close()
|
||||
return
|
||||
}
|
||||
await state.seen(fingerprint, name: remoteAddress)
|
||||
pendingConnections[fingerprint] = connection
|
||||
if await state.isApproved(fingerprint) {
|
||||
await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint)
|
||||
await establishConnection(connection)
|
||||
} else {
|
||||
await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint, isUnknown: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func approvedClientsHasChanged(_ approvedClients: Set<State.Client>) async {
|
||||
for approvedClient in approvedClients {
|
||||
if let connection = pendingConnections.removeValue(forKey: approvedClient.fingerprint) {
|
||||
await establishConnection(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func blockedClientsHasChanged(_ blockedClients: Set<State.Client>) {
|
||||
for blockedClient in blockedClients {
|
||||
if let connection = pendingConnections.removeValue(forKey: blockedClient.fingerprint) {
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func connectedClientsHasChanged(_ connectedClients: Set<State.ClientFingerprint>) {
|
||||
for connectedClient in connectedClients {
|
||||
if let connection = establishedConnections.removeValue(forKey: connectedClient) {
|
||||
connection.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func establishConnection(_ connection: Connection) async {
|
||||
guard let fingerprint = connection.peerCertificateChain.first?.fingerprint().hexString() else {
|
||||
connection.close()
|
||||
return
|
||||
}
|
||||
await withErrorNotification {
|
||||
await state.connect(fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetServer() async {
|
||||
await withErrorNotification {
|
||||
try await keyManager.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UTMRemoteServer {
|
||||
private class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
||||
private let state: UTMRemoteServer.State
|
||||
|
||||
init(state: UTMRemoteServer.State) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
|
||||
.banner
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
Task {
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let fingerprint = userInfo["FINGERPRINT"] as? String else {
|
||||
return
|
||||
}
|
||||
switch response.actionIdentifier {
|
||||
case "ALLOW_ACTION":
|
||||
await state.approve(fingerprint)
|
||||
case "DENY_ACTION":
|
||||
await state.block(fingerprint)
|
||||
case "DISCONNECT_ACTION":
|
||||
await state.disconnect(fingerprint)
|
||||
default:
|
||||
break
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func registerNotifications() {
|
||||
let allowAction = UNNotificationAction(identifier: "ALLOW_ACTION",
|
||||
title: NSString.localizedUserNotificationString(forKey: "Allow", arguments: nil),
|
||||
options: [])
|
||||
let denyAction = UNNotificationAction(identifier: "DENY_ACTION",
|
||||
title: NSString.localizedUserNotificationString(forKey: "Deny", arguments: nil),
|
||||
options: [])
|
||||
let disconnectAction = UNNotificationAction(identifier: "DISCONNECT_ACTION",
|
||||
title: NSString.localizedUserNotificationString(forKey: "Disconnect", arguments: nil),
|
||||
options: [])
|
||||
let unknownRemoteCategory = UNNotificationCategory(identifier: "UNKNOWN_REMOTE_CLIENT",
|
||||
actions: [denyAction, allowAction],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New unknown remote client connection.", arguments: nil),
|
||||
options: .customDismissAction)
|
||||
let trustedRemoteCategory = UNNotificationCategory(identifier: "TRUSTED_REMOTE_CLIENT",
|
||||
actions: [disconnectAction],
|
||||
intentIdentifiers: [],
|
||||
hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New trusted remote client connection.", arguments: nil),
|
||||
options: [])
|
||||
center.setNotificationCategories([unknownRemoteCategory, trustedRemoteCategory])
|
||||
notificationDelegate = NotificationDelegate(state: state)
|
||||
center.delegate = notificationDelegate
|
||||
}
|
||||
|
||||
private func unregisterNotifications() {
|
||||
center.setNotificationCategories([])
|
||||
notificationDelegate = nil
|
||||
center.delegate = nil
|
||||
}
|
||||
|
||||
private func notifyNewConnection(remoteAddress: String, fingerprint: String, isUnknown: Bool = false) async {
|
||||
let settings = await center.notificationSettings()
|
||||
guard settings.authorizationStatus == .authorized else {
|
||||
logger.info("Notifications disabled, ignoring connection request from '\(remoteAddress)' with fingerprint '\(fingerprint)'")
|
||||
return
|
||||
}
|
||||
let content = UNMutableNotificationContent()
|
||||
if isUnknown {
|
||||
content.title = NSString.localizedUserNotificationString(forKey: "Unknown Remote Client", arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(forKey: "A client with fingerprint '%@' is attempting to connect.", arguments: [fingerprint])
|
||||
content.categoryIdentifier = "UNKNOWN_REMOTE_CLIENT"
|
||||
} else {
|
||||
content.title = NSString.localizedUserNotificationString(forKey: "Remote Client Connected", arguments: nil)
|
||||
content.body = NSString.localizedUserNotificationString(forKey: "Established connection from %@.", arguments: [remoteAddress])
|
||||
content.categoryIdentifier = "TRUSTED_REMOTE_CLIENT"
|
||||
}
|
||||
content.userInfo = ["FINGERPRINT": fingerprint]
|
||||
let request = UNNotificationRequest(identifier: fingerprint,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
do {
|
||||
try await center.add(request)
|
||||
} catch {
|
||||
logger.error("Error sending remote connection request: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private func notifyError(_ error: Error) async {
|
||||
logger.error("UTM Remote Server error: '\(error)'")
|
||||
let settings = await center.notificationSettings()
|
||||
guard settings.authorizationStatus == .authorized else {
|
||||
return
|
||||
}
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSString.localizedUserNotificationString(forKey: "UTM Remote Server Error", arguments: nil)
|
||||
content.body = error.localizedDescription
|
||||
let request = UNNotificationRequest(identifier: UUID().uuidString,
|
||||
content: content,
|
||||
trigger: nil)
|
||||
do {
|
||||
try await center.add(request)
|
||||
} catch {
|
||||
logger.error("Error sending error notification: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UTMRemoteServer {
|
||||
@MainActor
|
||||
class State: ObservableObject {
|
||||
typealias ClientFingerprint = String
|
||||
struct Client: Codable, Identifiable, Hashable {
|
||||
let fingerprint: ClientFingerprint
|
||||
var name: String
|
||||
var lastSeen: Date
|
||||
|
||||
var id: String {
|
||||
fingerprint
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(fingerprint)
|
||||
}
|
||||
|
||||
static func == (lhs: Client, rhs: Client) -> Bool {
|
||||
lhs.hashValue == rhs.hashValue
|
||||
}
|
||||
}
|
||||
|
||||
enum ServerAction {
|
||||
case none
|
||||
case stop
|
||||
case start
|
||||
case reset
|
||||
}
|
||||
|
||||
@Published var allClients: [Client] {
|
||||
didSet {
|
||||
let all = Set(allClients)
|
||||
approvedClients.subtract(approvedClients.subtracting(all))
|
||||
blockedClients.subtract(blockedClients.subtracting(all))
|
||||
connectedClients.subtract(connectedClients.subtracting(all.map({ $0.fingerprint })))
|
||||
}
|
||||
}
|
||||
|
||||
@Published var approvedClients: Set<Client> {
|
||||
didSet {
|
||||
UserDefaults.standard.setValue(try! approvedClients.propertyList(), forKey: "TrustedClients")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var blockedClients: Set<Client> {
|
||||
didSet {
|
||||
UserDefaults.standard.setValue(try! blockedClients.propertyList(), forKey: "BlockedClients")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var connectedClients = Set<ClientFingerprint>()
|
||||
|
||||
@Published var serverAction: ServerAction = .none
|
||||
|
||||
var isBusy: Bool {
|
||||
serverAction != .none
|
||||
}
|
||||
|
||||
@Published private(set) var isServerActive = false
|
||||
|
||||
init() {
|
||||
var _approvedClients = Set<Client>()
|
||||
if let array = UserDefaults.standard.array(forKey: "TrustedClients") {
|
||||
if let clients = try? Set<Client>(fromPropertyList: array) {
|
||||
_approvedClients = clients
|
||||
}
|
||||
}
|
||||
self.approvedClients = _approvedClients
|
||||
var _blockedClients = Set<Client>()
|
||||
if let array = UserDefaults.standard.array(forKey: "BlockedClients") {
|
||||
if let clients = try? Set<Client>(fromPropertyList: array) {
|
||||
_blockedClients = clients
|
||||
}
|
||||
}
|
||||
self.blockedClients = _blockedClients
|
||||
self.allClients = Array(_approvedClients) + Array(_blockedClients)
|
||||
}
|
||||
|
||||
func isConnected(_ fingerprint: ClientFingerprint) -> Bool {
|
||||
connectedClients.contains(fingerprint)
|
||||
}
|
||||
|
||||
func isApproved(_ fingerprint: ClientFingerprint) -> Bool {
|
||||
approvedClients.contains(where: { $0.fingerprint == fingerprint }) && !isBlocked(fingerprint)
|
||||
}
|
||||
|
||||
func isBlocked(_ fingerprint: ClientFingerprint) -> Bool {
|
||||
blockedClients.contains(where: { $0.fingerprint == fingerprint })
|
||||
}
|
||||
|
||||
fileprivate func setServerActive(_ isActive: Bool) {
|
||||
isServerActive = isActive
|
||||
}
|
||||
|
||||
func requestServerAction(_ action: ServerAction) {
|
||||
serverAction = action
|
||||
}
|
||||
|
||||
private func client(forFingerprint fingerprint: ClientFingerprint, name: String? = nil) -> (Int?, Client) {
|
||||
if let idx = allClients.firstIndex(where: { $0.fingerprint == fingerprint }) {
|
||||
if let name = name {
|
||||
allClients[idx].name = name
|
||||
}
|
||||
return (idx, allClients[idx])
|
||||
} else {
|
||||
return (nil, Client(fingerprint: fingerprint, name: name ?? "", lastSeen: Date()))
|
||||
}
|
||||
}
|
||||
|
||||
func seen(_ fingerprint: ClientFingerprint, name: String? = nil) {
|
||||
var (idx, client) = client(forFingerprint: fingerprint, name: name)
|
||||
client.lastSeen = Date()
|
||||
if let idx = idx {
|
||||
allClients[idx] = client
|
||||
} else {
|
||||
allClients.append(client)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func connect(_ fingerprint: ClientFingerprint, name: String? = nil) {
|
||||
connectedClients.insert(fingerprint)
|
||||
}
|
||||
|
||||
func disconnect(_ fingerprint: ClientFingerprint) {
|
||||
connectedClients.remove(fingerprint)
|
||||
}
|
||||
|
||||
func approve(_ fingerprint: ClientFingerprint) {
|
||||
let (_, client) = client(forFingerprint: fingerprint)
|
||||
approvedClients.insert(client)
|
||||
blockedClients.remove(client)
|
||||
}
|
||||
|
||||
func block(_ fingerprint: ClientFingerprint) {
|
||||
let (_, client) = client(forFingerprint: fingerprint)
|
||||
approvedClients.remove(client)
|
||||
blockedClients.insert(client)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -384,3 +384,20 @@ extension String {
|
|||
return Int(numeric)
|
||||
}
|
||||
}
|
||||
|
||||
extension Encodable {
|
||||
func propertyList() throws -> Any {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
let xml = try encoder.encode(self)
|
||||
return try PropertyListSerialization.propertyList(from: xml, format: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension Decodable {
|
||||
init(fromPropertyList propertyList: Any) throws {
|
||||
let data = try PropertyListSerialization.data(fromPropertyList: propertyList, format: .xml, options: 0)
|
||||
let decoder = PropertyListDecoder()
|
||||
self = try decoder.decode(Self.self, from: data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class UTMRegistry: NSObject {
|
|||
super.init()
|
||||
if let newEntries = try? serializedEntries.mapValues({ value in
|
||||
let dict = value as! [String: Any]
|
||||
return try UTMRegistryEntry(from: dict)
|
||||
return try UTMRegistryEntry(fromPropertyList: dict)
|
||||
}) {
|
||||
entries = newEntries
|
||||
}
|
||||
|
|
|
@ -109,11 +109,7 @@ import Foundation
|
|||
}
|
||||
|
||||
func asDictionary() throws -> [String: Any] {
|
||||
let encoder = PropertyListEncoder()
|
||||
encoder.outputFormat = .xml
|
||||
let xml = try encoder.encode(self)
|
||||
let dict = try PropertyListSerialization.propertyList(from: xml, format: nil)
|
||||
return dict as! [String: Any]
|
||||
return try propertyList() as! [String: Any]
|
||||
}
|
||||
|
||||
/// Update the UUID
|
||||
|
@ -128,13 +124,6 @@ import Foundation
|
|||
|
||||
protocol UTMRegistryEntryDecodable: Decodable {}
|
||||
extension UTMRegistryEntry: UTMRegistryEntryDecodable {}
|
||||
extension UTMRegistryEntryDecodable {
|
||||
init(from dictionary: [String: Any]) throws {
|
||||
let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0)
|
||||
let decoder = PropertyListDecoder()
|
||||
self = try decoder.decode(Self.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Accessors
|
||||
@MainActor extension UTMRegistryEntry {
|
||||
|
|
|
@ -652,6 +652,7 @@
|
|||
CE9B15382B11A4A7003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15372B11A4A7003A32DD /* SwiftConnect */; };
|
||||
CE9B153A2B11A4AE003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15392B11A4AE003A32DD /* SwiftConnect */; };
|
||||
CE9B153C2B11A4B4003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B153B2B11A4B4003A32DD /* SwiftConnect */; };
|
||||
CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */; };
|
||||
CE9B15412B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
|
||||
CE9B15422B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
|
||||
CE9B15432B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
|
||||
|
@ -1912,6 +1913,7 @@
|
|||
CE9A352D26533A51005077CF /* JailbreakInterposer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JailbreakInterposer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
CE9A353026533A52005077CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
CE9A353F26533AE6005077CF /* JailbreakInterposer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = JailbreakInterposer.c; sourceTree = "<group>"; };
|
||||
CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteServer.swift; sourceTree = "<group>"; };
|
||||
CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteKeyManager.swift; sourceTree = "<group>"; };
|
||||
CE9B15452B12A87E003A32DD /* GenerateKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GenerateKey.h; sourceTree = "<group>"; };
|
||||
CE9B15462B12A87E003A32DD /* GenerateKey.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = GenerateKey.c; sourceTree = "<group>"; };
|
||||
|
@ -2826,6 +2828,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */,
|
||||
CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */,
|
||||
CE9B15452B12A87E003A32DD /* GenerateKey.h */,
|
||||
CE9B15462B12A87E003A32DD /* GenerateKey.c */,
|
||||
);
|
||||
|
@ -3732,6 +3735,7 @@
|
|||
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */,
|
||||
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */,
|
||||
848A98C8287206AE006F0550 /* VMConfigAppleVirtualizationView.swift in Sources */,
|
||||
CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */,
|
||||
847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */,
|
||||
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */,
|
||||
CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */,
|
||||
|
|
Loading…
Reference in New Issue