Skip to content

Commit f9e021a

Browse files
committed
fix: support dynamic App Store auth endpoint discovery
Resolve issues with App Store authentication by implementing dynamic auth endpoint discovery. If the initial auth request fails due to endpoint redirects or changes, the client parses the XML plist unmarshal error for any new authentication endpoint and automatically retries the login. Ported from the fix in ipatool (majd/ipatool#490).
1 parent 3b681c7 commit f9e021a

5 files changed

Lines changed: 127 additions & 7 deletions

File tree

internal/appstore/auth_endpoint.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package appstore
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"html"
7+
"net/url"
8+
"regexp"
9+
"strings"
10+
)
11+
12+
var authEndpointURLPattern = regexp.MustCompile(`https?://[^\s"'<>]+`)
13+
14+
func normalizeAuthEndpoint(endpoints ...string) string {
15+
for _, endpoint := range endpoints {
16+
endpoint = strings.TrimSpace(endpoint)
17+
if endpoint == "" {
18+
continue
19+
}
20+
21+
normalized := normalizeNativeAuthEndpoint(endpoint)
22+
if normalized != "" {
23+
return normalized
24+
}
25+
26+
return endpoint
27+
}
28+
29+
return fmt.Sprintf("https://%s%s", PrivateAuthDomain, PrivateAuthPathNative)
30+
}
31+
32+
func authEndpointFromResponseError(err error) string {
33+
var decodeErr *ResponseDecodeError
34+
if !errors.As(err, &decodeErr) {
35+
return ""
36+
}
37+
38+
return authEndpointFromText(strings.Join(append(decodeErr.URLs, decodeErr.Body), " "))
39+
}
40+
41+
func authEndpointFromText(text string) string {
42+
text = html.UnescapeString(strings.ReplaceAll(text, `\/`, `/`))
43+
44+
matches := authEndpointURLPattern.FindAllString(text, -1)
45+
for _, match := range matches {
46+
if endpoint := normalizeNativeAuthEndpoint(strings.TrimRight(match, ".,;)")); endpoint != "" {
47+
return endpoint
48+
}
49+
}
50+
51+
return ""
52+
}
53+
54+
func normalizeNativeAuthEndpoint(endpoint string) string {
55+
parsed, err := url.Parse(endpoint)
56+
if err != nil || parsed.Host != PrivateAuthDomain {
57+
return ""
58+
}
59+
60+
path := strings.TrimRight(parsed.Path, "/")
61+
if !strings.HasSuffix(path, "/fast") {
62+
path = strings.TrimRight(path, "/") + "/fast"
63+
}
64+
65+
parsed.Path = path + "/"
66+
67+
return parsed.String()
68+
}

internal/appstore/bag.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type bagResult struct {
99
URLBag struct {
1010
AuthEndpoint string `plist:"authenticateAccount,omitempty"`
1111
} `plist:"urlBag,omitempty"`
12+
AuthEndpoint string `plist:"authenticateAccount,omitempty"`
1213
}
1314

1415
// bag fetches the App Store bag.xml and returns the authenticate endpoint URL.
@@ -32,9 +33,5 @@ func (c *Client) bag() (string, error) {
3233
return "", fmt.Errorf("bag: status %d", res.StatusCode)
3334
}
3435

35-
if out.URLBag.AuthEndpoint == "" {
36-
return "", fmt.Errorf("bag: no authenticate endpoint")
37-
}
38-
39-
return out.URLBag.AuthEndpoint, nil
36+
return normalizeAuthEndpoint(out.AuthEndpoint, out.URLBag.AuthEndpoint), nil
4037
}

internal/appstore/http.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ func (c *Client) send(method, url string, headers map[string]string, body []byte
7878
}
7979
case formatXML:
8080
if _, err := plist.Unmarshal(normalizePlist(data), out); err != nil {
81-
return nil, fmt.Errorf("decode plist: %w", err)
81+
return nil, &ResponseDecodeError{
82+
Cause: err,
83+
StatusCode: res.StatusCode,
84+
ContentType: res.Header.Get("Content-Type"),
85+
Body: truncateBody(data, 500),
86+
URLs: extractURLs(data),
87+
}
8288
}
8389
}
8490

@@ -124,3 +130,41 @@ func normalizePlist(body []byte) []byte {
124130

125131
return n
126132
}
133+
134+
type ResponseDecodeError struct {
135+
Cause error
136+
StatusCode int
137+
ContentType string
138+
Body string
139+
URLs []string
140+
}
141+
142+
func (e *ResponseDecodeError) Error() string {
143+
return fmt.Sprintf("failed to unmarshal xml: %v", e.Cause)
144+
}
145+
146+
func (e *ResponseDecodeError) Unwrap() error {
147+
return e.Cause
148+
}
149+
150+
var urlPattern = regexp.MustCompile(`https?://[^\s"'<>]+`)
151+
152+
func extractURLs(body []byte) []string {
153+
matches := urlPattern.FindAll(body, -1)
154+
155+
urls := make([]string, 0, len(matches))
156+
for _, match := range matches {
157+
urls = append(urls, string(match))
158+
}
159+
160+
return urls
161+
}
162+
163+
func truncateBody(body []byte, max int) string {
164+
trimmed := strings.TrimSpace(string(body))
165+
if len(trimmed) <= max {
166+
return trimmed
167+
}
168+
169+
return trimmed[:max] + "..."
170+
}

internal/appstore/login.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ func (c *Client) Login(email, password, authCode string) (*Account, error) {
3737
return nil, err
3838
}
3939

40-
url := endpoint
40+
authEndpoint := normalizeAuthEndpoint(endpoint)
41+
url := authEndpoint
4142

4243
var (
4344
res *http.Response
@@ -63,6 +64,13 @@ func (c *Client) Login(email, password, authCode string) (*Account, error) {
6364
"Content-Type": "application/x-www-form-urlencoded",
6465
}, body, formatXML, &out)
6566
if err != nil {
67+
if discoveredEndpoint := authEndpointFromResponseError(err); discoveredEndpoint != "" && discoveredEndpoint != authEndpoint {
68+
authEndpoint = discoveredEndpoint
69+
url = authEndpoint
70+
71+
continue
72+
}
73+
6674
return nil, fmt.Errorf("login: %w", err)
6775
}
6876

internal/appstore/types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ const (
5252
downloadPath = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct"
5353
authURL = "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate"
5454

55+
PrivateAuthDomain = "auth." + iTunesDomain
56+
PrivateAuthPathNative = "/auth/v1/native/fast/"
57+
5558
hdrStoreFront = "X-Set-Apple-Store-Front"
5659
hdrPod = "pod"
5760

0 commit comments

Comments
 (0)