Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 72 additions & 25 deletions AltSign/Sources/ALTAppleAPI+Authentication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ private extension ALTAppleAPI
"Content-Type": "text/x-xml-plist",
"X-MMe-Client-Info": anisetteData.deviceDescription,
"Accept": "*/*",
"User-Agent": "akd/1.0 CFNetwork/978.0.7 Darwin/18.7.0"
"User-Agent": "AuthKit/1 (Macintosh; OS X 26.5.2) (com.apple.dt.Xcode/26.0)"
]

let bodyData = try PropertyListSerialization.data(fromPropertyList: parameters, format: .xml, options: 0)
Expand All @@ -445,45 +445,92 @@ private extension ALTAppleAPI
request.httpMethod = "POST"
request.httpBody = bodyData
httpHeaders.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }

let dataTask = self.session.dataTask(with: request) { (data, response, error) in
do

self.sendGrandSlamRequest(request, attempt: 1, deadline: ProcessInfo.processInfo.systemUptime + 20, completionHandler: completionHandler)
}
catch
{
completionHandler(.failure(error))
}
}

// Isolate every GSA attempt and keep transient server retries within this exchange's deadline.
func sendGrandSlamRequest(_ request: URLRequest, attempt: Int, deadline: TimeInterval,
completionHandler: @escaping (Result<[String: Any], Error>) -> Void)
{
let remaining = deadline - ProcessInfo.processInfo.systemUptime
guard remaining > 0 else {
completionHandler(.failure(URLError(.timedOut)))
return
}

let configuration = URLSessionConfiguration.ephemeral
configuration.timeoutIntervalForRequest = remaining
configuration.timeoutIntervalForResource = remaining
let session = URLSession(configuration: configuration)
var timedRequest = request
timedRequest.timeoutInterval = remaining

let dataTask = session.dataTask(with: timedRequest) { (data, response, error) in
session.finishTasksAndInvalidate()
do
{
if let error = error { throw error }
let httpResponse = response as? HTTPURLResponse

// Read structured Apple errors before considering a retry. Never expose response bodies.
let propertyList = data.flatMap { try? PropertyListSerialization.propertyList(from: $0, format: nil) }
let responseDictionary = propertyList as? [String: Any]
let dictionary = responseDictionary?["Response"] as? [String: Any]
let status = dictionary?["Status"] as? [String: Any]
if let errorCode = status?["ec"] as? Int, errorCode != 0
{
guard let data = data else { throw error ?? ALTAppleAPIError.unknown() }

guard let responseDictionary = try PropertyListSerialization.propertyList(from: data, format: nil) as? [String: Any],
let dictionary = responseDictionary["Response"] as? [String: Any],
let status = dictionary["Status"] as? [String: Any]
else { throw URLError(.badServerResponse) }

let errorCode = status["ec"] as? Int ?? 0
guard errorCode != 0 else { return completionHandler(.success(dictionary)) }

switch errorCode
{
case -20101, -22406: throw ALTAppleAPIError(.incorrectCredentials)
case -22421: throw ALTAppleAPIError(.invalidAnisetteData)
default:
guard let errorDescription = status["em"] as? String else { throw ALTAppleAPIError.unknown() }
guard let errorDescription = status?["em"] as? String else { throw ALTAppleAPIError.unknown() }

let localizedDescription = errorDescription + " (\(errorCode))"
throw NSError(domain: ALTUnderlyingAppleAPIErrorDomain, code: errorCode, userInfo: [NSLocalizedDescriptionKey: localizedDescription])
}
}
catch

// GSA carries its own status in the plist, including authentication challenges.
if let dictionary = dictionary, status != nil {
completionHandler(.success(dictionary))
return
}

if let httpResponse = httpResponse, (500...599).contains(httpResponse.statusCode), attempt < 5
{
completionHandler(.failure(error))
let delay = pow(2.0, Double(attempt - 1)) // Four retries: 1, 2, 4, then 8 seconds.
if ProcessInfo.processInfo.systemUptime + delay < deadline
{
DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
self.sendGrandSlamRequest(request, attempt: attempt + 1, deadline: deadline, completionHandler: completionHandler)
}
return
}
}

let message: String
if let httpResponse = httpResponse {
message = String(format: NSLocalizedString("Apple's authentication servers returned an unexpected response (HTTP %ld). Please try again.", comment: ""), httpResponse.statusCode)
} else {
message = NSLocalizedString("Apple's authentication servers returned an unexpected response. Please try again.", comment: "")
}
throw URLError(.badServerResponse, userInfo: [NSLocalizedDescriptionKey: message])
}
catch
{
completionHandler(.failure(error))
}

dataTask.resume()
}
catch
{
completionHandler(.failure(error))
}
dataTask.resume()
}

func makeTwoFactorCodeRequest(url: URL,
dsid: String,
idmsToken: String,
Expand Down
28 changes: 28 additions & 0 deletions Tests/GrandSlamTransport/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# GrandSlam transport regression tests

Run on macOS with Python 3 and Xcode command-line tools:

```sh
python3 Tests/GrandSlamTransport/test_transport.py
```

To retain machine-readable results:

```sh
python3 Tests/GrandSlamTransport/test_transport.py --results /tmp/altsign-gsa-results.json
```

The runner extracts `sendAuthenticationRequest` and `sendGrandSlamRequest` directly from the production Swift file, replacing only the GSA endpoint with an ephemeral loopback HTTP server. Minimal type shims avoid needing SRP, accounts, signing certificates, or the complete application. A test-only URLProtocol injects cancellation/offline failures into otherwise normal ephemeral sessions. Compilation and execution happen in a temporary directory.

The 17 cases cover:

- HTML 503 followed by a valid plist, using a separate connection for each attempt.
- Persistent 503: five total attempts, 1/2/4/8-second backoff, and the exchange deadline.
- Structured success and authentication challenges on HTTP 409/503, including a missing `ec` field, preserving existing GSA semantics.
- Incorrect credentials, invalid anisette, and arbitrary Apple error codes without retries, including structured errors returned with HTTP 503.
- Malformed HTML with HTTP 200/401 and unexpected plist shapes, without response-body or underlying parser-error leakage.
- Cancellation and offline errors without transport-helper retries.
- In-flight timeout, insufficient retry budget, and an already-expired deadline.
- Exactly one observed completion for each case and the modern User-Agent on GSA requests.

This is transport regression coverage on macOS Foundation. It does not perform live SRP, submit a two-factor code, validate an Apple account, prove the User-Agent's effect at Apple's edge, or replace iPhone installation/refresh testing. Timing checks assume a reasonably unloaded development machine. No Apple account, certificate, or network service beyond loopback is used.
Loading