Merge branch 'develop' into users/junepark678/altsign-fixes

This commit is contained in:
Spidy123222
2025-01-20 22:27:31 -08:00
committed by GitHub
61 changed files with 3642 additions and 950 deletions

View File

@@ -715,9 +715,10 @@ private extension AuthenticationOperation
// If we're not using the same certificate used to install AltStore, warn user that they need to refresh.
guard !provisioningProfile.certificates.contains(signer.certificate) else { return completionHandler(false) }
#if DEBUG
completionHandler(false)
#else
// #if DEBUG && targetEnvironment(simulator)
// completionHandler(false)
// #else
DispatchQueue.main.async {
let context = AuthenticatedOperationContext(context: self.context)
context.operations.removeAllObjects() // Prevent deadlock due to endless waiting on previous operations to finish.
@@ -733,7 +734,7 @@ private extension AuthenticationOperation
completionHandler(false)
}
}
#endif
// #endif
}
}

View File

@@ -103,7 +103,14 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result<Inst
target_minimuxer_address()
let documentsDirectory = FileManager.default.documentsDirectory.absoluteString
do {
try minimuxer.start(try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")), documentsDirectory)
// enable minimuxer console logging only if enabled in settings
let isMinimuxerConsoleLoggingEnabled = UserDefaults.standard.isMinimuxerConsoleLoggingEnabled
try minimuxer.startWithLogger(
try String(contentsOf: FileManager.default.documentsDirectory.appendingPathComponent("\(pairingFileName)")),
documentsDirectory,
isMinimuxerConsoleLoggingEnabled
)
} catch {
self.finish(.failure(error))
}

View File

@@ -53,38 +53,37 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
guard let installedApp = self.context.installedApp else {
return self.finish(.failure(OperationError.invalidParameters("EnableJITOperation.main: self.context.installedApp is nil")))
}
if #available(iOS 17, *) {
let sideJITenabled = UserDefaults.standard.sidejitenable
let SideJITIP = UserDefaults.standard.textInputSideJITServerurl ?? ""
if sideJITenabled {
installedApp.managedObjectContext?.perform {
EnableJITSideJITServer(serverurl: SideJITIP, installedapp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
let userdefaults = UserDefaults.standard
if #available(iOS 17, *), userdefaults.sidejitenable {
let SideJITIP = userdefaults.textInputSideJITServerurl ?? "http://sidejitserver._http._tcp.local:8080"
installedApp.managedObjectContext?.perform {
enableJITSideJITServer(serverURL: URL(string: SideJITIP)!, installedApp: installedApp) { result in
switch result {
case .failure(let error):
switch error {
case .invalidURL, .errorConnecting:
self.finish(.failure(OperationError.unableToConnectSideJIT))
case .deviceNotFound:
self.finish(.failure(OperationError.unableToRespondSideJITDevice))
case .other(let message):
if let startRange = message.range(of: "<p>"),
let endRange = message.range(of: "</p>", range: startRange.upperBound..<message.endIndex) {
let pContent = message[startRange.upperBound..<endRange.lowerBound]
self.finish(.failure(OperationError.SideJITIssue(error: String(pContent))))
print(message + " + " + String(pContent))
} else {
print(message)
self.finish(.failure(OperationError.SideJITIssue(error: message)))
}
case .success():
self.finish(.success(()))
print("Thank you for using this, it was made by Stossy11 and tested by trolley or sniper1239408")
}
case .success():
self.finish(.success(()))
print("JIT Enabled Successfully :3 (code made by Stossy11!)")
}
return
}
return
}
} else {
installedApp.managedObjectContext?.perform {
@@ -107,48 +106,40 @@ final class EnableJITOperation<Context: EnableJITContext>: ResultOperation<Void>
}
@available(iOS 17, *)
func EnableJITSideJITServer(serverurl: String, installedapp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
func enableJITSideJITServer(serverURL: URL, installedApp: InstalledApp, completion: @escaping (Result<Void, SideJITServerErrorType>) -> Void) {
guard let udid = fetch_udid()?.toString() else {
completion(.failure(.other("Unable to get UDID")))
return
}
var SJSURL = serverurl
let serverURLWithUDID = serverURL.appendingPathComponent(udid)
let fullURL = serverURLWithUDID.appendingPathComponent(installedApp.resignedBundleIdentifier)
if (UserDefaults.standard.textInputSideJITServerurl ?? "").isEmpty {
SJSURL = "http://sidejitserver._http._tcp.local:8080"
}
if !SJSURL.hasPrefix("http") {
completion(.failure(.invalidURL))
return
}
let fullurl = SJSURL + "/\(udid)/" + installedapp.resignedBundleIdentifier
let url = URL(string: fullurl)!
let task = URLSession.shared.dataTask(with: url) {(data, response, error) in
let task = URLSession.shared.dataTask(with: fullURL) { (data, response, error) in
if let error = error {
completion(.failure(.errorConnecting))
return
}
guard let data = data, let datastring = String(data: data, encoding: .utf8) else { return }
guard let data = data, let dataString = String(data: data, encoding: .utf8) else {
return
}
if datastring == "Enabled JIT for '\(installedapp.name)'!" {
if dataString == "Enabled JIT for '\(installedApp.name)'!" {
let content = UNMutableNotificationContent()
content.title = "JIT Successfully Enabled"
content.subtitle = "JIT Enabled For \(installedapp.name)"
content.sound = UNNotificationSound.default
content.subtitle = "JIT Enabled For \(installedApp.name)"
content.sound = .default
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
let request = UNNotificationRequest(identifier: "EnabledJIT", content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
completion(.success(()))
} else {
let errorType: SideJITServerErrorType = datastring == "Could not find device!" ? .deviceNotFound : .other(datastring)
let errorType: SideJITServerErrorType = dataString == "Could not find device!"
? .deviceNotFound
: .other(dataString)
completion(.failure(errorType))
}
}

View File

@@ -58,6 +58,8 @@ extension OperationError
case anisetteV3Error//(message: String)
case cacheClearError//(errors: [String])
case noWiFi
case invalidOperationContext
}
static var cancelled: CancellationError { CancellationError() }
@@ -130,6 +132,10 @@ extension OperationError
OperationError(code: .invalidParameters, failureReason: message)
}
static func invalidOperationContext(_ message: String? = nil) -> OperationError {
OperationError(code: .invalidOperationContext, failureReason: message)
}
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
}
@@ -232,7 +238,10 @@ struct OperationError: ALTLocalizedError {
case .invalidParameters:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
return String(format: NSLocalizedString("Invalid parameters\n%@", comment: ""), message)
case .invalidOperationContext:
let message = self._failureReason.map { ": \n\($0)" } ?? "."
return String(format: NSLocalizedString("Invalid Operation Context\n%@", comment: ""), message)
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")

View File

@@ -14,6 +14,8 @@ import AltStoreCore
import AltSign
import Roxas
class ANISETTE_VERBOSITY: Operation {} // dummy tag iface
@objc(FetchAnisetteDataOperation)
final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSocketDelegate
{
@@ -58,7 +60,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
UserDefaults.standard.menuAnisetteURL = urlString
let url = URL(string: urlString)
self.url = url
print("Anisette URL: \(self.url!.absoluteString)")
self.printOut("Anisette URL: \(self.url!.absoluteString)")
if let identifier = Keychain.shared.identifier,
let adiPb = Keychain.shared.adiPb {
@@ -107,7 +109,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
guard let url = URL(string: currentServerUrlString) else {
// Invalid URL, skip to next
let errmsg = "Skipping invalid URL: \(currentServerUrlString)"
print(errmsg)
self.printOut(errmsg)
showToast(viewContext: viewContext, message: errmsg)
tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
return
@@ -118,7 +120,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if success {
// If the server is reachable, return the URL
let okmsg = "Found working server: \(url.absoluteString)"
print(okmsg)
self.printOut(okmsg)
if(currentIndex > 0){
// notify user if available server is different the user-specified one
self.showToast(viewContext: viewContext, message: okmsg)
@@ -127,7 +129,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
} else {
// If not, try the next URL
let errmsg = "Failed to reach server: \(url.absoluteString), trying next server."
print(errmsg)
self.printOut(errmsg)
self.showToast(viewContext: viewContext, message: errmsg)
self.tryNextServer(from: serverUrls, viewContext, currentIndex: currentIndex + 1, completion: completion)
}
@@ -170,10 +172,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if v3 {
if json["result"] == "GetHeadersError" {
let message = json["message"]
print("Error getting V3 headers: \(message ?? "no message")")
self.printOut("Error getting V3 headers: \(message ?? "no message")")
if let message = message,
message.contains("-45061") {
print("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
self.printOut("Error message contains -45061 (not provisioned), resetting adi.pb and retrying")
Keychain.shared.adiPb = nil
return provision()
} else { throw OperationError.anisetteV3Error(message: message ?? "Unknown error") }
@@ -214,16 +216,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let response = response,
let version = response.value(forHTTPHeaderField: "Implementation-Version") {
print("Implementation-Version: \(version)")
} else { print("No Implementation-Version header") }
self.printOut("Implementation-Version: \(version)")
} else { self.printOut("No Implementation-Version header") }
print("Anisette used: \(formattedJSON)")
print("Original JSON: \(json)")
self.printOut("Anisette used: \(formattedJSON)")
self.printOut("Original JSON: \(json)")
if let anisette = ALTAnisetteData(json: formattedJSON) {
print("Anisette is valid!")
self.printOut("Anisette is valid!")
self.finish(.success(anisette))
} else {
print("Anisette is invalid!!!!")
self.printOut("Anisette is invalid!!!!")
if v3 {
throw OperationError.anisetteV3Error(message: "Invalid anisette (the returned data may not have all the required fields)")
} else {
@@ -242,22 +244,22 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
// MARK: - V1
func handleV1() {
print("Server is V1")
self.printOut("Server is V1")
if UserDefaults.shared.trustedServerURL == AnisetteManager.currentURLString {
print("Server has already been trusted, fetching anisette")
self.printOut("Server has already been trusted, fetching anisette")
return self.fetchAnisetteV1()
}
print("Alerting user about outdated server")
self.printOut("Alerting user about outdated server")
let alert = UIAlertController(title: "WARNING: Outdated anisette server", message: "We've detected you are using an older anisette server. Using this server has a higher likelihood of locking your account and causing other issues. Are you sure you want to continue?", preferredStyle: UIAlertController.Style.alert)
alert.addAction(UIAlertAction(title: "Continue", style: UIAlertAction.Style.destructive, handler: { action in
print("Fetching anisette via V1")
self.printOut("Fetching anisette via V1")
UserDefaults.shared.trustedServerURL = AnisetteManager.currentURLString
self.fetchAnisetteV1()
}))
alert.addAction(UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel, handler: { action in
print("Cancelled anisette operation")
self.printOut("Cancelled anisette operation")
self.finish(.failure(OperationError.cancelled))
}))
@@ -273,14 +275,14 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
}
func fetchAnisetteV1() {
print("Fetching anisette V1")
self.printOut("Fetching anisette V1")
URLSession.shared.dataTask(with: self.url!) { data, response, error in
do {
guard let data = data, error == nil else { throw OperationError.anisetteV1Error(message: "Unable to fetch data\(error != nil ? " (\(error!.localizedDescription))" : "")") }
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: false)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
@@ -290,7 +292,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func provision() {
fetchClientInfo {
print("Getting provisioning URLs")
self.printOut("Getting provisioning URLs")
var request = self.buildAppleRequest(url: URL(string: "https://gsa.apple.com/grandslam/GsService2/lookup")!)
request.httpMethod = "GET"
URLSession.shared.dataTask(with: request) { data, response, error in
@@ -302,12 +304,12 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let endProvisioningURL = URL(string: endProvisioningString) {
self.startProvisioningURL = startProvisioningURL
self.endProvisioningURL = endProvisioningURL
print("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
print("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
print("Starting a provisioning session")
self.printOut("startProvisioningURL: \(self.startProvisioningURL!.absoluteString)")
self.printOut("endProvisioningURL: \(self.endProvisioningURL!.absoluteString)")
self.printOut("Starting a provisioning session")
self.startProvisioningSession()
} else {
print("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid URLs! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid URLs. Please try again later", message: nil)))
}
}.resume()
@@ -329,19 +331,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
do {
if let json = try JSONSerialization.jsonObject(with: string.data(using: .utf8)!, options: []) as? [String: Any] {
guard let result = json["result"] as? String else {
print("The server didn't give us a result")
self.printOut("The server didn't give us a result")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a result", message: nil)))
return
}
print("Received result: \(result)")
self.printOut("Received result: \(result)")
switch result {
case "GiveIdentifier":
print("Giving identifier")
self.printOut("Giving identifier")
client.json(["identifier": Keychain.shared.identifier!])
case "GiveStartProvisioningData":
print("Getting start provisioning data")
self.printOut("Getting start provisioning data")
let body = [
"Header": [String: Any](),
"Request": [String: Any](),
@@ -353,19 +355,19 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let data = data,
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let spim = plist["Response"]?["spim"] as? String {
print("Giving start provisioning data")
self.printOut("Giving start provisioning data")
client.json(["spim": spim])
} else {
print("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid start provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid start provisioning data. Please try again later", message: nil)))
}
}.resume()
case "GiveEndProvisioningData":
print("Getting end provisioning data")
self.printOut("Getting end provisioning data")
guard let cpim = json["cpim"] as? String else {
print("The server didn't give us a cpim")
self.printOut("The server didn't give us a cpim")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us a cpim", message: nil)))
return
@@ -384,20 +386,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? Dictionary<String, Dictionary<String, Any>>,
let ptm = plist["Response"]?["ptm"] as? String,
let tk = plist["Response"]?["tk"] as? String {
print("Giving end provisioning data")
self.printOut("Giving end provisioning data")
client.json(["ptm": ptm, "tk": tk])
} else {
print("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
self.printOut("Apple didn't give valid end provisioning data! Got response: \(String(data: data ?? Data("nothing".utf8), encoding: .utf8) ?? "not utf8")")
client.disconnect(closeCode: 0)
self.finish(.failure(OperationError.provisioningError(result: "Apple didn't give valid end provisioning data. Please try again later", message: nil)))
}
}.resume()
case "ProvisioningSuccess":
print("Provisioning succeeded!")
self.printOut("Provisioning succeeded!")
client.disconnect(closeCode: 0)
guard let adiPb = json["adi_pb"] as? String else {
print("The server didn't give us an adi.pb file")
self.printOut("The server didn't give us an adi.pb file")
self.finish(.failure(OperationError.provisioningError(result: "The server didn't give us an adi.pb file", message: nil)))
return
}
@@ -406,27 +408,27 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
default:
if result.contains("Error") || result.contains("Invalid") || result == "ClosingPerRequest" || result == "Timeout" || result == "TextOnly" {
print("Failing because of \(result)")
self.printOut("Failing because of \(result)")
self.finish(.failure(OperationError.provisioningError(result: result, message: json["message"] as? String)))
}
}
}
} catch let error as NSError {
print("Failed to handle text: \(error.localizedDescription)")
self.printOut("Failed to handle text: \(error.localizedDescription)")
self.finish(.failure(OperationError.provisioningError(result: error.localizedDescription, message: nil)))
}
case .connected:
print("Connected")
self.printOut("Connected")
case .disconnected(let string, let code):
print("Disconnected: \(code); \(string)")
self.printOut("Disconnected: \(code); \(string)")
case .error(let error):
print("Got error: \(String(describing: error))")
self.printOut("Got error: \(String(describing: error))")
default:
print("Unknown event: \(event)")
self.printOut("Unknown event: \(event)")
}
}
@@ -460,10 +462,10 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
self.mdLu != nil &&
self.deviceId != nil &&
Keychain.shared.identifier != nil {
print("Skipping client_info fetch since all the properties we need aren't nil")
self.printOut("Skipping client_info fetch since all the properties we need aren't nil")
return callback()
}
print("Trying to get client_info")
self.printOut("Trying to get client_info")
let clientInfoURL = self.url!.appendingPathComponent("v3").appendingPathComponent("client_info")
URLSession.shared.dataTask(with: clientInfoURL) { data, response, error in
do {
@@ -473,20 +475,20 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: String] {
if let clientInfo = json["client_info"] {
print("Server is V3")
self.printOut("Server is V3")
self.clientInfo = clientInfo
self.userAgent = json["user_agent"]!
print("Client-Info: \(self.clientInfo!)")
print("User-Agent: \(self.userAgent!)")
self.printOut("Client-Info: \(self.clientInfo!)")
self.printOut("User-Agent: \(self.userAgent!)")
if Keychain.shared.identifier == nil {
print("Generating identifier")
self.printOut("Generating identifier")
var bytes = [Int8](repeating: 0, count: 16)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
if status != errSecSuccess {
print("ERROR GENERATING IDENTIFIER!!! \(status)")
self.printOut("ERROR GENERATING IDENTIFIER!!! \(status)")
return self.finish(.failure(OperationError.provisioningError(result: "Couldn't generate identifier", message: nil)))
}
@@ -495,16 +497,16 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
let decoded = Data(base64Encoded: Keychain.shared.identifier!)!
self.mdLu = decoded.sha256().hexEncodedString()
print("X-Apple-I-MD-LU: \(self.mdLu!)")
self.printOut("X-Apple-I-MD-LU: \(self.mdLu!)")
let uuid: UUID = decoded.object()
self.deviceId = uuid.uuidString.uppercased()
print("X-Mme-Device-Id: \(self.deviceId!)")
self.printOut("X-Mme-Device-Id: \(self.deviceId!)")
callback()
} else { self.handleV1() }
} else { self.finish(.failure(OperationError.anisetteV3Error(message: "Couldn't fetch client info. The returned data may not be in JSON"))) }
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.handleV1()
}
}.resume()
@@ -512,7 +514,7 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
func fetchAnisetteV3(_ identifier: String, _ adiPb: String) {
fetchClientInfo {
print("Fetching anisette V3")
self.printOut("Fetching anisette V3")
let url = UserDefaults.standard.menuAnisetteURL
var request = URLRequest(url: self.url!.appendingPathComponent("v3").appendingPathComponent("get_headers"))
request.httpMethod = "POST"
@@ -527,12 +529,21 @@ final class FetchAnisetteDataOperation: ResultOperation<ALTAnisetteData>, WebSoc
try self.extractAnisetteData(data, response as? HTTPURLResponse, v3: true)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
self.printOut("Failed to load: \(error.localizedDescription)")
self.finish(.failure(error))
}
}.resume()
}
}
private func printOut(_ text: String?){
let isInternalLoggingEnabled = OperationsLoggingControl.getFromDatabase(for: ANISETTE_VERBOSITY.self)
if(isInternalLoggingEnabled){
// logging enabled, so log it
text.map{ _ in print(text!) } ?? print()
}
}
}
extension WebSocketClient {

View File

@@ -13,15 +13,16 @@ import AltSign
import Roxas
@objc(FetchProvisioningProfilesOperation)
final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProvisioningProfile]>
{
let context: AppOperationContext
var additionalEntitlements: [ALTEntitlement: Any]?
private let appGroupsLock = NSLock()
internal let appGroupsLock = NSLock()
init(context: AppOperationContext)
// this class is abstract or shouldn't be instantiated outside, use the subclasses
fileprivate init(context: AppOperationContext)
{
self.context = context
@@ -40,11 +41,13 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
return
}
guard
let team = self.context.team,
let session = self.context.session
else {
return self.finish(.failure(OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))) }
guard let team = self.context.team,
let session = self.context.session else {
return self.finish(.failure(
OperationError.invalidParameters("FetchProvisioningProfilesOperation.main: self.context.team or self.context.session is nil"))
)
}
guard let app = self.context.app else { return self.finish(.failure(OperationError.appNotFound(name: nil))) }
@@ -120,7 +123,11 @@ final class FetchProvisioningProfilesOperation: ResultOperation<[String: ALTProv
extension FetchProvisioningProfilesOperation
{
func prepareProvisioningProfile(for app: ALTApplication, parentApp: ALTApplication?, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
private func prepareProvisioningProfile(for app: ALTApplication,
parentApp: ALTApplication?,
team: ALTTeam,
session: ALTAppleAPISession, c
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
@@ -134,19 +141,21 @@ extension FetchProvisioningProfilesOperation
// or if installedApp.team is nil but resignedBundleIdentifier contains the team's identifier.
let teamsMatch = installedApp.team?.identifier == team.identifier || (installedApp.team == nil && installedApp.resignedBundleIdentifier.contains(team.identifier))
// #if DEBUG
//
// if app.isAltStoreApp
// {
// // Use legacy bundle ID format for AltStore.
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
// else
// {
// preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
// }
//
// #else
// TODO: @mahee96: Try to keep the debug build and release build operations similar, refactor later with proper reasoning
// for now, restricted it to debug on simulator only
#if DEBUG && targetEnvironment(simulator)
if app.isAltStoreApp
{
// Use legacy bundle ID format for AltStore.
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
else
{
preferredBundleID = teamsMatch ? installedApp.resignedBundleIdentifier : nil
}
#else
if teamsMatch
{
@@ -160,7 +169,7 @@ extension FetchProvisioningProfilesOperation
preferredBundleID = nil
}
// #endif
#endif
}
else
{
@@ -211,35 +220,22 @@ extension FetchProvisioningProfilesOperation
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
self.fetchProvisioningProfile(for: appID, team: team, session: session) { (result) in
completionHandler(result)
}
}
}
}
}
//process
self.fetchProvisioningProfile(
for: appID, team: team, session: session, completionHandler: completionHandler
)
}
}
}
}
func registerAppID(for application: ALTApplication, name: String, bundleIdentifier: String, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
private func registerAppID(for application: ALTApplication,
name: String,
bundleIdentifier: String,
team: ALTTeam,
session: ALTAppleAPISession,
completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) { (appIDs, error) in
do
@@ -333,7 +329,81 @@ extension FetchProvisioningProfilesOperation
}
}
func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
internal func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}
class FetchProvisioningProfilesRefreshOperation: FetchProvisioningProfilesOperation, @unchecked Sendable {
override init(context: AppOperationContext)
{
super.init(context: context)
}
}
class FetchProvisioningProfilesInstallOperation: FetchProvisioningProfilesOperation, @unchecked Sendable{
override init(context: AppOperationContext)
{
super.init(context: context)
}
// modify Operations are allowed for the app groups and other stuffs
func fetchProvisioningProfile(appID: ALTAppID,
for app: ALTApplication,
team: ALTTeam,
session: ALTAppleAPISession,
completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
// Update features
self.updateFeatures(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Update app groups
self.updateAppGroups(for: appID, app: app, team: team, session: session) { (result) in
switch result
{
case .failure(let error): completionHandler(.failure(error))
case .success(let appID):
// Fetch Provisioning Profile
super.fetchProvisioningProfile(for: appID, team: team, session: session, completionHandler: completionHandler)
}
}
}
}
}
private func updateFeatures(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -412,7 +482,7 @@ extension FetchProvisioningProfilesOperation
}
}
func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
private func updateAppGroups(for appID: ALTAppID, app: ALTApplication, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTAppID, Error>) -> Void)
{
var entitlements = app.entitlements
for (key, value) in additionalEntitlements ?? [:]
@@ -511,7 +581,7 @@ extension FetchProvisioningProfilesOperation
Logger.sideload.notice("Created new App Group \(group.groupIdentifier, privacy: .public).")
groups.append(group)
case .failure(let error):
case .failure(let error):
Logger.sideload.notice("Failed to create new App Group \(adjustedGroupIdentifier, privacy: .public). \(error.localizedDescription, privacy: .public)")
errors.append(error)
}
@@ -547,34 +617,4 @@ extension FetchProvisioningProfilesOperation
}
}
}
func fetchProvisioningProfile(for appID: ALTAppID, team: ALTTeam, session: ALTAppleAPISession, completionHandler: @escaping (Result<ALTProvisioningProfile, Error>) -> Void)
{
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
switch Result(profile, error)
{
case .failure(let error): completionHandler(.failure(error))
case .success(let profile):
// Delete existing profile
ALTAppleAPI.shared.delete(profile, for: team, session: session) { (success, error) in
switch Result(success, error)
{
case .failure:
// As of March 20, 2023, the free provisioning profile is re-generated each fetch, and you can no longer delete it.
// So instead, we just return the fetched profile from above.
completionHandler(.success(profile))
case .success:
Logger.sideload.notice("Generating new free provisioning profile for App ID \(appID.bundleIdentifier, privacy: .public).")
// Fetch new provisioning profile
ALTAppleAPI.shared.fetchProvisioningProfile(for: appID, deviceType: .iphone, team: team, session: session) { (profile, error) in
completionHandler(Result(profile, error))
}
}
}
}
}
}
}

View File

@@ -38,10 +38,14 @@ class ResultOperation<ResultType>: Operation
result = .failure(error)
}
// diagnostics logging
let resultStatus = String(describing: result).prefix("success".count).uppercased()
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
" Result: \(result)\n")
// Diagnostics: perform verbose logging of the operations only if enabled (so as to not flood console logs)
let isLoggingEnabledForThisOperation = OperationsLoggingControl.getFromDatabase(for: type(of: self))
if UserDefaults.standard.isVerboseOperationsLoggingEnabled && isLoggingEnabledForThisOperation {
// diagnostics logging
let resultStatus = String(describing: result).prefix("success".count).uppercased()
print("\n ====> OPERATION: `\(type(of: self))` completed with: \(resultStatus) <====\n\n" +
" Result: \(result)\n")
}
self.resultHandler?(result)

View File

@@ -58,17 +58,18 @@ final class RemoveAppBackupOperation: ResultOperation<Void>
}
catch let error as CocoaError where error.code == CocoaError.Code.fileNoSuchFile
{
#if DEBUG
// When debugging, it's expected that app groups don't match, so ignore.
self.finish(.success(()))
#else
// TODO: @mahee96: Find out why should in debug builds the app-groups is not expected to match
// #if DEBUG
//
// // When debugging, it's expected that app groups don't match, so ignore.
// self.finish(.success(()))
//
// #else
Logger.sideload.error("Failed to remove app backup directory \(backupDirectoryURL.lastPathComponent, privacy: .public). \(error.localizedDescription, privacy: .public)")
self.finish(.failure(error))
#endif
// #endif
}
catch
{

View File

@@ -0,0 +1,219 @@
//
// RefreshAppOperation.swift
// AltStore
//
// Created by Riley Testut on 2/27/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import Foundation
import AltStoreCore
import Roxas
import AltSign
@objc(RemoveAppExtensionsOperation)
final class RemoveAppExtensionsOperation: ResultOperation<Void>
{
let context: AppOperationContext
let localAppExtensions: Set<ALTApplication>?
init(context: AppOperationContext, localAppExtensions: Set<ALTApplication>?)
{
self.context = context
self.localAppExtensions = localAppExtensions
super.init()
}
override func main()
{
super.main()
if let error = self.context.error
{
self.finish(.failure(error))
return
}
guard let targetAppBundle = context.app else {
return self.finish(.failure(
OperationError.invalidParameters("RemoveAppExtensionsOperation: context.app is nil")
))
}
self.removeAppExtensions(from: targetAppBundle,
localAppExtensions: localAppExtensions,
extensions: targetAppBundle.appExtensions,
context.authenticatedContext.presentingViewController)
}
private static func removeExtensions(from extensions: Set<ALTApplication>) throws {
for appExtension in extensions {
print("Deleting extension \(appExtension.bundleIdentifier)")
try FileManager.default.removeItem(at: appExtension.fileURL)
}
}
private func removeAppExtensions(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController?)
{
// target App Bundle doesn't contain extensions so don't bother
guard !targetAppBundle.appExtensions.isEmpty else {
return self.finish(.success(()))
}
// process extensionsInfo
let excessExtensions = processExtensionsInfo(from: targetAppBundle, localAppExtensions: localAppExtensions)
DispatchQueue.main.async {
guard let presentingViewController: UIViewController = presentingViewController,
presentingViewController.viewIfLoaded?.window != nil else {
// background mode: remove only the excess extensions automatically for re-installs
// keep all extensions for fresh install (localAppBundle = nil)
return self.backgroundModeExtensionsCleanup(excessExtensions: excessExtensions)
}
// present prompt to the user if we have a view context
let alertController = self.createAlertDialog(from: targetAppBundle, extensions: extensions, presentingViewController)
presentingViewController.present(alertController, animated: true){
// if for any reason the view wasn't presented, then just signal that as error
if presentingViewController.presentedViewController == nil {
let errMsg = "RemoveAppExtensionsOperation: unable to present dialog, view context not available." +
"\nDid you move to different screen or background after starting the operation?"
self.finish(.failure(
OperationError.invalidOperationContext(errMsg)
))
}
}
}
}
private func createAlertDialog(from targetAppBundle: ALTApplication,
extensions: Set<ALTApplication>,
_ presentingViewController: UIViewController) -> UIAlertController
{
/// Foreground prompt:
let firstSentence: String
if UserDefaults.standard.activeAppLimitIncludesExtensions
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to 3 active apps and app extensions.", comment: "")
}
else
{
firstSentence = NSLocalizedString("Non-developer Apple IDs are limited to creating 10 App IDs per week.", comment: "")
}
let message = firstSentence + " " + NSLocalizedString("Would you like to remove this app's extensions so they don't count towards your limit? There are \(extensions.count) Extensions", comment: "")
let alertController = UIAlertController(title: NSLocalizedString("App Contains Extensions", comment: ""), message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: { (action) in
self.finish(.failure(OperationError.cancelled))
}))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Keep App Extensions", comment: ""), style: .default) { (action) in
self.finish(.success(()))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove App Extensions", comment: ""), style: .destructive) { (action) in
do {
try Self.removeExtensions(from: targetAppBundle.appExtensions)
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Choose App Extensions", comment: ""), style: .default) { (action) in
let popoverContentController = AppExtensionViewHostingController(extensions: extensions) { (selection) in
do {
try Self.removeExtensions(from: Set(selection))
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
}
let suiview = popoverContentController.view!
suiview.translatesAutoresizingMaskIntoConstraints = false
popoverContentController.modalPresentationStyle = .popover
if let popoverPresentationController = popoverContentController.popoverPresentationController {
popoverPresentationController.sourceView = presentingViewController.view
popoverPresentationController.sourceRect = CGRect(x: 50, y: 50, width: 4, height: 4)
popoverPresentationController.delegate = popoverContentController
DispatchQueue.main.async {
presentingViewController.present(popoverContentController, animated: true)
}
}else{
self.finish(.failure(
OperationError.invalidParameters("RemoveAppExtensionsOperation: popoverContentController.popoverPresentationController is nil"))
)
}
})
return alertController
}
struct ExtensionsInfo{
let excessInTarget: Set<ALTApplication>
let necessaryInExisting: Set<ALTApplication>
}
private func processExtensionsInfo(from targetAppBundle: ALTApplication,
localAppExtensions: Set<ALTApplication>?) -> Set<ALTApplication>
{
//App-Extensions: Ensure existing app's extensions in DB and currently installing app bundle's extensions must match
let targetAppEx: Set<ALTApplication> = targetAppBundle.appExtensions
let targetAppExNames = targetAppEx.map{ appEx in appEx.bundleIdentifier}
guard let extensionsInExistingApp = localAppExtensions else {
let diagnosticsMsg = "RemoveAppExtensionsOperation: ExistingApp is nil, Hence keeping all app extensions from targetAppBundle"
+ "RemoveAppExtensionsOperation: ExistingAppEx: nil; targetAppBundleEx: \(targetAppExNames)"
print(diagnosticsMsg)
return Set() // nothing is excess since we are keeping all, so returning empty
}
let existingAppEx: Set<ALTApplication> = extensionsInExistingApp
let existingAppExNames = existingAppEx.map{ appEx in appEx.bundleIdentifier}
let excessExtensionsInTargetApp = targetAppEx.filter{
!(existingAppExNames.contains($0.bundleIdentifier))
}
let isMatching = (targetAppEx.count == existingAppEx.count) && excessExtensionsInTargetApp.isEmpty
let diagnosticsMsg = "RemoveAppExtensionsOperation: App Extensions in localAppBundle and targetAppBundle are matching: \(isMatching)\n"
+ "RemoveAppExtensionsOperation: \nlocalAppBundleEx: \(existingAppExNames); \ntargetAppBundleEx: \(String(describing: targetAppExNames))\n"
print(diagnosticsMsg)
return excessExtensionsInTargetApp
}
private func backgroundModeExtensionsCleanup(excessExtensions: Set<ALTApplication>) {
// perform silent extensions cleanup for those that aren't already present in existing app
print("\n Performing background mode Extensions removal \n")
print("RemoveAppExtensionsOperation: Excess Extensions In TargetAppBundle: \(excessExtensions.map{$0.bundleIdentifier})")
do {
try Self.removeExtensions(from: excessExtensions)
return self.finish(.success(()))
} catch {
return self.finish(.failure(error))
}
}
}

View File

@@ -244,6 +244,7 @@ private extension ResignAppOperation
{
for case let fileURL as URL in enumerator
{
// for both sim and device, in debug mode builds, remove the tests bundles (if any)
#if DEBUG
guard !fileURL.lastPathComponent.lowercased().contains(".xctest") else {
// Remove embedded XCTest (+ dSYM) bundle from copied app bundle.