mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
* Heartbeat rework * remove some debug loggings * stik mini tool poc * Saperate JITEnableContext into extensions and put each feature into their own files. Fix local pairing file is removed after switched to external device by directly loading the remote device's pairing file in place. * custom origin & xmlhttprequest * Fix some memory leak * allow background script access idevice (codes are from the old JSSupport branch) Tests are not carried out but most of the js api should work. Do we need to obfuscate some functions' name? * Fix JITEnableContext singleton initializer race condition * Fix instprox callback --------- Co-authored-by: Stephen B <158498287+StephenDev0@users.noreply.github.com>
313 lines
11 KiB
Swift
313 lines
11 KiB
Swift
//
|
|
// DeviceLibraryStore.swift
|
|
// StikJIT
|
|
//
|
|
// Created by Stephen.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
struct DeviceProfileEntry: Identifiable, Codable, Equatable {
|
|
var id: UUID
|
|
var name: String
|
|
var ipAddress: String
|
|
var pairingRelativePath: String
|
|
var pairingFilename: String
|
|
var dateAdded: Date
|
|
var lastUpdated: Date
|
|
var isTXM: Bool
|
|
|
|
init(id: UUID,
|
|
name: String,
|
|
ipAddress: String,
|
|
pairingRelativePath: String,
|
|
pairingFilename: String,
|
|
dateAdded: Date,
|
|
lastUpdated: Date,
|
|
isTXM: Bool = false) {
|
|
self.id = id
|
|
self.name = name
|
|
self.ipAddress = ipAddress
|
|
self.pairingRelativePath = pairingRelativePath
|
|
self.pairingFilename = pairingFilename
|
|
self.dateAdded = dateAdded
|
|
self.lastUpdated = lastUpdated
|
|
self.isTXM = isTXM
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id, name, ipAddress, pairingRelativePath, pairingFilename, dateAdded, lastUpdated, isTXM
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try container.decode(UUID.self, forKey: .id)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
ipAddress = try container.decode(String.self, forKey: .ipAddress)
|
|
pairingRelativePath = try container.decode(String.self, forKey: .pairingRelativePath)
|
|
pairingFilename = try container.decode(String.self, forKey: .pairingFilename)
|
|
dateAdded = try container.decode(Date.self, forKey: .dateAdded)
|
|
lastUpdated = try container.decode(Date.self, forKey: .lastUpdated)
|
|
isTXM = try container.decodeIfPresent(Bool.self, forKey: .isTXM) ?? false
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(id, forKey: .id)
|
|
try container.encode(name, forKey: .name)
|
|
try container.encode(ipAddress, forKey: .ipAddress)
|
|
try container.encode(pairingRelativePath, forKey: .pairingRelativePath)
|
|
try container.encode(pairingFilename, forKey: .pairingFilename)
|
|
try container.encode(dateAdded, forKey: .dateAdded)
|
|
try container.encode(lastUpdated, forKey: .lastUpdated)
|
|
try container.encode(isTXM, forKey: .isTXM)
|
|
}
|
|
}
|
|
|
|
enum DeviceLibraryError: LocalizedError {
|
|
case missingPairingData
|
|
case deviceNotFound
|
|
case pairingFileUnavailable
|
|
case fileOperationFailed(String)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .missingPairingData:
|
|
return "Select a pairing file before saving."
|
|
case .deviceNotFound:
|
|
return "The selected device could not be found."
|
|
case .pairingFileUnavailable:
|
|
return "The pairing file for this device is missing. Re-import it and try again."
|
|
case .fileOperationFailed(let reason):
|
|
return reason
|
|
}
|
|
}
|
|
}
|
|
|
|
final class DeviceLibraryStore: ObservableObject {
|
|
static let shared = DeviceLibraryStore()
|
|
|
|
@Published private(set) var devices: [DeviceProfileEntry] = []
|
|
@Published private(set) var activeDeviceID: UUID?
|
|
|
|
var activeDevice: DeviceProfileEntry? {
|
|
guard let activeDeviceID else { return nil }
|
|
return devices.first(where: { $0.id == activeDeviceID })
|
|
}
|
|
|
|
var isUsingExternalDevice: Bool {
|
|
activeDeviceID != nil
|
|
}
|
|
|
|
var defaultLocalDevice: DeviceProfileEntry {
|
|
DeviceProfileEntry(
|
|
id: localLoopbackID,
|
|
name: "This Device",
|
|
ipAddress: "10.7.0.2",
|
|
pairingRelativePath: "",
|
|
pairingFilename: "pairingFile.plist",
|
|
dateAdded: Date.distantPast,
|
|
lastUpdated: Date.distantPast
|
|
)
|
|
}
|
|
|
|
private let fileManager = FileManager.default
|
|
private let storageURL: URL
|
|
private let pairingsDirectory: URL
|
|
private let baseDirectory: URL
|
|
private let activeDeviceKey = "DeviceLibraryActiveDeviceID"
|
|
private let localLoopbackID = UUID(uuidString: "00000000-0000-0000-0000-000000000001") ?? UUID()
|
|
|
|
private init() {
|
|
baseDirectory = URL.documentsDirectory.appendingPathComponent("DeviceLibrary", isDirectory: true)
|
|
pairingsDirectory = baseDirectory.appendingPathComponent("Pairings", isDirectory: true)
|
|
storageURL = baseDirectory.appendingPathComponent("devices.json")
|
|
createDirectoriesIfNeeded()
|
|
loadFromDisk()
|
|
if let rawValue = UserDefaults.standard.string(forKey: activeDeviceKey),
|
|
let uuid = UUID(uuidString: rawValue),
|
|
devices.contains(where: { $0.id == uuid }) {
|
|
activeDeviceID = uuid
|
|
} else {
|
|
UserDefaults.standard.removeObject(forKey: activeDeviceKey)
|
|
UserDefaults.standard.set(false, forKey: UserDefaults.Keys.usingExternalDevice)
|
|
activeDeviceID = nil
|
|
}
|
|
updateExternalDeviceFlag()
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
func refresh() {
|
|
loadFromDisk()
|
|
}
|
|
|
|
func addDevice(name: String,
|
|
ipAddress: String,
|
|
pairingData: Data?,
|
|
originalFilename: String?,
|
|
isTXM: Bool) throws {
|
|
guard let pairingData else {
|
|
throw DeviceLibraryError.missingPairingData
|
|
}
|
|
|
|
let id = UUID()
|
|
let now = Date()
|
|
let relativePath = try persistPairingData(pairingData, for: id)
|
|
let entry = DeviceProfileEntry(
|
|
id: id,
|
|
name: name.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
ipAddress: ipAddress.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
pairingRelativePath: relativePath,
|
|
pairingFilename: originalFilename ?? "pairingFile.plist",
|
|
dateAdded: now,
|
|
lastUpdated: now,
|
|
isTXM: isTXM
|
|
)
|
|
devices.append(entry)
|
|
persistDevices()
|
|
}
|
|
|
|
func update(device: DeviceProfileEntry,
|
|
name: String,
|
|
ipAddress: String,
|
|
pairingData: Data?,
|
|
originalFilename: String?,
|
|
isTXM: Bool) throws {
|
|
guard !isDefaultDevice(device) else { return }
|
|
guard let index = devices.firstIndex(where: { $0.id == device.id }) else {
|
|
throw DeviceLibraryError.deviceNotFound
|
|
}
|
|
|
|
devices[index].name = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
devices[index].ipAddress = ipAddress.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
devices[index].lastUpdated = Date()
|
|
devices[index].isTXM = isTXM
|
|
if activeDeviceID == device.id {
|
|
UserDefaults.standard.set(devices[index].ipAddress, forKey: "TunnelDeviceIP")
|
|
}
|
|
|
|
if let pairingData {
|
|
let relativePath = try persistPairingData(pairingData, for: device.id)
|
|
devices[index].pairingRelativePath = relativePath
|
|
if let originalFilename {
|
|
devices[index].pairingFilename = originalFilename
|
|
}
|
|
}
|
|
|
|
persistDevices()
|
|
}
|
|
|
|
func remove(device: DeviceProfileEntry) throws {
|
|
guard !isDefaultDevice(device) else { return }
|
|
guard let index = devices.firstIndex(where: { $0.id == device.id }) else {
|
|
throw DeviceLibraryError.deviceNotFound
|
|
}
|
|
|
|
let relativePath = devices[index].pairingRelativePath
|
|
let storedURL = pairingsDirectory.appendingPathComponent(relativePath)
|
|
if fileManager.fileExists(atPath: storedURL.path) {
|
|
try? fileManager.removeItem(at: storedURL)
|
|
}
|
|
|
|
devices.remove(at: index)
|
|
if activeDeviceID == device.id {
|
|
clearActiveDevice()
|
|
}
|
|
persistDevices()
|
|
}
|
|
|
|
func activate(device: DeviceProfileEntry) throws {
|
|
if isDefaultDevice(device) {
|
|
clearActiveDevice()
|
|
return
|
|
}
|
|
let storedURL = pairingsDirectory.appendingPathComponent(device.pairingRelativePath)
|
|
guard fileManager.fileExists(atPath: storedURL.path) else {
|
|
throw DeviceLibraryError.pairingFileUnavailable
|
|
}
|
|
|
|
UserDefaults.standard.set(device.ipAddress, forKey: "TunnelDeviceIP")
|
|
activeDeviceID = device.id
|
|
UserDefaults.standard.set(device.id.uuidString, forKey: activeDeviceKey)
|
|
UserDefaults.standard.set(true, forKey: UserDefaults.Keys.usingExternalDevice)
|
|
persistDevices()
|
|
updateExternalDeviceFlag()
|
|
}
|
|
|
|
func clearActiveDevice() {
|
|
activeDeviceID = nil
|
|
UserDefaults.standard.removeObject(forKey: activeDeviceKey)
|
|
UserDefaults.standard.set(false, forKey: UserDefaults.Keys.usingExternalDevice)
|
|
UserDefaults.standard.removeObject(forKey: "TunnelDeviceIP")
|
|
updateExternalDeviceFlag()
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private func createDirectoriesIfNeeded() {
|
|
do {
|
|
if !fileManager.fileExists(atPath: baseDirectory.path) {
|
|
try fileManager.createDirectory(at: baseDirectory, withIntermediateDirectories: true)
|
|
}
|
|
if !fileManager.fileExists(atPath: pairingsDirectory.path) {
|
|
try fileManager.createDirectory(at: pairingsDirectory, withIntermediateDirectories: true)
|
|
}
|
|
} catch {
|
|
print("Failed to create DeviceLibrary directories: \(error)")
|
|
}
|
|
}
|
|
|
|
private func persistPairingData(_ data: Data, for id: UUID) throws -> String {
|
|
createDirectoriesIfNeeded()
|
|
let filename = "\(id.uuidString).mobiledevicepairing"
|
|
let destination = pairingsDirectory.appendingPathComponent(filename)
|
|
if fileManager.fileExists(atPath: destination.path) {
|
|
try fileManager.removeItem(at: destination)
|
|
}
|
|
do {
|
|
try data.write(to: destination, options: .atomic)
|
|
try fileManager.setAttributes([.posixPermissions: 0o600], ofItemAtPath: destination.path)
|
|
} catch {
|
|
throw DeviceLibraryError.fileOperationFailed("Unable to store pairing file. \(error.localizedDescription)")
|
|
}
|
|
return filename
|
|
}
|
|
|
|
private func loadFromDisk() {
|
|
guard fileManager.fileExists(atPath: storageURL.path) else {
|
|
devices = []
|
|
return
|
|
}
|
|
do {
|
|
let data = try Data(contentsOf: storageURL)
|
|
let decoded = try JSONDecoder().decode([DeviceProfileEntry].self, from: data)
|
|
devices = decoded
|
|
} catch {
|
|
print("Failed to load device library: \(error)")
|
|
devices = []
|
|
}
|
|
if let activeDeviceID, !devices.contains(where: { $0.id == activeDeviceID }) {
|
|
clearActiveDevice()
|
|
}
|
|
updateExternalDeviceFlag()
|
|
}
|
|
|
|
private func persistDevices() {
|
|
do {
|
|
createDirectoriesIfNeeded()
|
|
let data = try JSONEncoder().encode(devices)
|
|
try data.write(to: storageURL, options: .atomic)
|
|
} catch {
|
|
print("Failed to save device library: \(error)")
|
|
}
|
|
}
|
|
|
|
private func updateExternalDeviceFlag() {
|
|
UserDefaults.standard.set(isUsingExternalDevice, forKey: UserDefaults.Keys.usingExternalDevice)
|
|
}
|
|
|
|
func isDefaultDevice(_ device: DeviceProfileEntry) -> Bool {
|
|
device.id == localLoopbackID
|
|
}
|
|
}
|