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
23 changes: 12 additions & 11 deletions cmd/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ func downloadCmd() *cobra.Command {

acc = infoResult.Account

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

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Expand Down Expand Up @@ -111,12 +111,13 @@ func downloadCmd() *cobra.Command {
}

out, err := dependencies.AppStore.Download(appstore.DownloadInput{
Account: acc,
App: app,
OutputPath: outputPath,
Progress: progress,
ExternalVersionID: externalVersionID,
Platform: platform,
Account: acc,
App: app,
OutputPath: outputPath,
Progress: progress,
ExternalVersionID: externalVersionID,
Platform: platform,
RedownloadEndpoint: bagOutput.RedownloadEndpoint,
})
if err != nil {
return err
Expand Down
17 changes: 9 additions & 8 deletions cmd/get_version_metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ func getVersionMetadataCmd() *cobra.Command {

acc = infoResult.Account

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

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Expand All @@ -66,9 +66,10 @@ func getVersionMetadataCmd() *cobra.Command {
}

out, err := dependencies.AppStore.GetVersionMetadata(appstore.GetVersionMetadataInput{
Account: acc,
App: app,
VersionID: externalVersionID,
Account: acc,
App: app,
VersionID: externalVersionID,
RedownloadEndpoint: bagOutput.RedownloadEndpoint,
})
if err != nil {
return err
Expand Down
16 changes: 10 additions & 6 deletions cmd/list_versions.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ func ListVersionsCmd() *cobra.Command {

acc = infoResult.Account

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

if errors.Is(lastErr, appstore.ErrPasswordTokenExpired) {
loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{
Email: acc.Email,
Password: acc.Password,
Expand All @@ -64,7 +64,11 @@ func ListVersionsCmd() *cobra.Command {
app = lookupResult.App
}

out, err := dependencies.AppStore.ListVersions(appstore.ListVersionsInput{Account: acc, App: app})
out, err := dependencies.AppStore.ListVersions(appstore.ListVersionsInput{
Account: acc,
App: app,
RedownloadEndpoint: bagOutput.RedownloadEndpoint,
})
if err != nil {
return err
}
Expand Down
9 changes: 6 additions & 3 deletions pkg/appstore/appstore_bag.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import (
type BagInput struct{}

type BagOutput struct {
AuthEndpoint string
AuthEndpoint string
RedownloadEndpoint string
}

func (t *appstore) Bag(input BagInput) (BagOutput, error) {
Expand All @@ -33,7 +34,8 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) {
}

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

Expand All @@ -42,7 +44,8 @@ type bagResult struct {
}

type urlBag struct {
AuthEndpoint string `plist:"authenticateAccount,omitempty"`
AuthEndpoint string `plist:"authenticateAccount,omitempty"`
RedownloadEndpoint string `plist:"redownloadProduct,omitempty"`
}

func (*appstore) bagRequest(guid string) http.Request {
Expand Down
9 changes: 7 additions & 2 deletions pkg/appstore/appstore_bag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ var _ = Describe("AppStore (Bag)", func() {
})

When("request is successful with authenticateAccount in urlBag", func() {
const testAuthEndpoint = "https://example.com"
const (
testAuthEndpoint = "https://example.com"
testRedownloadEndpoint = "https://downloaddispatch.itunes.apple.com/r/redownload"
)

BeforeEach(func() {
mockMachine.EXPECT().
Expand All @@ -105,7 +108,8 @@ var _ = Describe("AppStore (Bag)", func() {
StatusCode: gohttp.StatusOK,
Data: bagResult{
URLBag: urlBag{
AuthEndpoint: testAuthEndpoint,
AuthEndpoint: testAuthEndpoint,
RedownloadEndpoint: testRedownloadEndpoint,
},
},
}, nil)
Expand All @@ -115,6 +119,7 @@ var _ = Describe("AppStore (Bag)", func() {
out, err := as.Bag(BagInput{})
Expect(err).ToNot(HaveOccurred())
Expect(out.AuthEndpoint).To(Equal(testAuthEndpoint))
Expect(out.RedownloadEndpoint).To(Equal(testRedownloadEndpoint))
})
})

Expand Down
77 changes: 12 additions & 65 deletions pkg/appstore/appstore_download.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"strconv"
"strings"

"github.com/majd/ipatool/v2/pkg/http"
"github.com/schollz/progressbar/v3"
"howett.net/plist"
)
Expand All @@ -19,12 +18,13 @@ var (
)

type DownloadInput struct {
Account Account
App App
OutputPath string
Progress *progressbar.ProgressBar
ExternalVersionID string
Platform Platform
Account Account
App App
OutputPath string
Progress *progressbar.ProgressBar
ExternalVersionID string
Platform Platform
RedownloadEndpoint string
}

type DownloadOutput struct {
Expand All @@ -48,38 +48,16 @@ func (t *appstore) Download(input DownloadInput) (DownloadOutput, error) {
}
}

req := t.downloadRequest(input.Account, input.App, guid, externalVersionID)

res, err := t.downloadClient.Send(req)
res, err := t.sendDownloadProduct(input.Account, input.App, guid, externalVersionID, input.RedownloadEndpoint)
if err != nil {
return DownloadOutput{}, fmt.Errorf("failed to send http request: %w", err)
}

if res.Data.FailureType == FailureTypePasswordTokenExpired ||
res.Data.FailureType == FailureTypeSignInRequired ||
res.Data.FailureType == FailureTypeDeviceVerificationFailed ||
res.Data.FailureType == FailureTypeLicenseAlreadyExists {
return DownloadOutput{}, ErrPasswordTokenExpired
}

if res.Data.FailureType == FailureTypeLicenseNotFound {
return DownloadOutput{}, ErrLicenseRequired
return DownloadOutput{}, err
}

if res.Data.FailureType != "" && res.Data.CustomerMessage != "" {
return DownloadOutput{}, NewErrorWithMetadata(fmt.Errorf("received error: %s", res.Data.CustomerMessage), res)
}

if res.Data.FailureType != "" {
return DownloadOutput{}, NewErrorWithMetadata(fmt.Errorf("received error: %s", res.Data.FailureType), res)
}

if len(res.Data.Items) == 0 {
return DownloadOutput{}, NewErrorWithMetadata(errors.New("invalid response"), res)
item, err := downloadProductItem(res)
if err != nil {
return DownloadOutput{}, err
}

item := res.Data.Items[0]

version := "unknown"

// Read the version from the item metadata
Expand Down Expand Up @@ -239,37 +217,6 @@ func (t *appstore) downloadFile(src, dst string, progress *progressbar.ProgressB
return nil
}

func (*appstore) downloadRequest(acc Account, app App, guid string, externalVersionID string) http.Request {
payload := map[string]interface{}{
"creditDisplay": "",
"guid": guid,
"salableAdamId": app.ID,
}

if externalVersionID != "" {
payload["externalVersionId"] = externalVersionID
}

podPrefix := ""
if acc.Pod != "" {
podPrefix = "p" + acc.Pod + "-"
}

return http.Request{
URL: fmt.Sprintf("https://%s%s%s?guid=%s", podPrefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathDownload, guid),
Method: http.MethodPOST,
ResponseFormat: http.ResponseFormatXML,
Headers: map[string]string{
"Content-Type": "application/x-apple-plist",
"iCloud-DSID": acc.DirectoryServicesID,
"X-Dsid": acc.DirectoryServicesID,
},
Payload: &http.XMLPayload{
Content: payload,
},
}
}

func fileName(app App, version string) string {
var parts []string

Expand Down
56 changes: 56 additions & 0 deletions pkg/appstore/appstore_download_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,62 @@ var _ = Describe("AppStore (Download)", func() {
})
})

When("a licensed tvOS app falls back to the redownload endpoint", func() {
const testRedownload = "https://downloaddispatch.itunes.apple.com/r/redownload"

BeforeEach(func() {
mockMachine.EXPECT().
MacAddress().
Return("00:11:22:33:44:55", nil)

mockPlatformClient.EXPECT().
Send(gomock.Any()).
Return(http.Result[platformVersionLookupResult]{
StatusCode: 200,
Data: platformVersionLookupResult{
Results: map[string]platformVersionLookupItem{
"42": {
Offers: []platformVersionLookupOffer{
{Version: platformVersionLookupVersion{ExternalID: platformVersionExternalID("123456")}},
},
},
},
},
}, nil)

gomock.InOrder(
mockDownloadClient.EXPECT().
Send(gomock.Any()).
Do(func(req http.Request) {
Expect(req.URL).To(ContainSubstring(PrivateAppStoreAPIPathDownload))
payload := req.Payload.(*http.XMLPayload)
Expect(payload.Content).To(HaveKeyWithValue(downloadVersionKeyVolumeStore, "123456"))
Expect(payload.Content).ToNot(HaveKey(downloadVersionKeyRedownload))
}).
Return(http.Result[downloadResult]{Data: downloadResult{FailureType: FailureTypeLicenseAlreadyExists}}, nil),
mockDownloadClient.EXPECT().
Send(gomock.Any()).
Do(func(req http.Request) {
Expect(req.URL).To(HavePrefix(testRedownload))
payload := req.Payload.(*http.XMLPayload)
Expect(payload.Content).To(HaveKeyWithValue(downloadVersionKeyRedownload, "123456"))
Expect(payload.Content).ToNot(HaveKey(downloadVersionKeyVolumeStore))
}).
Return(http.Result[downloadResult]{}, errors.New("stop after fallback")),
)
})

It("retries on redownload translating externalVersionId to appExtVrsId", func() {
_, err := as.Download(DownloadInput{
Account: Account{StoreFront: "143441"},
App: App{ID: 42},
Platform: PlatformAppleTV,
RedownloadEndpoint: testRedownload,
})
Expect(err).To(HaveOccurred())
})
})

DescribeTable("platform uses the standard download request",
func(platform Platform) {
mockMachine.EXPECT().
Expand Down
Loading
Loading