diff --git a/cmd/common.go b/cmd/common.go index 3b3a3dec..32465026 100644 --- a/cmd/common.go +++ b/cmd/common.go @@ -54,8 +54,16 @@ func newLogger(format OutputFormat, verbose bool) log.Logger { // newCookieJar returns a new cookie jar instance. func newCookieJar(machine machine.Machine) http.CookieJar { + cookiePath := filepath.Join(machine.HomeDirectory(), ConfigDirectoryName, CookieJarFileName) + // Remove stale zero-byte lock files. The juju/go4/lock library only does + // PID-based stale detection when the file has content; a 0-byte lock file + // (left by a process killed between O_TRUNC and PID write) blocks forever. + lockPath := cookiePath + ".lock" + if fi, err := os.Stat(lockPath); err == nil && fi.Size() == 0 { + os.Remove(lockPath) + } return util.Must(cookiejar.New(&cookiejar.Options{ - Filename: filepath.Join(machine.HomeDirectory(), ConfigDirectoryName, CookieJarFileName), + Filename: cookiePath, })) } diff --git a/pkg/appstore/appstore_bag.go b/pkg/appstore/appstore_bag.go index 29108a2f..d138ec4f 100644 --- a/pkg/appstore/appstore_bag.go +++ b/pkg/appstore/appstore_bag.go @@ -32,13 +32,32 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) { return BagOutput{}, fmt.Errorf("received unexpected status code: %d", res.StatusCode) } + endpoint := res.Data.AuthEndpoint + if endpoint == "" { + endpoint = res.Data.URLBag.AuthEndpoint + } + // The new auth endpoint requires the /fast sub-path WITH a trailing slash. + // Without the trailing slash Apple returns 301 + an HTML redirect page + // (which the plist parser chokes on), and only the appstored UA gets a + // (download-grade) token. With the slash, the Configurator UA mints a + // commerce-grade token that buyProduct accepts. + if strings.Contains(endpoint, "auth.itunes.apple.com") { + if !strings.HasSuffix(endpoint, "/fast") && !strings.HasSuffix(endpoint, "/fast/") { + endpoint = endpoint + "/fast" + } + if !strings.HasSuffix(endpoint, "/") { + endpoint = endpoint + "/" + } + } + return BagOutput{ - AuthEndpoint: res.Data.URLBag.AuthEndpoint, + AuthEndpoint: endpoint, }, nil } type bagResult struct { - URLBag urlBag `plist:"urlBag,omitempty"` + URLBag urlBag `plist:"urlBag,omitempty"` + AuthEndpoint string `plist:"authenticateAccount,omitempty"` } type urlBag struct { diff --git a/pkg/appstore/appstore_login.go b/pkg/appstore/appstore_login.go index 419fdeaa..bf9114f7 100644 --- a/pkg/appstore/appstore_login.go +++ b/pkg/appstore/appstore_login.go @@ -144,6 +144,8 @@ func (t *appstore) parseLoginResponse(res *http.Result[loginResult], attempt int err = ErrAuthCodeRequired } else if res.Data.FailureType == "" && res.Data.CustomerMessage == CustomerMessageAccountDisabled { err = NewErrorWithMetadata(errors.New("account is disabled"), res) + } else if res.Data.FailureType == "" && res.Data.CustomerMessage == CustomerMessageActionSignInPage { + err = NewErrorWithMetadata(errors.New("account requires browser sign-in (2FA or Apple ID review required)"), res) } else if res.Data.FailureType != "" { if res.Data.CustomerMessage != "" { err = NewErrorWithMetadata(errors.New(res.Data.CustomerMessage), res) @@ -160,7 +162,7 @@ func (t *appstore) parseLoginResponse(res *http.Result[loginResult], attempt int func (t *appstore) loginRequest(email, password, authCode, guid, endpoint string, attempt int) http.Request { return http.Request{ Method: http.MethodPOST, - URL: endpoint, + URL: util.IfEmpty(endpoint, fmt.Sprintf("https://%s%s", PrivateAuthDomain, PrivateAuthPathNative)), ResponseFormat: http.ResponseFormatXML, Headers: map[string]string{ "Content-Type": "application/x-www-form-urlencoded", diff --git a/pkg/appstore/constants.go b/pkg/appstore/constants.go index 66201514..b7a12f0f 100644 --- a/pkg/appstore/constants.go +++ b/pkg/appstore/constants.go @@ -13,6 +13,7 @@ const ( CustomerMessageAccountDisabled = "Your account is disabled." CustomerMessageSubscriptionRequired = "Subscription Required" CustomerMessagePasswordChanged = "Your password has changed." + CustomerMessageActionSignInPage = "AMD-Action::SP" iTunesAPIDomain = "itunes.apple.com" iTunesAPIPathSearch = "/search" @@ -25,6 +26,9 @@ const ( PrivateAppStoreAPIPathPurchase = "/WebObjects/MZFinance.woa/wa/buyProduct" PrivateAppStoreAPIPathDownload = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct" + PrivateAuthDomain = "auth." + iTunesAPIDomain + PrivateAuthPathNative = "/auth/v1/native/fast/" + HTTPHeaderStoreFront = "X-Set-Apple-Store-Front" HTTPHeaderPod = "pod" diff --git a/pkg/http/constants.go b/pkg/http/constants.go index 8f9c9f2f..0bf58af8 100644 --- a/pkg/http/constants.go +++ b/pkg/http/constants.go @@ -8,5 +8,9 @@ const ( ) const ( + // Configurator UA mints a commerce-grade password token (the "Aw..." token) + // that buyProduct accepts. The appstored UA only yields a download-grade + // token that buyProduct rejects with failureType 2034. Requires the auth + // endpoint to carry the trailing slash (see appstore_bag.go). DefaultUserAgent = "Configurator/2.17 (Macintosh; OS X 15.2; 24C5089c) AppleWebKit/0620.1.16.11.6" )