Skip to content
14 changes: 12 additions & 2 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"fmt"
"os"
"time"

Expand Down Expand Up @@ -42,7 +43,16 @@ func downloadCmd() *cobra.Command {
acc = infoResult.Account

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{Email: acc.Email, Password: acc.Password})
bagOutput, err := dependencies.AppStore.Bag(appstore.BagInput{})
if err != nil {
return fmt.Errorf("failed to get bag: %w", err)
}

loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Endpoint: bagOutput.AuthEndpoint,
})
Comment on lines -45 to +49
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, good catch. I had missed this before.

if err != nil {
return err
}
Expand Down Expand Up @@ -112,7 +122,7 @@ func downloadCmd() *cobra.Command {
retry.LastErrorOnly(true),
retry.DelayType(retry.FixedDelay),
retry.Delay(time.Millisecond),
retry.Attempts(2),
retry.Attempts(3),
retry.RetryIf(func(err error) bool {
lastErr = err

Expand Down
12 changes: 11 additions & 1 deletion cmd/get_version_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"fmt"
"time"

"github.com/avast/retry-go"
Expand Down Expand Up @@ -37,7 +38,16 @@ func getVersionMetadataCmd() *cobra.Command {
acc = infoResult.Account

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{Email: acc.Email, Password: acc.Password})
bagOutput, err := dependencies.AppStore.Bag(appstore.BagInput{})
if err != nil {
return fmt.Errorf("failed to get bag: %w", err)
}

loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Endpoint: bagOutput.AuthEndpoint,
})
if err != nil {
return err
}
Expand Down
12 changes: 11 additions & 1 deletion cmd/list_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"fmt"
"time"

"github.com/avast/retry-go"
Expand Down Expand Up @@ -36,7 +37,16 @@ func ListVersionsCmd() *cobra.Command {
acc = infoResult.Account

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{Email: acc.Email, Password: acc.Password})
bagOutput, err := dependencies.AppStore.Bag(appstore.BagInput{})
if err != nil {
return fmt.Errorf("failed to get bag: %w", err)
}

loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Endpoint: bagOutput.AuthEndpoint,
})
if err != nil {
return err
}
Expand Down
12 changes: 11 additions & 1 deletion cmd/purchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"errors"
"fmt"
"time"

"github.com/avast/retry-go"
Expand Down Expand Up @@ -29,7 +30,16 @@ func purchaseCmd() *cobra.Command {
acc = infoResult.Account

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{Email: acc.Email, Password: acc.Password})
bagOutput, err := dependencies.AppStore.Bag(appstore.BagInput{})
if err != nil {
return fmt.Errorf("failed to get bag: %w", err)
}

loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Endpoint: bagOutput.AuthEndpoint,
})
if err != nil {
return err
}
Expand Down
7 changes: 6 additions & 1 deletion pkg/appstore/appstore_bag.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) {
return BagOutput{}, fmt.Errorf("received unexpected status code: %d", res.StatusCode)
}

authEndpoint := res.Data.URLBag.AuthEndpoint
if authEndpoint == "" {
authEndpoint = fmt.Sprintf("https://%s%s", PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate)
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not so sure about this. The reason we retrieve the auth endpoint from the bag now is that the previous hardcoded endpoint was unstable and started failing as of a few weeks ago. What is the reason to have the fallback here? Does the bag endpoint omit the auth endpoint in some cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed it and instead use a DefaultAuthEndpoint constant directly in the retry paths.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant why do we need to use the default auth endpoint in first place? Can't the retry use the endpoint from the bag, or have you encountered cases where the bag was omitting the endpoints?


return BagOutput{
AuthEndpoint: res.Data.URLBag.AuthEndpoint,
AuthEndpoint: authEndpoint,
}, nil
}

Expand Down
23 changes: 22 additions & 1 deletion pkg/appstore/appstore_bag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ var _ = Describe("AppStore (Bag)", func() {
})
})

When("request is successful", func() {
When("request is successful with authenticateAccount in urlBag", func() {
const testAuthEndpoint = "https://example.com"

BeforeEach(func() {
Expand Down Expand Up @@ -117,4 +117,25 @@ var _ = Describe("AppStore (Bag)", func() {
Expect(out.AuthEndpoint).To(Equal(testAuthEndpoint))
})
})

When("request is successful but authenticateAccount is empty", func() {
BeforeEach(func() {
mockMachine.EXPECT().
MacAddress().
Return("aa:bb:cc:dd:ee:ff", nil)

mockBagClient.EXPECT().
Send(gomock.Any()).
Return(http.Result[bagResult]{
StatusCode: gohttp.StatusOK,
Data: bagResult{},
}, nil)
})

It("falls back to hardcoded auth endpoint", func() {
out, err := as.Bag(BagInput{})
Expect(err).ToNot(HaveOccurred())
Expect(out.AuthEndpoint).To(Equal("https://" + PrivateAppStoreAPIDomain + PrivateAppStoreAPIPathAuthenticate))
})
})
})
2 changes: 1 addition & 1 deletion pkg/appstore/appstore_purchase.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (t *appstore) purchaseWithParams(acc Account, app App, guid string, pricing
return ErrSubscriptionRequired
}

if res.Data.FailureType == FailureTypePasswordTokenExpired {
if res.Data.FailureType == FailureTypePasswordTokenExpired || res.Data.CustomerMessage == CustomerMessagePasswordChanged {
return ErrPasswordTokenExpired
Comment thread
birmacher marked this conversation as resolved.
Outdated
}

Expand Down
26 changes: 26 additions & 0 deletions pkg/appstore/appstore_purchase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,32 @@ var _ = Describe("AppStore (Purchase)", func() {
})
})

When("customer message indicates password has changed", func() {
BeforeEach(func() {
mockMachine.EXPECT().
MacAddress().
Return("00:00:00:00:00:00", nil)

mockPurchaseClient.EXPECT().
Send(gomock.Any()).
Return(http.Result[purchaseResult]{
Data: purchaseResult{
FailureType: "some_other_failure",
CustomerMessage: CustomerMessagePasswordChanged,
},
}, nil)
})

It("returns password token expired error", func() {
err := as.Purchase(PurchaseInput{
Account: Account{
StoreFront: "143441",
},
})
Expect(err).To(MatchError(ErrPasswordTokenExpired))
})
})

When("store API returns customer error message", func() {
BeforeEach(func() {
mockMachine.EXPECT().
Expand Down
6 changes: 4 additions & 2 deletions pkg/appstore/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
CustomerMessageBadLogin = "MZFinance.BadLogin.Configurator_message"
CustomerMessageAccountDisabled = "Your account is disabled."
CustomerMessageSubscriptionRequired = "Subscription Required"
CustomerMessagePasswordChanged = "Your password has changed."

iTunesAPIDomain = "itunes.apple.com"
iTunesAPIPathSearch = "/search"
Expand All @@ -17,8 +18,9 @@ const (
PrivateInitDomain = "init." + iTunesAPIDomain
PrivateInitPath = "/bag.xml"

PrivateAppStoreAPIDomain = "buy." + iTunesAPIDomain
PrivateAppStoreAPIPathPurchase = "/WebObjects/MZFinance.woa/wa/buyProduct"
PrivateAppStoreAPIDomain = "buy." + iTunesAPIDomain
PrivateAppStoreAPIPathAuthenticate = "/WebObjects/MZFinance.woa/wa/authenticate"
PrivateAppStoreAPIPathPurchase = "/WebObjects/MZFinance.woa/wa/buyProduct"
PrivateAppStoreAPIPathDownload = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct"

HTTPHeaderStoreFront = "X-Set-Apple-Store-Front"
Expand Down