From 65257ae43fde3e4dedffcdec314bd79e60ca623e Mon Sep 17 00:00:00 2001 From: Kabir Oberai Date: Tue, 24 Dec 2024 02:04:03 +0530 Subject: [PATCH] Use swift-deps, fix things --- Package.resolved | 47 ++++++++- Package.swift | 2 + .../DeveloperServicesAddAppOperation.swift | 3 +- ...perServicesFetchCertificateOperation.swift | 7 +- .../DeveloperServices+OpenAPI.swift | 24 ++--- .../DeveloperServicesClient.swift | 25 ++--- .../DeveloperServicesLoginManager.swift | 19 +--- ...veloperServicesFetchProfileOperation.swift | 42 ++++---- .../GrandSlam/Anisette/ADIDataProvider.swift | 69 +++++++++----- .../Anisette/AnisetteDataProvider.swift | 22 ++++- .../GrandSlam/Anisette/DeviceInfo.swift | 18 ++++ .../Anisette/OmnisetteADIProvider.swift | 32 +------ .../Anisette/SupersetteADIProvider.swift | 12 +-- .../Supersign/GrandSlam/GrandSlamClient.swift | 25 ++--- .../Lookup/GrandSlamLookupManager.swift | 13 +-- .../AsyncHTTPClient+HTTP.swift | 95 +++++++++---------- .../HTTPClientProtocol.swift | 53 ++++++++--- .../HTTPClientProtocol/URLSession+HTTP.swift | 9 +- .../Integration/SuperchargeInstaller.swift | 65 ++----------- Sources/Supersign/Signer/SigningContext.swift | 5 - .../Supersign/Utilities/KeyValueStorage.swift | 22 +++++ Sources/Supersign/Utilities/SigningInfo.swift | 28 +++++- .../Supersign/Utilities/ZIPCompressor.swift | 65 +++++++++++++ Sources/SupersignCLI/SupersignCLI.swift | 28 ++++++ Sources/SupersignCLI/main.swift | 25 ----- Sources/SupersignCLISupport/AuthCommand.swift | 30 ++---- Sources/SupersignCLISupport/AuthToken.swift | 20 ++-- Sources/SupersignCLISupport/DSCommand.swift | 15 +-- Sources/SupersignCLISupport/DevCommand.swift | 1 - .../SupersignCLISupport/DevSDKCommand.swift | 11 +-- .../SupersignCLISupport/InstallCommand.swift | 1 - .../ProcessZIPCompressor.swift | 50 ++++++++++ .../SupersignCLIDelegate.swift | 58 ----------- .../SupersignCommand.swift | 19 +--- 34 files changed, 534 insertions(+), 426 deletions(-) create mode 100644 Sources/Supersign/Utilities/ZIPCompressor.swift create mode 100644 Sources/SupersignCLI/SupersignCLI.swift delete mode 100644 Sources/SupersignCLI/main.swift create mode 100644 Sources/SupersignCLISupport/ProcessZIPCompressor.swift diff --git a/Package.resolved b/Package.resolved index b97700d..907580a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "5b3298fb2e08290a3e47a886cedfefce16ee07f46d1f0ccdc86fcdd488f8c170", + "originHash" : "19c490b9f4eaa20eede93f53a3b352116957402a682ed84a47bfaf23c195de03", "pins" : [ { "identity" : "aexml", @@ -28,6 +28,15 @@ "version" : "5.5.0" } }, + { + "identity" : "combine-schedulers", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/combine-schedulers", + "state" : { + "revision" : "9fa31f4403da54855f1e2aeaeff478f4f0e40b13", + "version" : "1.0.2" + } + }, { "identity" : "jsonutilities", "kind" : "remoteSourceControl", @@ -127,6 +136,15 @@ "version" : "1.6.1" } }, + { + "identity" : "swift-clocks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-clocks", + "state" : { + "revision" : "b9b24b69e2adda099a1fa381cda1eeec272d5b53", + "version" : "1.0.5" + } + }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -154,6 +172,15 @@ "version" : "3.9.1" } }, + { + "identity" : "swift-dependencies", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-dependencies", + "state" : { + "revision" : "5526c8a27675dc7b18d6fa643abfb64bcb200b77", + "version" : "1.6.2" + } + }, { "identity" : "swift-http-types", "kind" : "remoteSourceControl", @@ -262,6 +289,15 @@ "version" : "1.0.2" } }, + { + "identity" : "swift-syntax", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swiftlang/swift-syntax", + "state" : { + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" + } + }, { "identity" : "swift-system", "kind" : "remoteSourceControl", @@ -334,6 +370,15 @@ "version" : "8.16.0" } }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 8ea4706..66b4f9f 100644 --- a/Package.swift +++ b/Package.swift @@ -46,6 +46,7 @@ let package = Package( .package(url: "https://github.com/vapor/websocket-kit.git", from: "2.15.0"), .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.3.0"), + .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.6.2"), .package(url: "https://github.com/attaswift/BigInt", from: "5.5.0"), .package(url: "https://github.com/mxcl/Version", from: "2.1.0"), @@ -73,6 +74,7 @@ let package = Package( "CSupersign", .byName(name: "CSupersette", condition: .when(platforms: [.linux])), .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "SwiftyMobileDevice", package: "SwiftyMobileDevice"), .product(name: "Zupersign", package: "zsign"), .product(name: "SignerSupport", package: "SuperchargeCore"), diff --git a/Sources/Supersign/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift b/Sources/Supersign/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift index 664201c..e1c198d 100644 --- a/Sources/Supersign/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift +++ b/Sources/Supersign/DeveloperServices/App IDs/DeveloperServicesAddAppOperation.swift @@ -221,7 +221,8 @@ public struct DeveloperServicesAddAppOperation: DeveloperServicesOperation { let mobileprovision = try await DeveloperServicesFetchProfileOperation( context: self.context, - bundleID: newBundleID + bundleID: newBundleID, + signingInfo: signingInfo ).perform() return ProvisioningInfo( diff --git a/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift b/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift index c273fd7..f811178 100644 --- a/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift +++ b/Sources/Supersign/DeveloperServices/Certificates/DeveloperServicesFetchCertificateOperation.swift @@ -8,6 +8,7 @@ import Foundation import DeveloperAPI +import Dependencies public typealias DeveloperServicesCertificate = Components.Schemas.Certificate @@ -31,6 +32,8 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera } } + @Dependency(\.signingInfoManager) var signingInfoManager + public let context: SigningContext public let confirmRevocation: @Sendable ([DeveloperServicesCertificate]) async -> Bool public init( @@ -84,14 +87,14 @@ public struct DeveloperServicesFetchCertificateOperation: DeveloperServicesOpera try await group.waitForAll() } let signingInfo = try await createCertificate() - self.context.signingInfoManager[self.context.auth.identityID] = signingInfo + signingInfoManager[self.context.auth.identityID] = signingInfo return signingInfo } public func perform() async throws -> SigningInfo { let certificates = try await context.developerAPIClient.certificatesGetCollection().ok.body.json.data - guard let signingInfo = self.context.signingInfoManager[self.context.auth.identityID] else { + guard let signingInfo = signingInfoManager[self.context.auth.identityID] else { return try await self.replaceCertificates(certificates, requireConfirmation: true) } diff --git a/Sources/Supersign/DeveloperServices/DeveloperServices+OpenAPI.swift b/Sources/Supersign/DeveloperServices/DeveloperServices+OpenAPI.swift index 9b36646..8a2c0b7 100644 --- a/Sources/Supersign/DeveloperServices/DeveloperServices+OpenAPI.swift +++ b/Sources/Supersign/DeveloperServices/DeveloperServices+OpenAPI.swift @@ -3,18 +3,19 @@ import DeveloperAPI import HTTPTypes import OpenAPIRuntime import OpenAPIURLSession +import Dependencies extension DeveloperAPIClient { public init( - auth: DeveloperAPIAuthData, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory + auth: DeveloperAPIAuthData ) { + @Dependency(\.httpClient) var httpClient self.init( serverURL: try! Servers.Server1.url(), configuration: .init( dateTranscoder: .iso8601WithFractionalSeconds ), - transport: httpFactory.makeClient().asOpenAPITransport, + transport: httpClient.asOpenAPITransport, middlewares: [ auth.middleware ] @@ -48,24 +49,21 @@ public enum DeveloperAPIAuthData: Sendable { public struct XcodeAuthData: Sendable { public var loginToken: DeveloperServicesLoginToken - public var deviceInfo: DeviceInfo public var teamID: DeveloperServicesTeam.ID - public var anisetteDataProvider: AnisetteDataProvider public init( loginToken: DeveloperServicesLoginToken, - deviceInfo: DeviceInfo, - teamID: DeveloperServicesTeam.ID, - anisetteDataProvider: AnisetteDataProvider + teamID: DeveloperServicesTeam.ID ) { self.loginToken = loginToken - self.deviceInfo = deviceInfo self.teamID = teamID - self.anisetteDataProvider = anisetteDataProvider } } public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware { + @Dependency(\.deviceInfoProvider) private var deviceInfoProvider + @Dependency(\.anisetteDataProvider) private var anisetteDataProvider + public var authData: XcodeAuthData public init(authData: XcodeAuthData) { @@ -88,6 +86,8 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware { ) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { var request = request + let deviceInfo = try deviceInfoProvider.fetch() + // General request.headerFields[.acceptLanguage] = Locale.preferredLanguages.joined(separator: ", ") request.headerFields[.accept] = "application/vnd.api+json" @@ -102,7 +102,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware { request.headerFields[.init(DeviceInfo.clientInfoKey)!] = """ """ // deviceInfo.clientInfo.clientString - request.headerFields[.init(DeviceInfo.deviceIDKey)!] = authData.deviceInfo.deviceID + request.headerFields[.init(DeviceInfo.deviceIDKey)!] = deviceInfo.deviceID // GrandSlam authentication request.headerFields[.init("X-Apple-App-Info")!] = AppTokenKey.xcode.rawValue @@ -110,7 +110,7 @@ public struct DeveloperAPIXcodeAuthMiddleware: ClientMiddleware { request.headerFields[.init("X-Apple-GS-Token")!] = authData.loginToken.token // Anisette - let anisetteData = try await authData.anisetteDataProvider.fetchAnisetteData() + let anisetteData = try await anisetteDataProvider.fetchAnisetteData() for (key, value) in anisetteData.dictionary { request.headerFields[.init(key)!] = value } diff --git a/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift b/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift index 78535dd..2261e24 100644 --- a/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift +++ b/Sources/Supersign/DeveloperServices/DeveloperServicesClient.swift @@ -7,6 +7,7 @@ // import Foundation +import Dependencies public struct DeveloperServicesClient: Sendable { @@ -53,28 +54,18 @@ public struct DeveloperServicesClient: Sendable { } } - public let loginToken: DeveloperServicesLoginToken - public let deviceInfo: DeviceInfo - public let anisetteDataProvider: AnisetteDataProvider - private let httpClient: HTTPClientProtocol + @Dependency(\.deviceInfoProvider) var deviceInfoProvider + @Dependency(\.anisetteDataProvider) var anisetteDataProvider + @Dependency(\.httpClient) var httpClient - public init( - loginToken: DeveloperServicesLoginToken, - deviceInfo: DeviceInfo, - anisetteProvider: AnisetteDataProvider, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) { + public let loginToken: DeveloperServicesLoginToken + + public init(loginToken: DeveloperServicesLoginToken) { self.loginToken = loginToken - self.deviceInfo = deviceInfo - self.anisetteDataProvider = anisetteProvider - self.httpClient = httpFactory.makeClient() } public init(authData: XcodeAuthData) { self.loginToken = authData.loginToken - self.deviceInfo = authData.deviceInfo - self.anisetteDataProvider = authData.anisetteDataProvider - self.httpClient = defaultHTTPClientFactory.makeClient() } private func send( @@ -85,6 +76,8 @@ public struct DeveloperServicesClient: Sendable { throw Error.malformedRequest } + let deviceInfo = try deviceInfoProvider.fetch() + var httpRequest = HTTPRequest(url: url, method: "POST") let acceptedLanguages = Locale.preferredLanguages.joined(separator: ", ") diff --git a/Sources/Supersign/DeveloperServices/DeveloperServicesLoginManager.swift b/Sources/Supersign/DeveloperServices/DeveloperServicesLoginManager.swift index c97c4ec..809a281 100644 --- a/Sources/Supersign/DeveloperServices/DeveloperServicesLoginManager.swift +++ b/Sources/Supersign/DeveloperServices/DeveloperServicesLoginManager.swift @@ -7,6 +7,7 @@ // import Foundation +import Dependencies public struct DeveloperServicesLoginToken: Codable, Sendable { public let adsid: String @@ -26,23 +27,9 @@ public struct DeveloperServicesLoginManager: Sendable { case missingLoginToken } - public let deviceInfo: DeviceInfo - public let anisetteProvider: AnisetteDataProvider - private let client: GrandSlamClient + private let client = GrandSlamClient() - public init( - deviceInfo: DeviceInfo, - anisetteProvider: AnisetteDataProvider, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) throws { - self.deviceInfo = deviceInfo - self.anisetteProvider = anisetteProvider - self.client = GrandSlamClient( - deviceInfo: deviceInfo, - anisetteProvider: anisetteProvider, - httpFactory: httpFactory - ) - } + public init() {} private func logIn( withLoginData loginData: GrandSlamLoginData diff --git a/Sources/Supersign/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift b/Sources/Supersign/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift index b686edf..03c196a 100644 --- a/Sources/Supersign/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift +++ b/Sources/Supersign/DeveloperServices/Profiles/DeveloperServicesFetchProfileOperation.swift @@ -20,9 +20,11 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation public let context: SigningContext public let bundleID: String - public init(context: SigningContext, bundleID: String) { + public let signingInfo: SigningInfo + public init(context: SigningContext, bundleID: String, signingInfo: SigningInfo) { self.context = context self.bundleID = bundleID + self.signingInfo = signingInfo } public func perform() async throws -> Mobileprovision { @@ -47,18 +49,7 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation throw Errors.tooManyMatchingBundleIDs } - // note: free developer accounts don't seem to persist profiles at all - // so this will often be empty. - let profiles = bundleIDs - .included? - .compactMap { included -> Components.Schemas.Profile? in - if case .Profile(let profile) = included, profile.relationships?.bundleId?.data?.id == bundleID.id { - profile - } else { - nil - } - } ?? [] - + let profiles = bundleID.relationships?.profiles?.data ?? [] switch profiles.count { case 0: // we're good @@ -70,23 +61,36 @@ public struct DeveloperServicesFetchProfileOperation: DeveloperServicesOperation break } + let serialNumber = signingInfo.certificate.serialNumber() + let certs = try await context.developerAPIClient.certificatesGetCollection( + query: .init( + filter_lbrack_serialNumber_rbrack_: [serialNumber] + ) + ) + .ok.body.json.data + + let allDevices = try await context.developerAPIClient.devicesGetCollection() + .ok.body.json.data + let response = try await context.developerAPIClient.profilesCreateInstance( body: .json( .init( data: .init( _type: .profiles, attributes: .init( - name: "SC profile \(bundleID)", + name: "SC profile \(bundleID.id)", profileType: .iosAppDevelopment ), relationships: .init( bundleId: .init( - data: .init( - _type: .bundleIds, - id: bundleID.id - ) + data: .init(_type: .bundleIds, id: bundleID.id) ), - certificates: .init(data: []) + devices: .init(data: allDevices.map { + .init(_type: .devices, id: $0.id) + }), + certificates: .init(data: certs.map { + .init(_type: .certificates, id: $0.id) + }) ) ) ) diff --git a/Sources/Supersign/GrandSlam/Anisette/ADIDataProvider.swift b/Sources/Supersign/GrandSlam/Anisette/ADIDataProvider.swift index d45d3ae..5f70aea 100644 --- a/Sources/Supersign/GrandSlam/Anisette/ADIDataProvider.swift +++ b/Sources/Supersign/GrandSlam/Anisette/ADIDataProvider.swift @@ -1,6 +1,7 @@ import Foundation import Crypto import ConcurrencyExtras +import Dependencies public protocol RawADIProvider: Sendable { func clientInfo() async throws -> String @@ -26,6 +27,43 @@ extension RawADIProvider { } } +private struct UnimplementedRawADIProvider: RawADIProvider { + func startProvisioning( + spim: Data, + userID: UUID + ) async throws -> (any RawADIProvisioningSession, Data) { + let closure: (Data, UUID) async throws -> (any RawADIProvisioningSession, Data) = unimplemented() + return try await closure(spim, userID) + } + + func requestOTP( + userID: UUID, + routingInfo: inout UInt64, + provisioningInfo: Data + ) async throws -> (machineID: Data, otp: Data) { + let closure: (UUID, inout UInt64, Data) async throws -> (Data, Data) = unimplemented() + return try await closure(userID, &routingInfo, provisioningInfo) + } +} + +public enum RawADIProviderDependencyKey: DependencyKey { + public static let testValue: RawADIProvider = UnimplementedRawADIProvider() + public static let liveValue: RawADIProvider = { + #if os(Linux) + return SupersetteADIProvider() + #else + return OmnisetteADIProvider() + #endif + }() +} + +extension DependencyValues { + public var rawADIProvider: RawADIProvider { + get { self[RawADIProviderDependencyKey.self] } + set { self[RawADIProviderDependencyKey.self] = newValue } + } +} + public protocol RawADIProvisioningSession: Sendable { func endProvisioning( routingInfo: UInt64, @@ -35,7 +73,7 @@ public protocol RawADIProvisioningSession: Sendable { } // uses CoreADI APIs -public final class ADIDataProvider: AnisetteDataProvider { +public struct ADIDataProvider: AnisetteDataProvider { public enum ADIError: Error { case hashingFailed @@ -44,41 +82,28 @@ public final class ADIDataProvider: AnisetteDataProvider { case badEndResponse } - public let rawProvider: RawADIProvider - public let deviceInfo: DeviceInfo - public let storage: KeyValueStorage + @Dependency(\.keyValueStorage) var storage + @Dependency(\.rawADIProvider) var rawProvider + @Dependency(\.httpClient) var httpClient - private let httpClient: HTTPClientProtocol - private let lookupManager: GrandSlamLookupManager + private let lookupManager = GrandSlamLookupManager() private let localUserUID: UUID private let localUserID: String private let _clientInfo = LockIsolated(nil) - public init( - rawProvider: RawADIProvider, - deviceInfo: DeviceInfo, - storage: KeyValueStorage, // ideally secure, eg keychain - provisioningData: ProvisioningData? = nil, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) throws { - self.rawProvider = rawProvider - self.deviceInfo = deviceInfo - self.storage = storage - - self.httpClient = httpFactory.makeClient() - self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory) - + public init(provisioningData: ProvisioningData? = nil) { + @Dependency(\.keyValueStorage) var storage if let provisioningData { self.localUserUID = provisioningData.localUserUID try? storage.setData(provisioningData.adiPb, forKey: Self.provisioningKey) try? storage.setString("\(provisioningData.routingInfo)", forKey: Self.routingInfoKey) - } else if let localUserUIDString = try storage.string(forKey: Self.localUserUIDKey), + } else if let localUserUIDString = try? storage.string(forKey: Self.localUserUIDKey), let localUserUID = UUID(uuidString: localUserUIDString) { self.localUserUID = localUserUID } else { let localUserUID = UUID() - try storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey) + try? storage.setString(localUserUID.uuidString, forKey: Self.localUserUIDKey) self.localUserUID = localUserUID } // localUserID = SHA256(local user UID) diff --git a/Sources/Supersign/GrandSlam/Anisette/AnisetteDataProvider.swift b/Sources/Supersign/GrandSlam/Anisette/AnisetteDataProvider.swift index 14fc1ed..d570c1a 100644 --- a/Sources/Supersign/GrandSlam/Anisette/AnisetteDataProvider.swift +++ b/Sources/Supersign/GrandSlam/Anisette/AnisetteDataProvider.swift @@ -7,6 +7,7 @@ // import Foundation +import Dependencies public protocol AnisetteDataProvider: Sendable { // This is a suggestion and not a requirement. @@ -24,5 +25,24 @@ public struct ProvisioningData: Hashable, Codable, Sendable { extension AnisetteDataProvider { public func provisioningData() -> ProvisioningData? { nil } - public func setProvisioningData(_ data: ProvisioningData) {} + public func resetProvisioning() async {} +} + +extension DependencyValues { + public var anisetteDataProvider: AnisetteDataProvider { + get { self[AnisetteDataProviderDependencyKey.self] } + set { self[AnisetteDataProviderDependencyKey.self] = newValue } + } +} + +public struct AnisetteDataProviderDependencyKey: DependencyKey { + public static let testValue: AnisetteDataProvider = UnimplementedAnisetteDataProvider() + public static let liveValue: AnisetteDataProvider = ADIDataProvider() +} + +private struct UnimplementedAnisetteDataProvider: AnisetteDataProvider { + func fetchAnisetteData() async throws -> AnisetteData { + let closure: () async throws -> AnisetteData = unimplemented() + return try await closure() + } } diff --git a/Sources/Supersign/GrandSlam/Anisette/DeviceInfo.swift b/Sources/Supersign/GrandSlam/Anisette/DeviceInfo.swift index 444c718..64a0fc9 100644 --- a/Sources/Supersign/GrandSlam/Anisette/DeviceInfo.swift +++ b/Sources/Supersign/GrandSlam/Anisette/DeviceInfo.swift @@ -7,6 +7,7 @@ // import Foundation +import Dependencies public struct DeviceInfo: Codable, Sendable { @@ -90,3 +91,20 @@ public struct DeviceInfo: Codable, Sendable { } } + +public struct DeviceInfoProvider: TestDependencyKey, Sendable { + public var fetch: @Sendable () throws -> DeviceInfo + + public init(fetch: @escaping @Sendable () throws -> DeviceInfo) { + self.fetch = fetch + } + + public static let testValue = DeviceInfoProvider(fetch: unimplemented()) +} + +extension DependencyValues { + public var deviceInfoProvider: DeviceInfoProvider { + get { self[DeviceInfoProvider.self] } + set { self[DeviceInfoProvider.self] = newValue } + } +} diff --git a/Sources/Supersign/GrandSlam/Anisette/OmnisetteADIProvider.swift b/Sources/Supersign/GrandSlam/Anisette/OmnisetteADIProvider.swift index 3d735af..197fef8 100644 --- a/Sources/Supersign/GrandSlam/Anisette/OmnisetteADIProvider.swift +++ b/Sources/Supersign/GrandSlam/Anisette/OmnisetteADIProvider.swift @@ -1,17 +1,17 @@ import Foundation +import Dependencies struct OmnisetteADIProvider: RawADIProvider { + @Dependency(\.httpClient) private var client + // should implement v3 of https://github.com/SideStore/omnisette-server // list: https://servers.sidestore.io/servers.json // e.g. https://ani.sidestore.io private let url: URL - private let client: HTTPClientProtocol init( - url: URL = URL(string: "https://ani.sidestore.io")!, // URL(string: "http://localhost:6969")!, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory + url: URL = URL(string: "https://ani.sidestore.io")! // URL(string: "http://localhost:6969")! ) { self.url = url - self.client = httpFactory.makeClient() } static let decoder: JSONDecoder = { @@ -182,27 +182,3 @@ extension UUID { withUnsafeBytes(of: uuid) { Data($0) } } } - -extension ADIDataProvider { - public static func adiProvider( - deviceInfo: DeviceInfo, - storage: KeyValueStorage, - provisioningData: ProvisioningData? = nil, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) throws -> ADIDataProvider { - #if os(Linux) - let provider = SupersetteADIProvider( - configDirectory: URL.homeDirectory.appending(path: ".config/Supercharge/Anisette") - ) - #else - let provider = OmnisetteADIProvider() - #endif - return try ADIDataProvider( - rawProvider: provider, - deviceInfo: deviceInfo, - storage: storage, - provisioningData: provisioningData, - httpFactory: httpFactory - ) - } -} diff --git a/Sources/Supersign/GrandSlam/Anisette/SupersetteADIProvider.swift b/Sources/Supersign/GrandSlam/Anisette/SupersetteADIProvider.swift index 7d4f9be..c88f56f 100644 --- a/Sources/Supersign/GrandSlam/Anisette/SupersetteADIProvider.swift +++ b/Sources/Supersign/GrandSlam/Anisette/SupersetteADIProvider.swift @@ -2,19 +2,15 @@ import Foundation import CSupersette +import Dependencies public actor SupersetteADIProvider: RawADIProvider { @MainActor private static var loadTask: Task? + @Dependency(\.httpClient) private var httpClient public let directory: URL - public let httpClient: HTTPClientProtocol - - public init( - configDirectory: URL, - httpClientFactory: HTTPClientFactory = defaultHTTPClientFactory - ) { - self.directory = configDirectory - self.httpClient = httpClientFactory.makeClient() + public init() { + self.directory = URL.homeDirectory.appending(path: ".config/Supercharge/Anisette") } private func _loadADI(id: UUID) async throws { diff --git a/Sources/Supersign/GrandSlam/GrandSlamClient.swift b/Sources/Supersign/GrandSlam/GrandSlamClient.swift index 7ddd39a..d9275cd 100644 --- a/Sources/Supersign/GrandSlam/GrandSlamClient.swift +++ b/Sources/Supersign/GrandSlam/GrandSlamClient.swift @@ -7,28 +7,23 @@ // import Foundation +import Dependencies -final class GrandSlamClient: Sendable { +struct GrandSlamClient: Sendable { private let encoder = PropertyListEncoder() - private let lookupManager: GrandSlamLookupManager + private let lookupManager = GrandSlamLookupManager() - let deviceInfo: DeviceInfo - let anisetteDataProvider: AnisetteDataProvider - private let httpClient: HTTPClientProtocol - init( - deviceInfo: DeviceInfo, - anisetteProvider: AnisetteDataProvider, - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) { - self.deviceInfo = deviceInfo - self.anisetteDataProvider = anisetteProvider - self.lookupManager = .init(deviceInfo: deviceInfo, httpFactory: httpFactory) - self.httpClient = httpFactory.makeClient() - } + @Dependency(\.deviceInfoProvider) var deviceInfoProvider + @Dependency(\.anisetteDataProvider) var anisetteDataProvider + @Dependency(\.httpClient) var httpClient + + init() {} func send(_ request: R) async throws -> R.Decoder.Value { + let deviceInfo = try deviceInfoProvider.fetch() + let anisetteData = try await anisetteDataProvider.fetchAnisetteData() let url = try await lookupManager.fetchURL(forEndpoint: R.endpoint) diff --git a/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift b/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift index 0ea3c24..ca735a7 100644 --- a/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift +++ b/Sources/Supersign/GrandSlam/Lookup/GrandSlamLookupManager.swift @@ -7,6 +7,7 @@ // import Foundation +import Dependencies actor GrandSlamLookupManager { @@ -19,14 +20,14 @@ actor GrandSlamLookupManager { private let decoder = PropertyListDecoder() private var endpoints: GrandSlamEndpoints? - let httpClient: HTTPClientProtocol - let deviceInfo: DeviceInfo - init(deviceInfo: DeviceInfo, httpFactory: HTTPClientFactory = defaultHTTPClientFactory) { - self.deviceInfo = deviceInfo - self.httpClient = httpFactory.makeClient() - } + @Dependency(\.deviceInfoProvider) var deviceInfoProvider + @Dependency(\.httpClient) var httpClient + + init() {} private func performLookup() async throws -> GrandSlamEndpoints { + let deviceInfo = try deviceInfoProvider.fetch() + /* { "X-Apple-I-Locale" = "en_IN"; "X-Apple-I-TimeZone" = "Asia/Kolkata"; diff --git a/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift b/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift index 45a6487..4026845 100644 --- a/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift +++ b/Sources/Supersign/HTTPClientProtocol/AsyncHTTPClient+HTTP.swift @@ -15,6 +15,22 @@ import NIOFoundationCompat import WebSocketKit import OpenAPIRuntime import OpenAPIAsyncHTTPClient +import Dependencies + +extension HTTPClientDependencyKey: DependencyKey { + public static let liveValue: HTTPClientProtocol = { + // if ssl cert parsing fails we're screwed so we might as well force try + // swiftlint:disable:next force_try + let appleRootCA = try! NIOSSLCertificate(bytes: Array(appleRootPEM.utf8), format: .pem) + var tlsConfiguration: TLSConfiguration = .makeClientConfiguration() + tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])] + let config = HTTPClient.Configuration( + tlsConfiguration: tlsConfiguration, + decompression: .enabled(limit: .none) + ) + return HTTPClient(configuration: config) + }() +} extension HTTPClient: HTTPClientProtocol { @discardableResult @@ -118,53 +134,34 @@ private final class WebSocketSessionWrapper: WebSocketSession { } } -final class AsyncHTTPClientFactory: HTTPClientFactory { - private static let appleRootPEM = """ - -----BEGIN CERTIFICATE----- - MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET - MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv - biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 - MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw - bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx - FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw - ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ - +FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 - XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w - tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW - q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM - aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E - BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 - R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE - ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 - d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl - IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 - YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj - b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp - Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc - NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP - y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 - R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg - xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP - IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX - UKqK1drk/NAJBzewdXUh - -----END CERTIFICATE----- - """ - - private let client: HTTPClient - private init() { - // if ssl cert parsing fails we're screwed so we might as well force try - // swiftlint:disable:next force_try - let appleRootCA = try! NIOSSLCertificate(bytes: Array(Self.appleRootPEM.utf8), format: .pem) - var tlsConfiguration: TLSConfiguration = .makeClientConfiguration() - tlsConfiguration.additionalTrustRoots = [.certificates([appleRootCA])] - let config = HTTPClient.Configuration( - tlsConfiguration: tlsConfiguration, - decompression: .enabled(limit: .none) - ) - client = HTTPClient(configuration: config) - } - static let shared = AsyncHTTPClientFactory() - - func makeClient() -> HTTPClientProtocol { client } -} +private let appleRootPEM = """ +-----BEGIN CERTIFICATE----- +MIIEuzCCA6OgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzET +MBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMDYwNDI1MjE0 +MDM2WhcNMzUwMjA5MjE0MDM2WjBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBw +bGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +FjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDkkakJH5HbHkdQ6wXtXnmELes2oldMVeyLGYne+Uts9QerIjAC6Bg+ ++FAJ039BqJj50cpmnCRrEdCju+QbKsMflZ56DKRHi1vUFjczy8QPTc4UadHJGXL1 +XQ7Vf1+b8iUDulWPTV0N8WQ1IxVLFVkds5T39pyez1C6wVhQZ48ItCD3y6wsIG9w +tj8BMIy3Q88PnT3zK0koGsj+zrW5DtleHNbLPbU6rfQPDgCSC7EhFi501TwN22IW +q6NxkkdTVcGvL0Gz+PvjcM3mo0xFfh9Ma1CWQYnEdGILEINBhzOKgbEwWOxaBDKM +aLOPHd5lc/9nXmW8Sdh2nzMUZaF3lMktAgMBAAGjggF6MIIBdjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUK9BpR5R2Cf70a40uQKb3 +R01/CF4wHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wggERBgNVHSAE +ggEIMIIBBDCCAQAGCSqGSIb3Y2QFATCB8jAqBggrBgEFBQcCARYeaHR0cHM6Ly93 +d3cuYXBwbGUuY29tL2FwcGxlY2EvMIHDBggrBgEFBQcCAjCBthqBs1JlbGlhbmNl +IG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0 +YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBj +b25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZp +Y2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMA0GCSqGSIb3DQEBBQUAA4IBAQBc +NplMLXi37Yyb3PN3m/J20ncwT8EfhYOFG5k9RzfyqZtAjizUsZAS2L70c5vu0mQP +y3lPNNiiPvl4/2vIB+x9OYOLUyDTOMSxv5pPCmv/K/xZpwUJfBdAVhEedNO3iyM7 +R6PVbyTi69G3cN8PReEnyvFteO3ntRcXqNx+IjXKJdXZD9Zr1KIkIxH3oayPc4Fg +xhtbCS+SsvhESPBgOJ4V9T0mZyCKM2r3DYLP3uujL/lTaltkwGMzd/c6ByxW69oP +IQ7aunMZT7XZNn/Bh1XZp5m5MkL72NVxnn6hUrcbvZNCJBIqxw8dtk2cXmPIS4AX +UKqK1drk/NAJBzewdXUh +-----END CERTIFICATE----- +""" #endif diff --git a/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift b/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift index 01ee097..79c25f8 100644 --- a/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift +++ b/Sources/Supersign/HTTPClientProtocol/HTTPClientProtocol.swift @@ -7,6 +7,8 @@ import Foundation import OpenAPIRuntime +import Dependencies +import HTTPTypes public struct HTTPRequest: Sendable { public enum Body: Sendable { @@ -57,6 +59,7 @@ public protocol HTTPClientProtocol: Sendable { _ request: HTTPRequest, onProgress: sending @isolated(any) (Double?) -> Void ) async throws -> HTTPResponse + func makeWebSocket(url: URL) async throws -> WebSocketSession } @@ -66,6 +69,44 @@ extension HTTPClientProtocol { } } +private struct UnimplementedHTTPClient: HTTPClientProtocol, ClientTransport { + public var asOpenAPITransport: ClientTransport { self } + + func send( + _ request: HTTPTypes.HTTPRequest, + body: OpenAPIRuntime.HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { + let closure: (HTTPTypes.HTTPRequest, OpenAPIRuntime.HTTPBody?, URL, String) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) = unimplemented() + return try await closure(request, body, baseURL, operationID) + } + + func makeRequest( + _ request: HTTPRequest, + onProgress: sending @isolated(any) (Double?) -> Void + ) async throws -> HTTPResponse { + let closure: () throws -> HTTPResponse = unimplemented() + return try closure() + } + + public func makeWebSocket(url: URL) async throws -> any WebSocketSession { + let closure: (URL) async throws -> any WebSocketSession = unimplemented() + return try await closure(url) + } +} + +public enum HTTPClientDependencyKey: TestDependencyKey { + public static let testValue: HTTPClientProtocol = UnimplementedHTTPClient() +} + +extension DependencyValues { + public var httpClient: HTTPClientProtocol { + get { self[HTTPClientDependencyKey.self] } + set { self[HTTPClientDependencyKey.self] = newValue } + } +} + public protocol WebSocketSession: Sendable { func receive() async throws -> WebSocketMessage func send(_ message: WebSocketMessage) async throws @@ -76,15 +117,3 @@ public enum WebSocketMessage: Sendable { case text(String) case data(Data) } - -public protocol HTTPClientFactory: Sendable { - func makeClient() -> HTTPClientProtocol -} - -public let defaultHTTPClientFactory: HTTPClientFactory = { - #if os(Linux) - return AsyncHTTPClientFactory.shared - #else - return URLHTTPClientFactory.shared - #endif -}() diff --git a/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift b/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift index dbd0362..781bcc4 100644 --- a/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift +++ b/Sources/Supersign/HTTPClientProtocol/URLSession+HTTP.swift @@ -10,15 +10,12 @@ import Foundation import ConcurrencyExtras import OpenAPIRuntime import OpenAPIURLSession +import Dependencies public struct UnknownHTTPError: Error {} -final class URLHTTPClientFactory: HTTPClientFactory { - static let shared = URLHTTPClientFactory() - - private let client = Client() - - func makeClient() -> HTTPClientProtocol { client } +extension HTTPClientDependencyKey: DependencyKey { + public static let liveValue: HTTPClientProtocol = Client() } private final class Client: HTTPClientProtocol { diff --git a/Sources/Supersign/Integration/SuperchargeInstaller.swift b/Sources/Supersign/Integration/SuperchargeInstaller.swift index b858e07..6cea107 100644 --- a/Sources/Supersign/Integration/SuperchargeInstaller.swift +++ b/Sources/Supersign/Integration/SuperchargeInstaller.swift @@ -9,6 +9,7 @@ import Foundation import SwiftyMobileDevice import ConcurrencyExtras +import Dependencies extension LockdownClient { static let installerLabel = "supersign" @@ -17,39 +18,11 @@ extension LockdownClient { #if !os(iOS) public protocol IntegratedInstallerDelegate: AnyObject, Sendable { - func setPresentedMessage(_ message: IntegratedInstaller.Message?) func installerDidUpdate(toStage stage: String, progress: Double?) // defaults to always returning true func confirmRevocation(of certificates: [DeveloperServicesCertificate]) async -> Bool - - /// Decompress the zipped ipa file - /// - /// - Parameter ipa: The `ipa` file to decompress. - /// - Parameter directory: The directory into which `ipa` should be decompressed. - /// - Parameter progress: A closure to which the callee can provide progress updates. - /// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate. - func decompress( - ipa: URL, - in directory: URL, - progress: @escaping @Sendable (_ currentProgress: Double?) -> Void - ) async throws - - // `compress` is required because the only way to upload symlinks via afc is by - // putting them in a zip archive (afc_make_symlink was disabled due to security or - // something) - - /// Compress the app before installation. - /// - /// - Parameter payloadDir: The `Payload` directory which is to be compressed. - /// - Parameter progress: A closure to which the callee can provide progress updates. - /// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate. - func compress( - payloadDir: URL, - progress: @escaping @Sendable (_ currentProgress: Double?) -> Void - ) async throws -> URL - } extension IntegratedInstallerDelegate { @@ -83,9 +56,10 @@ public actor IntegratedInstaller { let lookupMode: LookupMode let auth: DeveloperAPIAuthData let configureDevice: Bool - let storage: KeyValueStorage public weak var delegate: IntegratedInstallerDelegate? + @Dependency(\.zipCompressor) private var compressor + private var appInstaller: AppInstaller? private let tempDir = FileManager.default.temporaryDirectoryShim @@ -142,14 +116,12 @@ public actor IntegratedInstaller { lookupMode: LookupMode, auth: DeveloperAPIAuthData, configureDevice: Bool, - storage: KeyValueStorage, delegate: IntegratedInstallerDelegate ) { self.udid = udid self.lookupMode = lookupMode self.auth = auth self.configureDevice = configureDevice - self.storage = storage self.delegate = delegate } @@ -251,8 +223,8 @@ public actor IntegratedInstaller { switch app.pathExtension { case "ipa": - try await delegate?.decompress( - ipa: app, + try await compressor.decompress( + file: app, in: tempDir, progress: { progress in self.queueUpdateTask { @@ -305,16 +277,10 @@ public actor IntegratedInstaller { try await updateProgress(to: 1) -// guard let deviceInfo = DeviceInfo.current() else { -// throw Error.deviceInfoFetchFailed -// } -// let anisetteProvider = try ADIDataProvider.adiProvider(deviceInfo: deviceInfo, storage: storage) - let context = try SigningContext( udid: udid, deviceName: deviceName, - auth: auth, - signingInfoManager: KeyValueSigningInfoManager(storage: storage) + auth: auth ) let signer = Signer(context: context) { certs in @@ -335,26 +301,13 @@ public actor IntegratedInstaller { didProvision: { @Sendable [self] in // TODO: reintroduce Superconfig _ = self -// if let pairingKeys = pairingKeys { -// let info = try context.signingInfoManager.info(forTeamID: context.teamID) -// try Superconfig( -// udid: udid, -// pairingKeys: pairingKeys, -// deviceInfo: deviceInfo, -// preferredTeamID: teamID.rawValue, -// preferredSigningInfo: info, -// appleID: appleID, -// provisioningData: anisetteProvider.provisioningData(), -// token: token -// ).save(inAppDir: appDir) -// } } ) try await self.updateStage(to: "Packaging", initialProgress: nil) - let ipa = try await delegate?.compress( - payloadDir: appDir.deletingLastPathComponent(), + let ipa = try await compressor.compress( + directory: appDir.deletingLastPathComponent(), progress: { progress in self.queueUpdateTask { $0.updateProgressIgnoringCancellation(to: progress) @@ -364,7 +317,7 @@ public actor IntegratedInstaller { try await self.updateProgress(to: 1) - let appInstaller = AppInstaller(ipa: ipa!, udid: udid, connectionPreferences: .init(lookupMode: lookupMode)) + let appInstaller = AppInstaller(ipa: ipa, udid: udid, connectionPreferences: .init(lookupMode: lookupMode)) self.appInstaller = appInstaller try await appInstaller.install( progress: { stage in diff --git a/Sources/Supersign/Signer/SigningContext.swift b/Sources/Supersign/Signer/SigningContext.swift index aa82051..29d3d6c 100644 --- a/Sources/Supersign/Signer/SigningContext.swift +++ b/Sources/Supersign/Signer/SigningContext.swift @@ -13,10 +13,7 @@ public struct SigningContext: Sendable { public let udid: String public let deviceName: String - public let auth: DeveloperAPIAuthData - - public let signingInfoManager: SigningInfoManager public let signerImpl: SignerImpl public var developerAPIClient: DeveloperAPIClient { @@ -27,13 +24,11 @@ public struct SigningContext: Sendable { udid: String, deviceName: String, auth: DeveloperAPIAuthData, - signingInfoManager: SigningInfoManager, signerImpl: SignerImpl? = nil ) throws { self.udid = udid self.deviceName = deviceName self.auth = auth - self.signingInfoManager = signingInfoManager self.signerImpl = try signerImpl ?? .first() } diff --git a/Sources/Supersign/Utilities/KeyValueStorage.swift b/Sources/Supersign/Utilities/KeyValueStorage.swift index c3d2717..d3a53f9 100644 --- a/Sources/Supersign/Utilities/KeyValueStorage.swift +++ b/Sources/Supersign/Utilities/KeyValueStorage.swift @@ -8,6 +8,7 @@ import Foundation import ConcurrencyExtras +import Dependencies public enum KeyValueStorageError: Error { case stringConversionFailure @@ -21,6 +22,17 @@ public protocol KeyValueStorage: Sendable { func setString(_ string: String?, forKey key: String) throws } +public enum KeyValueStorageDependencyKey: TestDependencyKey { + public static let testValue: KeyValueStorage = UnimplementedKeyValueStorage() +} + +extension DependencyValues { + public var keyValueStorage: KeyValueStorage { + get { self[KeyValueStorageDependencyKey.self] } + set { self[KeyValueStorageDependencyKey.self] = newValue } + } +} + extension KeyValueStorage { public func string(forKey key: String) throws -> String? { try data(forKey: key).map { @@ -44,6 +56,16 @@ extension KeyValueStorage { } } +private struct UnimplementedKeyValueStorage: KeyValueStorage { + func data(forKey key: String) throws -> Data? { + unimplemented(placeholder: nil) + } + + func setData(_ data: Data?, forKey key: String) throws { + unimplemented() + } +} + public final class MemoryKeyValueStorage: KeyValueStorage { private let dict = LockIsolated<[String: Data]>([:]) diff --git a/Sources/Supersign/Utilities/SigningInfo.swift b/Sources/Supersign/Utilities/SigningInfo.swift index 8bdab12..b4b4dd2 100644 --- a/Sources/Supersign/Utilities/SigningInfo.swift +++ b/Sources/Supersign/Utilities/SigningInfo.swift @@ -8,6 +8,7 @@ import Foundation import ConcurrencyExtras +import Dependencies public struct SigningInfo: Codable, Sendable { public let privateKey: PrivateKey @@ -19,6 +20,18 @@ public protocol SigningInfoManager: Sendable { func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws } +public enum SigningInfoManagerDependencyKey: DependencyKey { + public static let testValue: SigningInfoManager = UnimplementedSigningInfoManager() + public static let liveValue: SigningInfoManager = KeyValueSigningInfoManager() +} + +extension DependencyValues { + public var signingInfoManager: SigningInfoManager { + get { self[SigningInfoManagerDependencyKey.self] } + set { self[SigningInfoManagerDependencyKey.self] = newValue } + } +} + extension SigningInfoManager { subscript(identityID: String) -> SigningInfo? { get { @@ -30,6 +43,16 @@ extension SigningInfoManager { } } +private struct UnimplementedSigningInfoManager: SigningInfoManager { + func info(forIdentityID identityID: String) throws -> SigningInfo? { + unimplemented(placeholder: nil) + } + + func setInfo(_ info: SigningInfo?, forIdentityID identityID: String) throws { + unimplemented() + } +} + public final class MemoryBackedSigningInfoManager: SigningInfoManager { private let infos = LockIsolated<[String: SigningInfo]>([:]) @@ -50,10 +73,7 @@ public struct KeyValueSigningInfoManager: SigningInfoManager { private let encoder = PropertyListEncoder() private let decoder = PropertyListDecoder() - public let storage: KeyValueStorage - public init(storage: KeyValueStorage) { - self.storage = storage - } + @Dependency(\.keyValueStorage) var storage public func info(forIdentityID identityID: String) throws -> SigningInfo? { guard let data = try storage.data(forKey: identityID) diff --git a/Sources/Supersign/Utilities/ZIPCompressor.swift b/Sources/Supersign/Utilities/ZIPCompressor.swift new file mode 100644 index 0000000..1178783 --- /dev/null +++ b/Sources/Supersign/Utilities/ZIPCompressor.swift @@ -0,0 +1,65 @@ +import Foundation +import Dependencies + +public struct ZIPCompressor: TestDependencyKey, Sendable { + public var compress: @Sendable ( + _ directory: URL, + _ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void + ) async throws -> URL + + public var decompress: @Sendable ( + _ file: URL, + _ directory: URL, + _ progress: @escaping @Sendable (_ currentProgress: Double?) -> Void + ) async throws -> Void + + public init( + compress: @Sendable @escaping (URL, @Sendable @escaping (Double?) -> Void) async throws -> URL, + decompress: @Sendable @escaping (URL, URL, @Sendable @escaping (Double?) -> Void) async throws -> Void + ) { + self.decompress = decompress + self.compress = compress + } + + public static let testValue = ZIPCompressor( + compress: unimplemented(), + decompress: unimplemented() + ) + + /// Decompress the zipped ipa file + /// + /// - Parameter ipa: The `ipa` file to decompress. + /// - Parameter directory: The directory into which `ipa` should be decompressed. + /// - Parameter progress: A closure to which the callee can provide progress updates. + /// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate. + public func decompress( + file: URL, + in directory: URL, + progress: @escaping @Sendable (_ currentProgress: Double?) -> Void + ) async throws { + try await decompress(file, directory, progress) + } + + // `compress` is required because the only way to upload symlinks via afc is by + // putting them in a zip archive (afc_make_symlink was disabled due to security or + // something) + + /// Compress the app before installation. + /// + /// - Parameter payloadDir: The `Payload` directory which is to be compressed. + /// - Parameter progress: A closure to which the callee can provide progress updates. + /// - Parameter currentProgress: The current progress, or `nil` to indicate it is indeterminate. + public func compress( + directory: URL, + progress: @escaping @Sendable (_ currentProgress: Double?) -> Void + ) async throws -> URL { + try await compress(directory, progress) + } +} + +extension DependencyValues { + public var zipCompressor: ZIPCompressor { + get { self[ZIPCompressor.self] } + set { self[ZIPCompressor.self] = newValue } + } +} diff --git a/Sources/SupersignCLI/SupersignCLI.swift b/Sources/SupersignCLI/SupersignCLI.swift new file mode 100644 index 0000000..43052a4 --- /dev/null +++ b/Sources/SupersignCLI/SupersignCLI.swift @@ -0,0 +1,28 @@ +import Foundation +import Supersign +import SupersignCLISupport +import Dependencies + +@main enum SupersignCLIMain { + static func main() async throws { + try await SupersignCLI.run() + } +} + +#warning("Improve persistence mechanism") + +// for macOS, we can use KeychainStorage but we need to sign with entitlements. +// for Windows, we could use dpapi.h or wincred.h. +// for Linux, maybe use libsecret? +// see https://github.com/atom/node-keytar +extension KeyValueStorageDependencyKey: DependencyKey { + public static let liveValue: KeyValueStorage = { +// #if os(macOS) +// KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials") +// #else + DirectoryStorage( + base: URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data") + ) +// #endif + }() +} diff --git a/Sources/SupersignCLI/main.swift b/Sources/SupersignCLI/main.swift deleted file mode 100644 index f38766c..0000000 --- a/Sources/SupersignCLI/main.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import Supersign -import SupersignCLISupport - -// let app = Bundle.module.url(forResource: "Supercharge", withExtension: "ipa")! - -#warning("Improve persistence mechanism") - -// for macOS, we can use KeychainStorage but we need to sign with entitlements. -// for Windows, we could use dpapi.h or wincred.h. -// for Linux, maybe use libsecret? -// see https://github.com/atom/node-keytar - -let storage: KeyValueStorage -// #if os(macOS) -// storage = KeychainStorage(service: "com.kabiroberai.Supercharge-Keychain.credentials") -// #else -let directory = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/Supercharge/data") -storage = DirectoryStorage(base: directory) -// #endif - -try await SupersignCLI.run(configuration: SupersignCLI.Configuration( - superchargeApp: nil, - storage: storage -)) diff --git a/Sources/SupersignCLISupport/AuthCommand.swift b/Sources/SupersignCLISupport/AuthCommand.swift index 8a422a2..47089cc 100644 --- a/Sources/SupersignCLISupport/AuthCommand.swift +++ b/Sources/SupersignCLISupport/AuthCommand.swift @@ -2,6 +2,7 @@ import Foundation import Supersign import ArgumentParser import Crypto +import Dependencies enum AuthMode: String, CaseIterable, CustomStringConvertible, ExpressibleByArgument { case key @@ -86,28 +87,15 @@ struct AuthOperation { print("Logging in...") - let deviceInfo = try DeviceInfo.fetch() - - let provider = try ADIDataProvider.adiProvider( - deviceInfo: deviceInfo, - storage: SupersignCLI.config.storage - ) let authDelegate = SupersignCLIAuthDelegate() - let manager = try DeveloperServicesLoginManager( - deviceInfo: deviceInfo, - anisetteProvider: provider - ) + let manager = DeveloperServicesLoginManager() let token = try await manager.logIn( withUsername: username, password: password, twoFactorDelegate: authDelegate ) - let client = DeveloperServicesClient( - loginToken: token, - deviceInfo: deviceInfo, - anisetteProvider: provider - ) + let client = DeveloperServicesClient(loginToken: token) let teams = try await client.send(DeveloperServicesListTeamsRequest()) let team = try await Console.choose( from: teams, @@ -179,11 +167,8 @@ struct AuthLogoutCommand: AsyncParsableCommand { } if reset2FA { - try ADIDataProvider.adiProvider( - deviceInfo: .fetch(), - storage: SupersignCLI.config.storage - ) - .resetProvisioning() + @Dependency(\.anisetteDataProvider) var anisetteProvider + await anisetteProvider.resetProvisioning() print("Forgot device") } } @@ -225,3 +210,8 @@ extension DeviceInfo { return deviceInfo } } + +extension DeviceInfoProvider: DependencyKey { + private static let current = Result { try DeviceInfo.fetch() } + public static let liveValue = DeviceInfoProvider { try current.get() } +} diff --git a/Sources/SupersignCLISupport/AuthToken.swift b/Sources/SupersignCLISupport/AuthToken.swift index 8a50544..ee9f7ef 100644 --- a/Sources/SupersignCLISupport/AuthToken.swift +++ b/Sources/SupersignCLISupport/AuthToken.swift @@ -1,5 +1,6 @@ import Foundation import Supersign +import Dependencies enum AuthToken: Codable, CustomStringConvertible { struct Xcode: Codable { @@ -38,23 +39,28 @@ enum AuthToken: Codable, CustomStringConvertible { extension AuthToken { + private static var storage: KeyValueStorage { + @Dependency(\.keyValueStorage) var storage + return storage + } + private static let encoder = JSONEncoder() private static let decoder = JSONDecoder() static func saved() throws -> Self { - guard let data = try SupersignCLI.config.storage.data(forKey: "SUPAuthToken") else { + guard let data = try storage.data(forKey: "SUPAuthToken") else { throw Console.Error("Please log in with `supersign ds login` before running this command.") } return try decoder.decode(AuthToken.self, from: data) } static func clear() throws { - try SupersignCLI.config.storage.setData(nil, forKey: "SUPAuthToken") + try Self.storage.setData(nil, forKey: "SUPAuthToken") } func save() throws { let data = try Self.encoder.encode(self) - try SupersignCLI.config.storage.setData(data, forKey: "SUPAuthToken") + try Self.storage.setData(data, forKey: "SUPAuthToken") } func authData() throws -> DeveloperAPIAuthData { @@ -62,19 +68,13 @@ extension AuthToken { case .appStoreConnect(let data): return .appStoreConnect(.init(id: data.id, issuerID: data.issuerID, pem: data.pem)) case .xcode(let data): - let deviceInfo = try DeviceInfo.fetch() return .xcode(.init( loginToken: DeveloperServicesLoginToken( adsid: data.adsid, token: data.token, expiry: data.expiry ), - deviceInfo: deviceInfo, - teamID: .init(rawValue: data.teamID), - anisetteDataProvider: try ADIDataProvider.adiProvider( - deviceInfo: deviceInfo, - storage: SupersignCLI.config.storage - ) + teamID: .init(rawValue: data.teamID) )) } } diff --git a/Sources/SupersignCLISupport/DSCommand.swift b/Sources/SupersignCLISupport/DSCommand.swift index 4a6be52..ca9b7be 100644 --- a/Sources/SupersignCLISupport/DSCommand.swift +++ b/Sources/SupersignCLISupport/DSCommand.swift @@ -2,6 +2,8 @@ import Foundation import Supersign import ArgumentParser import DeveloperAPI +import OpenAPIRuntime +import Dependencies struct DSTeamsListCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( @@ -62,13 +64,12 @@ struct DSAnisetteCommand: AsyncParsableCommand { ) func run() async throws { - // swiftlint:disable:next force_try - let res = try! await ADIDataProvider( - rawProvider: Provider(), - deviceInfo: .current()!, - storage: SupersignCLI.config.storage - ).fetchAnisetteData() - + let provider = withDependencies { + $0.rawADIProvider = Provider() + } operation: { + ADIDataProvider() + } + let res = try await provider.fetchAnisetteData() print(res) } } diff --git a/Sources/SupersignCLISupport/DevCommand.swift b/Sources/SupersignCLISupport/DevCommand.swift index b4e6a8f..4a51f3a 100644 --- a/Sources/SupersignCLISupport/DevCommand.swift +++ b/Sources/SupersignCLISupport/DevCommand.swift @@ -110,7 +110,6 @@ struct DevRunCommand: AsyncParsableCommand { lookupMode: .only(client.connectionType), auth: try token.authData(), configureDevice: false, - storage: SupersignCLI.config.storage, delegate: installDelegate ) diff --git a/Sources/SupersignCLISupport/DevSDKCommand.swift b/Sources/SupersignCLISupport/DevSDKCommand.swift index 9eac443..43c6fc2 100644 --- a/Sources/SupersignCLISupport/DevSDKCommand.swift +++ b/Sources/SupersignCLISupport/DevSDKCommand.swift @@ -2,9 +2,7 @@ import Foundation import Supersign import Version import ArgumentParser -#if os(Linux) -import FoundationNetworking -#endif +import Dependencies struct DevSDKCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( @@ -77,10 +75,9 @@ struct DarwinSDKVersions: Decodable { var current: String var metadata: [String: Metadata] - static func all( - httpFactory: HTTPClientFactory = defaultHTTPClientFactory - ) async throws -> DarwinSDKVersions { - let data = try await httpFactory.makeClient().makeRequest(HTTPRequest(url: url)).body ?? Data() + static func all() async throws -> DarwinSDKVersions { + @Dependency(\.httpClient) var httpClient + let data = try await httpClient.makeRequest(HTTPRequest(url: url)).body ?? Data() return try decoder.decode(self, from: data) } } diff --git a/Sources/SupersignCLISupport/InstallCommand.swift b/Sources/SupersignCLISupport/InstallCommand.swift index b98b244..d93e0f1 100644 --- a/Sources/SupersignCLISupport/InstallCommand.swift +++ b/Sources/SupersignCLISupport/InstallCommand.swift @@ -28,7 +28,6 @@ struct InstallCommand: AsyncParsableCommand { lookupMode: .only(client.connectionType), auth: try token.authData(), configureDevice: false, - storage: SupersignCLI.config.storage, delegate: installDelegate ) diff --git a/Sources/SupersignCLISupport/ProcessZIPCompressor.swift b/Sources/SupersignCLISupport/ProcessZIPCompressor.swift new file mode 100644 index 0000000..8c82b04 --- /dev/null +++ b/Sources/SupersignCLISupport/ProcessZIPCompressor.swift @@ -0,0 +1,50 @@ +// +// File.swift +// Supersign +// +// Created by Kabir Oberai on 23/12/24. +// + +import Foundation +import Supersign +import Dependencies + +extension ZIPCompressor: DependencyKey { + // TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows + + public static let liveValue = ZIPCompressor( + compress: { dir, progress in + progress(nil) + + let dest = dir.deletingLastPathComponent().appendingPathComponent("app.ipa") + + let zip = Process() + zip.executableURL = URL(fileURLWithPath: "/usr/bin/env") + zip.currentDirectoryURL = dir.deletingLastPathComponent() + zip.arguments = ["zip", "-yqru0", dest.path, "Payload"] + try zip.run() + await zip.waitForExit() + guard zip.terminationStatus == 0 else { + throw ZIPCompressorError.compressionFailed + } + + return dest + }, + decompress: { ipa, directory, progress in + progress(nil) + let unzip = Process() + unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env") + unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path] + try unzip.run() + await unzip.waitForExit() + guard unzip.terminationStatus == 0 else { + throw ZIPCompressorError.decompressionFailed + } + } + ) +} + +enum ZIPCompressorError: Error { + case compressionFailed + case decompressionFailed +} diff --git a/Sources/SupersignCLISupport/SupersignCLIDelegate.swift b/Sources/SupersignCLISupport/SupersignCLIDelegate.swift index b7a9214..d5b96a7 100644 --- a/Sources/SupersignCLISupport/SupersignCLIDelegate.swift +++ b/Sources/SupersignCLISupport/SupersignCLIDelegate.swift @@ -9,11 +9,6 @@ final class SupersignCLIAuthDelegate: TwoFactorAuthDelegate { } actor SupersignCLIDelegate: IntegratedInstallerDelegate { - public enum Error: Swift.Error { - case decompressionFailed - case compressionFailed - } - init() {} private let updateTask = LockIsolated?>(nil) @@ -96,57 +91,4 @@ actor SupersignCLIDelegate: IntegratedInstallerDelegate { return false } } - - // TODO: Use `powershell Compress-Archive` and `powershell Expand-Archive` on Windows - - func decompress( - ipa: URL, - in directory: URL, - progress: @escaping (Double?) -> Void - ) async throws { - progress(nil) - - let unzip = Process() - unzip.executableURL = URL(fileURLWithPath: "/usr/bin/env") - unzip.arguments = ["unzip", "-q", ipa.path, "-d", directory.path] - try await unzip.launchAndWait() - guard unzip.terminationStatus == 0 else { - throw Error.decompressionFailed - } - } - - func compress( - payloadDir: URL, - progress: @escaping (Double?) -> Void - ) async throws -> URL { - progress(nil) - - let dest = payloadDir.deletingLastPathComponent().appendingPathComponent("app.ipa") - - let zip = Process() - zip.executableURL = URL(fileURLWithPath: "/usr/bin/env") - zip.currentDirectoryURL = payloadDir.deletingLastPathComponent() - zip.arguments = ["zip", "-yqru0", dest.path, "Payload"] - try await zip.launchAndWait() - guard zip.terminationStatus == 0 else { throw Error.compressionFailed } - - return dest - } -} - -extension Process { - fileprivate func launchAndWait() async throws { - try await withCheckedThrowingContinuation { continuation in - terminationHandler = { _ in - continuation.resume() - } - do { - try self.run() - } catch { - continuation.resume(throwing: error) - return - } - Task.detached { self.waitUntilExit() } - } - } } diff --git a/Sources/SupersignCLISupport/SupersignCommand.swift b/Sources/SupersignCLISupport/SupersignCommand.swift index e937fed..8e23be9 100644 --- a/Sources/SupersignCLISupport/SupersignCommand.swift +++ b/Sources/SupersignCLISupport/SupersignCommand.swift @@ -3,24 +3,7 @@ import Supersign import ArgumentParser public enum SupersignCLI { - public struct Configuration: Sendable { - public let superchargeApp: URL? - public let storage: KeyValueStorage - - public init( - superchargeApp: URL?, - storage: KeyValueStorage - ) { - self.superchargeApp = superchargeApp - self.storage = storage - } - } - - private static nonisolated(unsafe) var _config: Configuration! - static var config: Configuration { _config } - - public static func run(configuration: Configuration, arguments: [String]? = nil) async throws { - _config = configuration + public static func run(arguments: [String]? = nil) async throws { await SupersignCommand.cancellableMain(arguments) } }