diff --git a/cmd/auth.go b/cmd/auth.go index 34e1d4a2..0adf2e78 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -47,7 +47,7 @@ func loginCmd() *cobra.Command { Use: "login", Short: "Login to the App Store", RunE: func(cmd *cobra.Command, args []string) error { - interactive := cmd.Context().Value("interactive").(bool) + interactive, _ := cmd.Context().Value(interactiveKey).(bool) if password == "" && !interactive { return errors.New("password is required when not running in interactive mode; use the \"--password\" flag") diff --git a/cmd/download.go b/cmd/download.go index 0df1d366..06151d10 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -35,6 +35,11 @@ func downloadCmd() *cobra.Command { purchased := false return retry.Do(func() error { + bag, err := dependencies.AppStore.Bag(appstore.BagInput{}) + if err != nil { + return fmt.Errorf("failed to get bag: %w", err) + } + infoResult, err := dependencies.AppStore.AccountInfo() if err != nil { return err @@ -43,15 +48,10 @@ 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) - } - loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{ Email: acc.Email, Password: acc.Password, - Endpoint: bagOutput.AuthEndpoint, + Endpoint: bag.AuthEndpoint, }) if err != nil { return err @@ -81,7 +81,7 @@ func downloadCmd() *cobra.Command { Msg("purchase") } - interactive, _ := cmd.Context().Value("interactive").(bool) + interactive, _ := cmd.Context().Value(interactiveKey).(bool) var progress *progressbar.ProgressBar if interactive { progress = progressbar.NewOptions64(1, @@ -101,7 +101,7 @@ func downloadCmd() *cobra.Command { } out, err := dependencies.AppStore.Download(appstore.DownloadInput{ - Account: acc, App: app, OutputPath: outputPath, Progress: progress, ExternalVersionID: externalVersionID}) + Account: acc, App: app, OutputPath: outputPath, Progress: progress, ExternalVersionID: externalVersionID, Endpoint: bag.DownloadEndpoint}) if err != nil { return err } diff --git a/cmd/get_version_metadata.go b/cmd/get_version_metadata.go index 3fcd3367..2b8a1668 100644 --- a/cmd/get_version_metadata.go +++ b/cmd/get_version_metadata.go @@ -30,6 +30,11 @@ func getVersionMetadataCmd() *cobra.Command { var acc appstore.Account return retry.Do(func() error { + bag, err := dependencies.AppStore.Bag(appstore.BagInput{}) + if err != nil { + return fmt.Errorf("failed to get bag: %w", err) + } + infoResult, err := dependencies.AppStore.AccountInfo() if err != nil { return err @@ -38,15 +43,10 @@ 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) - } - loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{ Email: acc.Email, Password: acc.Password, - Endpoint: bagOutput.AuthEndpoint, + Endpoint: bag.AuthEndpoint, }) if err != nil { return err @@ -69,6 +69,7 @@ func getVersionMetadataCmd() *cobra.Command { Account: acc, App: app, VersionID: externalVersionID, + Endpoint: bag.DownloadEndpoint, }) if err != nil { return err diff --git a/cmd/list_versions.go b/cmd/list_versions.go index a583703c..dc195b26 100644 --- a/cmd/list_versions.go +++ b/cmd/list_versions.go @@ -29,6 +29,11 @@ func ListVersionsCmd() *cobra.Command { var acc appstore.Account return retry.Do(func() error { + bag, err := dependencies.AppStore.Bag(appstore.BagInput{}) + if err != nil { + return fmt.Errorf("failed to get bag: %w", err) + } + infoResult, err := dependencies.AppStore.AccountInfo() if err != nil { return err @@ -37,15 +42,10 @@ 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) - } - loginResult, err := dependencies.AppStore.Login(appstore.LoginInput{ Email: acc.Email, Password: acc.Password, - Endpoint: bagOutput.AuthEndpoint, + Endpoint: bag.AuthEndpoint, }) if err != nil { return err @@ -64,7 +64,7 @@ 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, Endpoint: bag.DownloadEndpoint}) if err != nil { return err } diff --git a/pkg/appstore/appstore_bag.go b/pkg/appstore/appstore_bag.go index 29108a2f..b900f52c 100644 --- a/pkg/appstore/appstore_bag.go +++ b/pkg/appstore/appstore_bag.go @@ -11,7 +11,8 @@ import ( type BagInput struct{} type BagOutput struct { - AuthEndpoint string + AuthEndpoint string + DownloadEndpoint string } func (t *appstore) Bag(input BagInput) (BagOutput, error) { @@ -33,7 +34,8 @@ func (t *appstore) Bag(input BagInput) (BagOutput, error) { } return BagOutput{ - AuthEndpoint: res.Data.URLBag.AuthEndpoint, + AuthEndpoint: res.Data.URLBag.AuthEndpoint, + DownloadEndpoint: res.Data.URLBag.DownloadEndpoint, }, nil } @@ -42,7 +44,8 @@ type bagResult struct { } type urlBag struct { - AuthEndpoint string `plist:"authenticateAccount,omitempty"` + AuthEndpoint string `plist:"authenticateAccount,omitempty"` + DownloadEndpoint string `plist:"redownloadProduct,omitempty"` } func (*appstore) bagRequest(guid string) http.Request { diff --git a/pkg/appstore/appstore_bag_test.go b/pkg/appstore/appstore_bag_test.go index ae9f1f27..ba7c5992 100644 --- a/pkg/appstore/appstore_bag_test.go +++ b/pkg/appstore/appstore_bag_test.go @@ -85,8 +85,11 @@ var _ = Describe("AppStore (Bag)", func() { }) }) - When("request is successful with authenticateAccount in urlBag", func() { - const testAuthEndpoint = "https://example.com" + When("request is successful with endpoints in urlBag", func() { + const ( + testAuthEndpoint = "https://example.com" + testDownloadEndpoint = "https://downloaddispatch.example.com/r/redownload" + ) BeforeEach(func() { mockMachine.EXPECT(). @@ -105,7 +108,8 @@ var _ = Describe("AppStore (Bag)", func() { StatusCode: gohttp.StatusOK, Data: bagResult{ URLBag: urlBag{ - AuthEndpoint: testAuthEndpoint, + AuthEndpoint: testAuthEndpoint, + DownloadEndpoint: testDownloadEndpoint, }, }, }, nil) @@ -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.DownloadEndpoint).To(Equal(testDownloadEndpoint)) }) }) diff --git a/pkg/appstore/appstore_download.go b/pkg/appstore/appstore_download.go index 4e714dab..23691eb1 100644 --- a/pkg/appstore/appstore_download.go +++ b/pkg/appstore/appstore_download.go @@ -24,6 +24,7 @@ type DownloadInput struct { OutputPath string Progress *progressbar.ProgressBar ExternalVersionID string + Endpoint string } type DownloadOutput struct { @@ -39,7 +40,7 @@ func (t *appstore) Download(input DownloadInput) (DownloadOutput, error) { guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "") - req := t.downloadRequest(input.Account, input.App, guid, input.ExternalVersionID) + req := t.downloadRequest(input.Endpoint, input.Account, input.App, guid, input.ExternalVersionID) res, err := t.downloadClient.Send(req) if err != nil { @@ -48,8 +49,7 @@ func (t *appstore) Download(input DownloadInput) (DownloadOutput, error) { if res.Data.FailureType == FailureTypePasswordTokenExpired || res.Data.FailureType == FailureTypeSignInRequired || - res.Data.FailureType == FailureTypeDeviceVerificationFailed || - res.Data.FailureType == FailureTypeLicenseAlreadyExists { + res.Data.FailureType == FailureTypeDeviceVerificationFailed { return DownloadOutput{}, ErrPasswordTokenExpired } @@ -172,7 +172,7 @@ 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 { +func (*appstore) downloadRequest(endpoint string, acc Account, app App, guid string, externalVersionID string) http.Request { payload := map[string]interface{}{ "creditDisplay": "", "guid": guid, @@ -180,16 +180,11 @@ func (*appstore) downloadRequest(acc Account, app App, guid string, externalVers } if externalVersionID != "" { - payload["externalVersionId"] = externalVersionID - } - - podPrefix := "" - if acc.Pod != "" { - podPrefix = "p" + acc.Pod + "-" + payload["appExtVrsId"] = externalVersionID } return http.Request{ - URL: fmt.Sprintf("https://%s%s%s?guid=%s", podPrefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathDownload, guid), + URL: fmt.Sprintf("%s?guid=%s", endpoint, guid), Method: http.MethodPOST, ResponseFormat: http.ResponseFormatXML, Headers: map[string]string{ diff --git a/pkg/appstore/appstore_download_test.go b/pkg/appstore/appstore_download_test.go index f5fb8f90..f33f8c68 100644 --- a/pkg/appstore/appstore_download_test.go +++ b/pkg/appstore/appstore_download_test.go @@ -97,10 +97,10 @@ var _ = Describe("AppStore (Download)", func() { }) }) - When("request uses a custom pod", func() { + When("request is sent", func() { const ( - testPod = "42" - testGUID = "001122334455" + testEndpoint = "https://downloaddispatch.example.com/r/redownload" + testGUID = "001122334455" ) BeforeEach(func() { @@ -111,18 +111,13 @@ var _ = Describe("AppStore (Download)", func() { mockDownloadClient.EXPECT(). Send(gomock.Any()). Do(func(req http.Request) { - expectedURL := "https://p" + testPod + "-" + PrivateAppStoreAPIDomain + PrivateAppStoreAPIPathDownload + "?guid=" + testGUID - Expect(req.URL).To(Equal(expectedURL)) + Expect(req.URL).To(Equal(testEndpoint + "?guid=" + testGUID)) }). Return(http.Result[downloadResult]{}, errors.New("")) }) - It("sends the download request to the pod-specific host", func() { - _, err := as.Download(DownloadInput{ - Account: Account{ - Pod: testPod, - }, - }) + It("sends the download request to the endpoint provided by the caller", func() { + _, err := as.Download(DownloadInput{Endpoint: testEndpoint}) Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/appstore/appstore_get_version_metadata.go b/pkg/appstore/appstore_get_version_metadata.go index efa182be..993c17ec 100644 --- a/pkg/appstore/appstore_get_version_metadata.go +++ b/pkg/appstore/appstore_get_version_metadata.go @@ -13,6 +13,7 @@ type GetVersionMetadataInput struct { Account Account App App VersionID string + Endpoint string } type GetVersionMetadataOutput struct { @@ -28,7 +29,7 @@ func (t *appstore) GetVersionMetadata(input GetVersionMetadataInput) (GetVersion guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "") - req := t.getVersionMetadataRequest(input.Account, input.App, guid, input.VersionID) + req := t.getVersionMetadataRequest(input.Endpoint, input.Account, input.App, guid, input.VersionID) res, err := t.downloadClient.Send(req) if err != nil { @@ -68,21 +69,16 @@ func (t *appstore) GetVersionMetadata(input GetVersionMetadataInput) (GetVersion return GetVersionMetadataOutput(metadata), nil } -func (t *appstore) getVersionMetadataRequest(acc Account, app App, guid string, version string) http.Request { +func (t *appstore) getVersionMetadataRequest(endpoint string, acc Account, app App, guid string, version string) http.Request { payload := map[string]interface{}{ - "creditDisplay": "", - "guid": guid, - "salableAdamId": app.ID, - "externalVersionId": version, - } - - podPrefix := "" - if acc.Pod != "" { - podPrefix = "p" + acc.Pod + "-" + "creditDisplay": "", + "guid": guid, + "salableAdamId": app.ID, + "appExtVrsId": version, } return http.Request{ - URL: fmt.Sprintf("https://%s%s%s?guid=%s", podPrefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathDownload, guid), + URL: fmt.Sprintf("%s?guid=%s", endpoint, guid), Method: http.MethodPOST, ResponseFormat: http.ResponseFormatXML, Headers: map[string]string{ diff --git a/pkg/appstore/appstore_get_version_metadata_test.go b/pkg/appstore/appstore_get_version_metadata_test.go index 8c0a72a8..9ef552db 100644 --- a/pkg/appstore/appstore_get_version_metadata_test.go +++ b/pkg/appstore/appstore_get_version_metadata_test.go @@ -222,10 +222,10 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() { }) }) - When("request uses a custom pod", func() { + When("request is sent", func() { const ( - testPod = "42" - testGUID = "001122334455" + testEndpoint = "https://downloaddispatch.example.com/r/redownload" + testGUID = "001122334455" ) BeforeEach(func() { @@ -236,18 +236,13 @@ var _ = Describe("AppStore (GetVersionMetadata)", func() { mockDownloadClient.EXPECT(). Send(gomock.Any()). Do(func(req http.Request) { - expectedURL := "https://p" + testPod + "-" + PrivateAppStoreAPIDomain + PrivateAppStoreAPIPathDownload + "?guid=" + testGUID - Expect(req.URL).To(Equal(expectedURL)) + Expect(req.URL).To(Equal(testEndpoint + "?guid=" + testGUID)) }). Return(http.Result[downloadResult]{}, errors.New("request error")) }) - It("sends the request to the pod-specific host", func() { - _, err := as.GetVersionMetadata(GetVersionMetadataInput{ - Account: Account{ - Pod: testPod, - }, - }) + It("sends the request to the endpoint provided by the caller", func() { + _, err := as.GetVersionMetadata(GetVersionMetadataInput{Endpoint: testEndpoint}) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to send http request")) }) diff --git a/pkg/appstore/appstore_list_versions.go b/pkg/appstore/appstore_list_versions.go index dc3c215d..95fa298c 100644 --- a/pkg/appstore/appstore_list_versions.go +++ b/pkg/appstore/appstore_list_versions.go @@ -9,8 +9,9 @@ import ( ) type ListVersionsInput struct { - Account Account - App App + Account Account + App App + Endpoint string } type ListVersionsOutput struct { @@ -26,7 +27,7 @@ func (t *appstore) ListVersions(input ListVersionsInput) (ListVersionsOutput, er guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "") - req := t.listVersionsRequest(input.Account, input.App, guid) + req := t.listVersionsRequest(input.Endpoint, input.Account, input.App, guid) res, err := t.downloadClient.Send(req) if err != nil { @@ -76,20 +77,15 @@ func (t *appstore) ListVersions(input ListVersionsInput) (ListVersionsOutput, er }, nil } -func (t *appstore) listVersionsRequest(acc Account, app App, guid string) http.Request { +func (t *appstore) listVersionsRequest(endpoint string, acc Account, app App, guid string) http.Request { payload := map[string]interface{}{ "creditDisplay": "", "guid": guid, "salableAdamId": app.ID, } - podPrefix := "" - if acc.Pod != "" { - podPrefix = "p" + acc.Pod + "-" - } - return http.Request{ - URL: fmt.Sprintf("https://%s%s%s?guid=%s", podPrefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathDownload, guid), + URL: fmt.Sprintf("%s?guid=%s", endpoint, guid), Method: http.MethodPOST, ResponseFormat: http.ResponseFormatXML, Headers: map[string]string{ diff --git a/pkg/appstore/appstore_list_versions_test.go b/pkg/appstore/appstore_list_versions_test.go index 7b4fb67a..1cd7a005 100644 --- a/pkg/appstore/appstore_list_versions_test.go +++ b/pkg/appstore/appstore_list_versions_test.go @@ -62,10 +62,10 @@ var _ = Describe("AppStore (ListVersions)", func() { }) }) - When("request uses a custom pod", func() { + When("request is sent", func() { const ( - testPod = "42" - testGUID = "001122334455" + testEndpoint = "https://downloaddispatch.example.com/r/redownload" + testGUID = "001122334455" ) BeforeEach(func() { @@ -76,18 +76,13 @@ var _ = Describe("AppStore (ListVersions)", func() { mockDownloadClient.EXPECT(). Send(gomock.Any()). Do(func(req http.Request) { - expectedURL := "https://p" + testPod + "-" + PrivateAppStoreAPIDomain + PrivateAppStoreAPIPathDownload + "?guid=" + testGUID - Expect(req.URL).To(Equal(expectedURL)) + Expect(req.URL).To(Equal(testEndpoint + "?guid=" + testGUID)) }). Return(http.Result[downloadResult]{}, errors.New("")) }) - It("sends the request to the pod-specific host", func() { - _, err := as.ListVersions(ListVersionsInput{ - Account: Account{ - Pod: testPod, - }, - }) + It("sends the request to the endpoint provided by the caller", func() { + _, err := as.ListVersions(ListVersionsInput{Endpoint: testEndpoint}) Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/appstore/constants.go b/pkg/appstore/constants.go index 66201514..a231054e 100644 --- a/pkg/appstore/constants.go +++ b/pkg/appstore/constants.go @@ -23,7 +23,6 @@ const ( PrivateAppStoreAPIDomain = "buy." + iTunesAPIDomain PrivateAppStoreAPIPathPurchase = "/WebObjects/MZFinance.woa/wa/buyProduct" - PrivateAppStoreAPIPathDownload = "/WebObjects/MZFinance.woa/wa/volumeStoreDownloadProduct" HTTPHeaderStoreFront = "X-Set-Apple-Store-Front" HTTPHeaderPod = "pod"