diff --git a/Sources/Anisette/AnisetteDataManager.swift b/Sources/Anisette/AnisetteDataManager.swift index 8c64485..41a2955 100644 --- a/Sources/Anisette/AnisetteDataManager.swift +++ b/Sources/Anisette/AnisetteDataManager.swift @@ -433,8 +433,17 @@ public final class AnisetteDataManager: @unchecked Sendable { } } - let clientInfo = headers?.clientInfo ?? Constants.Anisette.defaultClientInfo - let client = try await getClient(for: resolvedMode, clientInfo: clientInfo) + let windowsClientInfo = " " + let akdUserAgent = "akd/1.0 CFNetwork/808.1.4" + var clientInfo = headers?.clientInfo ?? windowsClientInfo + if clientInfo.isEmpty || clientInfo.contains("MacBook") || clientInfo.contains(" AnisetteClient { + private func getClient(for mode: AnisetteMode, clientInfo: String, userAgent: String = "akd/1.0 CFNetwork/808.1.4") async throws -> AnisetteClient { switch mode { case .remote(let server): try FileManager.default.createDirectory(at: provisioningDir, withIntermediateDirectories: true) return try AnisetteClient( provisioningDir: provisioningDir, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: userAgent, lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, provider: RemoteAnisetteDataProvider(serverURL: server) @@ -489,7 +498,7 @@ public final class AnisetteDataManager: @unchecked Sendable { return try AnisetteClient( provisioningDir: targetProvDir, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: userAgent, lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } @@ -648,7 +657,7 @@ public final class AnisetteDataManager: @unchecked Sendable { self.localProvider = try AnisetteClient( provisioningDir: prov, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: "akd/1.0 CFNetwork/808.1.4", lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } @@ -699,7 +708,7 @@ public final class AnisetteDataManager: @unchecked Sendable { let provider = try AnisetteClient( provisioningDir: prov, clientInfo: clientInfo, - userAgent: Constants.Anisette.defaultUserAgent, + userAgent: "akd/1.0 CFNetwork/808.1.4", lookupURL: Constants.Anisette.URLs.grandSlamLookup, requiredLibraries: Constants.Anisette.Libraries.requiredNames, libraryDirectoryResolver: { libDir } diff --git a/Sources/Constants.swift b/Sources/Constants.swift index 53d3070..02c336b 100644 --- a/Sources/Constants.swift +++ b/Sources/Constants.swift @@ -15,13 +15,13 @@ public enum Constants { public static let service = "iCloud" public static let headerVersion = "1.0.1" public static let authApp = "com.apple.gs.xcode.auth" - public static let userAgent = "AuthKit/1 (Macintosh; OS X 26.6) (com.apple.dt.Xcode/26.0)" + public static let userAgent = "akd/1.0 CFNetwork/808.1.4" // SideInstaller temporary Windows GSA fix } // https://idmsa.apple.com/appleauth/auth/devices public enum AppleAuth { public static let appIDKey = "ba2ec180e6ca6e6c6a542255453b24d6e6e5b2be0cc48bc1b0d8ad64cfe0228f" - public static let userAgent = "AuthKit/1 (Macintosh; OS X 26.6)" + public static let userAgent = "akd/1.0 CFNetwork/808.1.4" // SideInstaller temporary Windows GSA fix } // https://developerservices2.apple.com/services/ diff --git a/Sources/DeveloperPortal/Authentication.swift b/Sources/DeveloperPortal/Authentication.swift index 0b8e0f2..680a1a4 100644 --- a/Sources/DeveloperPortal/Authentication.swift +++ b/Sources/DeveloperPortal/Authentication.swift @@ -317,11 +317,38 @@ public extension DeveloperPortal { request.httpMethod = "POST" request.httpBody = plistData + // SideInstaller base_headers parity (Windows GSA / akd) + // Prefer anisette identity UA; never send Macintosh/Xcode (Apple 503s those). + let windowsClientInfo = " " + let akdUserAgent = "akd/1.0 CFNetwork/808.1.4" + var clientInfo = anisetteData.clientInfo.trimmingCharacters(in: .whitespacesAndNewlines) + if clientInfo.isEmpty + || clientInfo.contains("MacBook") + || clientInfo.contains("macOS") + || clientInfo.contains("Mac OS") + || clientInfo.contains(" TwoFactorAuthValidationResult { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context) + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.grandSlamValidate, context: context, asMac: true) verifyRequest.setValue(code, forHTTPHeaderField: "security-code") if let allHeaders = verifyRequest.allHTTPHeaderFields { @@ -805,9 +863,8 @@ public extension DeveloperPortal { } debugLog("[SideSign] Verifying trusted device security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + let (verifyData, verifyHttpResponse) = try await performTwoFactorRequest(verifyRequest, context: context, actionName: "validate2FACode") + let verifyStatusCode = verifyHttpResponse.safeStatusCode return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: false, httpResponse: verifyHttpResponse) } @@ -817,7 +874,7 @@ public extension DeveloperPortal { mode: String, context: TwoFactorAuthContext) async throws -> TwoFactorAuthValidationResult { - var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context) + var verifyRequest = makeTwoFactorAuthRequest(url: Constants.URLs.phoneSecurityCode, context: context, asMac: true) verifyRequest.httpMethod = "POST" verifyRequest.httpBody = try PropertyListSerialization.data(fromPropertyList: [ "securityCode.code": code, @@ -829,9 +886,8 @@ public extension DeveloperPortal { } debugLog("[SideSign] Verifying secondary security code...") - let (verifyData, verifyResponse) = try await session.data(for: verifyRequest) - let verifyHttpResponse = verifyResponse as? HTTPURLResponse - let verifyStatusCode = verifyHttpResponse?.safeStatusCode ?? 0 + let (verifyData, verifyHttpResponse) = try await performTwoFactorRequest(verifyRequest, context: context, actionName: "validate2FACode") + let verifyStatusCode = verifyHttpResponse.safeStatusCode return try parseTwoFactorAuthVerifyResponse(data: verifyData, statusCode: verifyStatusCode, requirePeToken: true, httpResponse: verifyHttpResponse) } @@ -893,25 +949,37 @@ public extension DeveloperPortal { return .success } - private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext) -> URLRequest { + /// SideInstaller MAC_CLIENT_INFO: gsa.apple.com/auth 2FA pages often return a bare 403 + /// for the Windows client used on GrandSlam; retry those requests as a Mac. + private static let macClientInfoFor2FA = + " " + + private func makeTwoFactorAuthRequest(url: URL, context: TwoFactorAuthContext, asMac: Bool = false) -> URLRequest { let identityToken = "\(context.dsid):\(context.idmsToken)" let encodedIdentityToken = Data(identityToken.utf8).base64EncodedString() var request = URLRequest(url: url) let a = context.anisetteData let h = customHeaders + var ua = a.userAgent.trimmingCharacters(in: .whitespacesAndNewlines) + if ua.isEmpty || ua.contains("Macintosh") || ua.contains("Xcode") || ua.contains("Mac OS") { + ua = "akd/1.0 CFNetwork/808.1.4" + } + let clientInfo = asMac ? Self.macClientInfoFor2FA : ( + a.clientInfo.isEmpty ? Self.macClientInfoFor2FA : a.clientInfo + ) let headers: [String: String] = [ "Accept": "application/x-buddyml", "Accept-Language": "en-us", "Content-Type": "application/x-plist", - "User-Agent": h.developerServices.userAgent, + "User-Agent": ua, "X-Apple-App-Info": h.grandSlam.authApp, "X-Xcode-Version": context.xcodeVersion, "X-Apple-Identity-Token": encodedIdentityToken, "X-Apple-I-MD": a.oneTimePassword, "X-Apple-I-MD-M": a.machineID, "X-Mme-Device-Id": a.deviceID, - "X-MMe-Client-Info": a.clientInfo, + "X-Mme-Client-Info": clientInfo, "X-Apple-I-MD-LU": a.localUserID, "X-Apple-I-MD-RINFO": a.routingInfo, "X-Apple-I-SRL-NO": a.serialNumber, @@ -924,7 +992,40 @@ public extension DeveloperPortal { return request } - private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { + /// SideInstaller `send_auth`: gsa.apple.com/auth often refuses Windows with 403/5xx; + /// retry once as Mac (MAC_CLIENT_INFO). Do NOT treat generic "ec" substring as service error. + private func performTwoFactorRequest(_ request: URLRequest, context: TwoFactorAuthContext, actionName: String) async throws -> (Data, HTTPURLResponse) { + var current = request + var triedMac = false + while true { + let (data, response) = try await session.data(for: current) + let http = response as? HTTPURLResponse + let status = http?.safeStatusCode ?? 0 + let body = String(data: data, encoding: .utf8) ?? "" + + let shouldRetryAsMac = + !triedMac + && (status == 403 || status == 503 || status == 500 || status == 433 || status == 0) + if shouldRetryAsMac { + debugLog("[SideSign] \(actionName) HTTP \(status) — retrying as Mac (SideInstaller 2FA parity)") + guard let url = current.url else { + throw ServerError.badServerResponse(reason: "\(actionName) missing URL", jsonPayload: body) + } + var macRequest = makeTwoFactorAuthRequest(url: url, context: context, asMac: true) + macRequest.httpMethod = current.httpMethod + macRequest.httpBody = current.httpBody + current = macRequest + triedMac = true + continue + } + guard let http else { + throw ServerError.badServerResponse(reason: "\(actionName) non-HTTP response", jsonPayload: body) + } + return (data, http) + } + } + +private func parsePlistOrJSON(_ data: Data) -> [String: any Sendable]? { (try? PropertyListSerialization.propertyList(from: data, format: nil)) as? [String: any Sendable] ?? (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: any Sendable] } diff --git a/Sources/DeveloperPortal/DeveloperPortalAPI.swift b/Sources/DeveloperPortal/DeveloperPortalAPI.swift index dfea7cc..bd6ac7f 100644 --- a/Sources/DeveloperPortal/DeveloperPortalAPI.swift +++ b/Sources/DeveloperPortal/DeveloperPortalAPI.swift @@ -255,8 +255,21 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { let session: URLSession - public init(session: URLSession = .shared, customHeaders: SideSignHeaders = SideSignHeaders()) { - self.session = session + /// SideInstaller uses pool_max_idle_per_host(0) so GSA never reuses connections + /// (reused connections often get HTTP 5xx / empty bodies). + private static func makeGSASession() -> URLSession { + let config = URLSessionConfiguration.ephemeral + config.httpShouldUsePipelining = false + config.httpMaximumConnectionsPerHost = 1 + config.requestCachePolicy = .reloadIgnoringLocalCacheData + config.urlCache = nil + config.timeoutIntervalForRequest = 60 + config.timeoutIntervalForResource = 120 + return URLSession(configuration: config) + } + + public init(session: URLSession? = nil, customHeaders: SideSignHeaders = SideSignHeaders()) { + self.session = session ?? Self.makeGSASession() self.cachedCustomHeaders = customHeaders } @@ -327,7 +340,7 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { "X-Apple-I-MD-LU": a.localUserID, "X-Apple-I-MD-RINFO": a.routingInfo, "X-Mme-Device-Id": a.deviceID, - "X-MMe-Client-Info": a.clientInfo, + "X-Mme-Client-Info": a.clientInfo, "X-Apple-I-Client-Time": a.clientTime, "X-Apple-Locale": a.locale, "X-Apple-I-Locale": a.locale, @@ -438,7 +451,7 @@ public final class DeveloperPortal: DeveloperPortalAPI, Sendable { headers["X-Apple-I-MD-LU"] = a.localUserID headers["X-Apple-I-MD-RINFO"] = a.routingInfo headers["X-Mme-Device-Id"] = a.deviceID - headers["X-MMe-Client-Info"] = a.clientInfo + headers["X-Mme-Client-Info"] = a.clientInfo headers["X-Apple-I-Client-Time"] = a.clientTime headers["X-Apple-Locale"] = a.locale headers["X-Apple-I-Locale"] = a.locale