Skip to content

Commit b5b31d2

Browse files
authored
Merge pull request #389 from noppefoxwolf/hotfix/supports-503-html-response
Support Service Temporarily Unavailable response error
2 parents d1439a4 + d6d00b2 commit b5b31d2

File tree

7 files changed

+296
-14
lines changed

7 files changed

+296
-14
lines changed

Sources/AppleAPI/Client.swift

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public class Client {
1818
case incorrectSecurityCode
1919
case unexpectedSignInResponse(statusCode: Int, message: String?)
2020
case appleIDAndPrivacyAcknowledgementRequired
21+
case serviceTemporarilyUnavailable
2122
case noTrustedPhoneNumbers
2223
case notAuthenticated
2324
case invalidHashcash
@@ -32,6 +33,8 @@ public class Client {
3233
return "Invalid username and password combination. Attempted to sign in with username \(username)."
3334
case .appleIDAndPrivacyAcknowledgementRequired:
3435
return "You must sign in to https://appstoreconnect.apple.com and acknowledge the Apple ID & Privacy agreement."
36+
case .serviceTemporarilyUnavailable:
37+
return "The service is temporarily unavailable. Please try again later."
3538
case .invalidPhoneNumberIndex(let min, let max, let given):
3639
return "Not a valid phone number index. Expecting a whole number between \(min)-\(max), but was given \(given ?? "nothing")."
3740
case .noTrustedPhoneNumbers:
@@ -188,20 +191,25 @@ public class Client {
188191
}
189192

190193
let httpResponse = response as! HTTPURLResponse
191-
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
192-
193-
switch httpResponse.statusCode {
194-
case 200:
195-
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
196-
case 401:
197-
throw Error.invalidUsernameOrPassword(username: accountName)
198-
case 409:
199-
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
200-
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
201-
throw Error.appleIDAndPrivacyAcknowledgementRequired
202-
default:
203-
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
204-
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
194+
do {
195+
let responseBody = try JSONDecoder().decode(SignInResponse.self, from: data)
196+
switch httpResponse.statusCode {
197+
case 200:
198+
return Current.network.dataTask(with: URLRequest.olympusSession).asVoid()
199+
case 401:
200+
throw Error.invalidUsernameOrPassword(username: accountName)
201+
case 409:
202+
return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey)
203+
case 412 where Client.authTypes.contains(responseBody.authType ?? ""):
204+
throw Error.appleIDAndPrivacyAcknowledgementRequired
205+
default:
206+
throw Error.unexpectedSignInResponse(statusCode: httpResponse.statusCode,
207+
message: responseBody.serviceErrors?.map { $0.description }.joined(separator: ", "))
208+
}
209+
} catch DecodingError.dataCorrupted where httpResponse.statusCode == 503 {
210+
throw Error.serviceTemporarilyUnavailable
211+
} catch {
212+
throw error
205213
}
206214
}
207215
}

Tests/AppleAPITests/AppleAPITests.swift

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,96 @@ final class AppleAPITests: XCTestCase {
661661
""")
662662
}
663663

664+
func test_Login_Service_Temporarily_Unavailable() {
665+
var log = ""
666+
Current.logging.log = { log.append($0 + "\n") }
667+
668+
var readLineCount = 0
669+
Current.shell.readLine = { prompt in
670+
defer { readLineCount += 1 }
671+
672+
Current.logging.log(prompt)
673+
674+
// security code
675+
return "000000"
676+
}
677+
678+
Current.network.dataTask = { convertible in
679+
680+
switch convertible.pmkRequest.url! {
681+
case .itcServiceKey:
682+
return fixture(for: .itcServiceKey,
683+
fileURL: Bundle.module.url(forResource: "ITCServiceKey", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
684+
statusCode: 200,
685+
headers: ["Content-Type": "application/json"])
686+
case .signIn:
687+
if convertible.pmkRequest.httpMethod == "GET" {
688+
return fixture(for: .signIn,
689+
fileURL: Bundle.module.url(forResource: "Federate", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
690+
statusCode: 200,
691+
headers: ["Content-Type": "application/json",
692+
"X-Apple-HC-Bits": "10",
693+
"X-Apple-HC-Challenge": "somestring",
694+
"scnt": ""])
695+
} else {
696+
return fixture(for: .signIn,
697+
fileURL: Bundle.module.url(forResource: "SignIn", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
698+
statusCode: 503,
699+
headers: ["Content-Type": "text/html",
700+
"X-Apple-ID-Session-Id": "",
701+
"scnt": ""])
702+
}
703+
case .authOptions:
704+
return fixture(for: .authOptions,
705+
fileURL: Bundle.module.url(forResource: "AuthOptions", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
706+
statusCode: 200,
707+
headers: ["Content-Type": "application/json",
708+
"X-Apple-ID-Session-Id": "",
709+
"scnt": ""])
710+
case .submitSecurityCode(.device(code: "000000")):
711+
return fixture(for: .submitSecurityCode(.device(code: "000000")),
712+
statusCode: 204,
713+
headers: ["Content-Type": "application/json",
714+
"X-Apple-ID-Session-Id": "",
715+
"scnt": ""])
716+
case .trust:
717+
return fixture(for: .trust,
718+
statusCode: 204,
719+
headers: [:])
720+
case .olympusSession:
721+
return fixture(for: .olympusSession,
722+
fileURL: Bundle.module.url(forResource: "OlympusSession", withExtension: "json", subdirectory: "Fixtures/Login_Service_Temporarily_Unavailable")!,
723+
statusCode: 200,
724+
headers: ["Content-Type": "application/json",
725+
"X-Apple-ID-Session-Id": "",
726+
"scnt": ""])
727+
default:
728+
print(convertible.pmkRequest.url!)
729+
XCTFail()
730+
return .init(error: PMKError.invalidCallingConvention)
731+
}
732+
}
733+
734+
let expectation = self.expectation(description: "promise fulfills")
735+
736+
let client = Client()
737+
client.login(accountName: "test@example.com", password: "ABC123")
738+
.tap { result in
739+
guard case .rejected(let error as AppleAPI.Client.Error) = result else {
740+
XCTFail("login fulfilled, but should have rejected with .noTrustedPhoneNumbers error")
741+
return
742+
}
743+
XCTAssertEqual(error, AppleAPI.Client.Error.serviceTemporarilyUnavailable)
744+
expectation.fulfill()
745+
}
746+
.cauterize()
747+
748+
wait(for: [expectation], timeout: 1.0)
749+
750+
XCTAssertEqual(log, "")
751+
}
752+
753+
664754
func testValidHashCashMint() {
665755
let bits: UInt = 11
666756
let resource = "4d74fb15eb23f465f1f6fcbf534e5877"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"trustedPhoneNumbers" : [ {
3+
"obfuscatedNumber" : "(•••) •••-••00",
4+
"pushMode" : "sms",
5+
"numberWithDialCode" : "+1 (•••) •••-••00",
6+
"id" : 1
7+
} ],
8+
"securityCode" : {
9+
"length" : 6,
10+
"tooManyCodesSent" : false,
11+
"tooManyCodesValidated" : false,
12+
"securityCodeLocked" : false,
13+
"securityCodeCooldown" : false
14+
},
15+
"authenticationType" : "hsa2",
16+
"recoveryUrl" : "https://iforgot.apple.com/phone/add?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142",
17+
"cantUsePhoneNumberUrl" : "https://iforgot.apple.com/iforgot/phone/add?context=cantuse&prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142",
18+
"recoveryWebUrl" : "https://iforgot.apple.com/password/verify/appleid?prs_account_nm=test%40example.com&autoSubmitAccount=true&appId=142",
19+
"repairPhoneNumberUrl" : "https://gsa.apple.com/appleid/account/manage/repair/verify/phone",
20+
"repairPhoneNumberWebUrl" : "https://appleid.apple.com/widget/account/repair?#!repair",
21+
"aboutTwoFactorAuthenticationUrl" : "https://support.apple.com/kb/HT204921",
22+
"autoVerified" : false,
23+
"showAutoVerificationUI" : false,
24+
"managedAccount" : false,
25+
"trustedPhoneNumber" : {
26+
"obfuscatedNumber" : "(•••) •••-••00",
27+
"pushMode" : "sms",
28+
"numberWithDialCode" : "+1 (•••) •••-••00",
29+
"id" : 1
30+
},
31+
"hsa2Account" : true,
32+
"restrictedAccount" : false,
33+
"supportsRecovery" : true
34+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"authType" : "hsa2"
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"authServiceUrl" : "https://idmsa.apple.com/appleauth",
3+
"authServiceKey" : "NNNNN"
4+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
{
2+
"user" : {
3+
"fullName" : "Test User",
4+
"firstName" : "Test",
5+
"lastName" : "User",
6+
"emailAddress" : "test@example.com",
7+
"prsId" : "000000000"
8+
},
9+
"provider" : {
10+
"providerId" : 00000,
11+
"name" : "Test User",
12+
"contentTypes" : [ "SOFTWARE" ],
13+
"subType" : "INDIVIDUAL",
14+
"pla" : [ {
15+
"id" : "1BC01216-52D4-43DC-8555-195F4454C348",
16+
"version" : "5014",
17+
"types" : [ "contractContentTypeDisplay.iOSFreeApps", "contractContentTypeDisplay.MacOSXFreeApplications" ],
18+
"contractCountryOfOrigins" : [ "CAN" ]
19+
} ]
20+
},
21+
"theme" : "APPSTORE_CONNECT",
22+
"availableProviders" : [ {
23+
"providerId" : 000000,
24+
"name" : "Test User",
25+
"contentTypes" : [ "SOFTWARE" ],
26+
"subType" : "INDIVIDUAL"
27+
} ],
28+
"backingType" : "ITC",
29+
"backingTypes" : [ "ITC" ],
30+
"roles" : [ "ADMIN", "LEGAL" ],
31+
"unverifiedRoles" : [ ],
32+
"featureFlags" : [ "showWwdrUserRoles", "adpRad", "apiKeys" ],
33+
"agreeToTerms" : true,
34+
"termsSignatures" : [ "ASC", "RAD" ],
35+
"modules" : [ {
36+
"key" : "Apps",
37+
"name" : "ITC.HomePage.Apps.IconText",
38+
"localizedName" : "My Apps",
39+
"url" : "https://appstoreconnect.apple.com/apps",
40+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Apps@2x.d3ce493e56172e92aed6.png",
41+
"down" : false,
42+
"visible" : true,
43+
"hasNotifications" : false
44+
}, {
45+
"key" : "AppAnalytics",
46+
"name" : "ITC.HomePage.AppAnalytics.IconText",
47+
"localizedName" : "App Analytics",
48+
"url" : "https://analytics.itunes.apple.com/",
49+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/AppAnalytics@2x.e19f711d943cb42d65b2.png",
50+
"down" : false,
51+
"visible" : true,
52+
"hasNotifications" : false
53+
}, {
54+
"key" : "SalesTrends",
55+
"name" : "ITC.HomePage.SalesTrends.IconText",
56+
"localizedName" : "Sales and Trends",
57+
"url" : "https://appstoreconnect.apple.com/trends",
58+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/SalesTrends@2x.b1f802112426525d990a.png",
59+
"down" : false,
60+
"visible" : true,
61+
"hasNotifications" : false
62+
}, {
63+
"key" : "FinancialReports",
64+
"name" : "ITC.HomePage.FinancialReports.IconText",
65+
"localizedName" : "Payments and Financial Reports",
66+
"url" : "https://appstoreconnect.apple.com/itc/payments_and_financial_reports",
67+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/FinancialReports@2x.a7b266a5136cc65c643b.png",
68+
"down" : false,
69+
"visible" : true,
70+
"hasNotifications" : false
71+
}, {
72+
"key" : "Account",
73+
"name" : "ITC.HomePage.Account.IconText",
74+
"localizedName" : "Users and Access",
75+
"url" : "https://appstoreconnect.apple.com/access/users",
76+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ManageUsers@2x.81511f3933fb2fb4b20d.png",
77+
"down" : false,
78+
"visible" : true,
79+
"hasNotifications" : false
80+
}, {
81+
"key" : "ContractsTaxBanking",
82+
"name" : "ITC.HomePage.ContractsTaxBanking.IconText",
83+
"localizedName" : "Agreements, Tax, and Banking",
84+
"url" : "https://appstoreconnect.apple.com/WebObjects/iTunesConnect.woa/da/jumpTo?page=contracts",
85+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/ContractsTaxBanking@2x.74466eb8570dd797602e.png",
86+
"down" : false,
87+
"visible" : true,
88+
"hasNotifications" : false
89+
}, {
90+
"key" : "Resources",
91+
"name" : "ITC.HomePage.Resources.IconText",
92+
"localizedName" : "Resources and Help",
93+
"url" : "https://developer.apple.com/app-store-connect/",
94+
"iconUrl" : "https://appstoreconnect.apple.com/static/img/ico_homepage/themed/apps/Resources@2x.3c8d0d8c08e876cf9470.png",
95+
"down" : false,
96+
"visible" : true,
97+
"hasNotifications" : false
98+
} ],
99+
"helpLinks" : [ {
100+
"key" : "AllAsc",
101+
"url" : "https://help.apple.com/app-store-connect/",
102+
"localizedText" : "App Store Connect Resources"
103+
}, {
104+
"key" : "Xcode",
105+
"url" : "https://help.apple.com/xcode/mac/current/",
106+
"localizedText" : "Xcode Help"
107+
}, {
108+
"key" : "SupportContact",
109+
"url" : "https://developer.apple.com/support/",
110+
"localizedText" : "Support and Contact"
111+
} ],
112+
"userProfile" : [ {
113+
"key" : "signIn",
114+
"url" : "https://appstoreconnect.apple.com/login",
115+
"localizedText" : "Sign In"
116+
}, {
117+
"key" : "personalDetails",
118+
"url" : "https://appstoreconnect.apple.com/access/users/07E3E586-44B1-48D3-BF8D-430F754F1BAA/settings",
119+
"localizedText" : "Edit Profile"
120+
}, {
121+
"key" : "signOut",
122+
"url" : "https://appstoreconnect.apple.com/logout",
123+
"localizedText" : "Sign Out"
124+
} ],
125+
"pccDto" : null,
126+
"publicUserId" : "07E3E586-44B1-48D3-BF8D-430F754F1BAA",
127+
"ofacState" : null
128+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<html>
2+
3+
<head>
4+
<title>503 Service Temporarily Unavailable</title>
5+
</head>
6+
7+
<body>
8+
<center>
9+
<h1>503 Service Temporarily Unavailable</h1>
10+
</center>
11+
<hr>
12+
<center>Apple</center>
13+
</body>
14+
15+
</html>

0 commit comments

Comments
 (0)