Skip to content

Commit

Permalink
login: increment 'attempt' while following redirects
Browse files Browse the repository at this point in the history
Apple has introduced some dynamic GET parameters into their
redirects, forcing us to use the main domain ( no `p71-`
and such prefixes ) to obtain those parameters.

However, when following such redirects, we shall also increment
the `attempt` parameter ( something that was hard-coded before ).
  • Loading branch information
tux-mind committed Dec 4, 2024
1 parent 63ee6fc commit 2a5347c
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 47 deletions.
101 changes: 59 additions & 42 deletions pkg/appstore/appstore_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"

"github.com/majd/ipatool/v2/pkg/http"
"github.com/majd/ipatool/v2/pkg/util"
)

var (
Expand All @@ -31,7 +33,7 @@ func (t *appstore) Login(input LoginInput) (LoginOutput, error) {

guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "")

acc, err := t.login(input.Email, input.Password, input.AuthCode, guid, 0)
acc, err := t.login(input.Email, input.Password, input.AuthCode, guid)
if err != nil {
return LoginOutput{}, err
}
Expand Down Expand Up @@ -59,28 +61,36 @@ type loginResult struct {
PasswordToken string `plist:"passwordToken,omitempty"`
}

func (t *appstore) login(email, password, authCode, guid string, attempt int) (Account, error) {
request := t.loginRequest(email, password, authCode, guid)
res, err := t.loginClient.Send(request)

if err != nil {
return Account{}, fmt.Errorf("request failed: %w", err)
}

if attempt == 0 && res.Data.FailureType == FailureTypeInvalidCredentials {
return t.login(email, password, authCode, guid, 1)
}

if res.Data.FailureType != "" && res.Data.CustomerMessage != "" {
return Account{}, NewErrorWithMetadata(errors.New(res.Data.CustomerMessage), res)
func (t *appstore) login(email, password, authCode, guid string) (Account, error) {
redirect := ""
var err error

Check failure on line 66 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

declarations should never be cuddled (wsl)
retry := true

Check failure on line 67 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

assignments should only be cuddled with other assignments (wsl)
var res http.Result[loginResult]

Check failure on line 68 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

declarations should never be cuddled (wsl)

for attempt := 1; retry && attempt <= 4; attempt++ {
ac := authCode
if attempt == 1 {
ac = ""
}
request := t.loginRequest(email, password, ac, guid, attempt)

Check failure on line 75 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

assignments should only be cuddled with other assignments (wsl)
request.URL, redirect = util.IfEmpty(redirect, request.URL), ""

Check failure on line 76 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

ineffectual assignment to redirect (ineffassign)
res, err = t.loginClient.Send(request)
if err != nil {

Check failure on line 78 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

only one cuddle assignment allowed before if statement (wsl)
return Account{}, fmt.Errorf("request failed: %w", err)
}

if retry, redirect, err = t.parseLoginResponse(&res, attempt, authCode); err != nil {
return Account{}, err
}
}

if res.Data.FailureType != "" {
return Account{}, NewErrorWithMetadata(errors.New("something went wrong"), res)
if retry {
return Account{}, NewErrorWithMetadata(errors.New("too many attempts"), res)
}

if res.Data.FailureType == "" && authCode == "" && res.Data.CustomerMessage == CustomerMessageBadLogin {
return Account{}, ErrAuthCodeRequired
sf, err := res.GetHeader(HTTPHeaderStoreFront)
if err != nil {
return Account{}, NewErrorWithMetadata(fmt.Errorf("failed to get storefront header: %w", err), res)
}

addr := res.Data.Account.Address
Expand All @@ -89,7 +99,7 @@ func (t *appstore) login(email, password, authCode, guid string, attempt int) (A
Email: res.Data.Account.Email,
PasswordToken: res.Data.PasswordToken,
DirectoryServicesID: res.Data.DirectoryServicesID,
StoreFront: res.Headers[HTTPHeaderStoreFront],
StoreFront: sf,
Password: password,
}

Expand All @@ -106,39 +116,46 @@ func (t *appstore) login(email, password, authCode, guid string, attempt int) (A
return acc, nil
}

func (t *appstore) loginRequest(email, password, authCode, guid string) http.Request {
attempt := "4"
if authCode != "" {
attempt = "2"
func (t *appstore) parseLoginResponse(res *http.Result[loginResult], attempt int, authCode string) (retry bool, redirect string, err error) {

Check failure on line 119 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

named return "retry" with type "bool" found (nonamedreturns)
if res.StatusCode == 302 {
if redirect, err = res.GetHeader("location"); err != nil {
err = fmt.Errorf("failed to retrieve redirect location: %w", err)
} else {
retry = true
}
} else if attempt == 1 && res.Data.FailureType == FailureTypeInvalidCredentials {
retry = true
} else if res.Data.FailureType == "" && authCode == "" && res.Data.CustomerMessage == CustomerMessageBadLogin {
err = ErrAuthCodeRequired
} else if res.Data.FailureType != "" {
if res.Data.CustomerMessage != "" {
err = NewErrorWithMetadata(errors.New(res.Data.CustomerMessage), res)
} else {
err = NewErrorWithMetadata(errors.New("something went wrong"), res)
}
} else if res.StatusCode != 200 || res.Data.PasswordToken == "" || res.Data.DirectoryServicesID == "" {
err = NewErrorWithMetadata(errors.New("something went wrong"), res)
}
return

Check failure on line 139 in pkg/appstore/appstore_login.go

View workflow job for this annotation

GitHub Actions / Lint

return statements should not be cuddled if block has more than two lines (wsl)
}

func (t *appstore) loginRequest(email, password, authCode, guid string, attempt int) http.Request {
return http.Request{
Method: http.MethodPOST,
URL: t.authDomain(authCode, guid),
URL: fmt.Sprintf("https://%s%s", PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate),
ResponseFormat: http.ResponseFormatXML,
Headers: map[string]string{
"Content-Type": "application/x-www-form-urlencoded",
},
Payload: &http.XMLPayload{
Content: map[string]interface{}{
"appleId": email,
"attempt": attempt,
"createSession": "true",
"guid": guid,
"password": fmt.Sprintf("%s%s", password, authCode),
"rmp": "0",
"why": "signIn",
"appleId": email,
"attempt": strconv.Itoa(attempt),
"guid": guid,
"password": fmt.Sprintf("%s%s", password, authCode),
"rmp": "0",
"why": "signIn",
},
},
}
}

func (*appstore) authDomain(authCode, guid string) string {
prefix := PrivateAppStoreAPIDomainPrefixWithoutAuthCode
if authCode != "" {
prefix = PrivateAppStoreAPIDomainPrefixWithAuthCode
}

return fmt.Sprintf(
"https://%s-%s%s?guid=%s", prefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate, guid)
}
64 changes: 62 additions & 2 deletions pkg/appstore/appstore_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,24 +135,82 @@ var _ = Describe("AppStore (Login)", func() {
}, nil)
})

It("returns error", func() {
It("returns ErrAuthCodeRequired error", func() {
_, err := as.Login(LoginInput{
Password: testPassword,
})
Expect(err).To(HaveOccurred())
Expect(err).To(Equal(ErrAuthCodeRequired))
})
})

When("store API redirects", func() {
const (
testRedirectLocation = "https://" + PrivateAppStoreAPIDomain + PrivateAppStoreAPIPathAuthenticate + "?PRH=31&Pod=31"
)

BeforeEach(func() {
firstCall := mockClient.EXPECT().
Send(gomock.Any()).
Do(func(req http.Request) {
Expect(req.Payload).To(BeAssignableToTypeOf(&http.XMLPayload{}))
x := req.Payload.(*http.XMLPayload)
Expect(x.Content).To(HaveKeyWithValue("attempt", "1"))
}).
Return(http.Result[loginResult]{
StatusCode: 302,
Headers: map[string]string{"Location": testRedirectLocation},
}, nil)
secondCall := mockClient.EXPECT().
Send(gomock.Any()).
Do(func(req http.Request) {
Expect(req.URL).To(Equal(testRedirectLocation))
Expect(req.Payload).To(BeAssignableToTypeOf(&http.XMLPayload{}))
x := req.Payload.(*http.XMLPayload)
Expect(x.Content).To(HaveKeyWithValue("attempt", "2"))
}).
Return(http.Result[loginResult]{}, errors.New("test complete"))
gomock.InOrder(firstCall, secondCall)
})

It("follows the redirect and increments attempt", func() {
_, err := as.Login(LoginInput{
Password: testPassword,
})
Expect(err).To(MatchError("request failed: test complete"))
})
})

When("store API redirects too much", func() {
BeforeEach(func() {
mockClient.EXPECT().
Send(gomock.Any()).
Return(http.Result[loginResult]{
StatusCode: 302,
Headers: map[string]string{"Location": "hello"},
}, nil).
Times(4)
})
It("bails out", func() {
_, err := as.Login(LoginInput{
Password: testPassword,
})
Expect(err).To(MatchError("too many attempts"))
})
})

When("store API returns valid response", func() {
const (
testPasswordToken = "test-password-token"
testDirectoryServicesID = "directory-services-id"
testStoreFront = "test-storefront"
)

BeforeEach(func() {
mockClient.EXPECT().
Send(gomock.Any()).
Return(http.Result[loginResult]{
StatusCode: 200,
Headers: map[string]string{HTTPHeaderStoreFront: testStoreFront},
Data: loginResult{
PasswordToken: testPasswordToken,
DirectoryServicesID: testDirectoryServicesID,
Expand All @@ -178,6 +236,7 @@ var _ = Describe("AppStore (Login)", func() {
PasswordToken: testPasswordToken,
Password: testPassword,
DirectoryServicesID: testDirectoryServicesID,
StoreFront: testStoreFront,
}

var got Account
Expand Down Expand Up @@ -207,6 +266,7 @@ var _ = Describe("AppStore (Login)", func() {
PasswordToken: testPasswordToken,
Password: testPassword,
DirectoryServicesID: testDirectoryServicesID,
StoreFront: testStoreFront,
}

var got Account
Expand Down
14 changes: 12 additions & 2 deletions pkg/http/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"howett.net/plist"
)

const (
appStoreAuthURL = "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/authenticate"
)

//go:generate go run go.uber.org/mock/mockgen -source=client.go -destination=client_mock.go -package=http
type Client[R interface{}] interface {
Send(request Request) (Result[R], error)
Expand Down Expand Up @@ -47,8 +51,14 @@ func (t *AddHeaderTransport) RoundTrip(req *http.Request) (*http.Response, error
func NewClient[R interface{}](args Args) Client[R] {
return &client[R]{
internalClient: http.Client{
Timeout: 0,
Jar: args.CookieJar,
Timeout: 0,
Jar: args.CookieJar,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if req.Referer() == appStoreAuthURL {
return http.ErrUseLastResponse
}
return nil

Check failure on line 60 in pkg/http/client.go

View workflow job for this annotation

GitHub Actions / Lint

return with no blank line before (nlreturn)
},
Transport: &AddHeaderTransport{http.DefaultTransport},
},
cookieJar: args.CookieJar,
Expand Down
2 changes: 1 addition & 1 deletion pkg/http/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ const (
)

const (
DefaultUserAgent = "Configurator/2.15 (Macintosh; OS X 11.0.0; 16G29) AppleWebKit/2603.3.8"
DefaultUserAgent = "Configurator/2.17 (Macintosh; OS X 15.2; 24C5089c) AppleWebKit/0620.1.16.11.6"
)
19 changes: 19 additions & 0 deletions pkg/http/result.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
package http

import (
"errors"
"strings"
)

var (
ErrHeaderNotFound = errors.New("header not found")
)

type Result[R interface{}] struct {
StatusCode int
Headers map[string]string
Data R
}

func (c *Result[R]) GetHeader(key string) (string, error) {
key = strings.ToLower(key)
for k, v := range c.Headers {
if strings.ToLower(k) == key {
return v, nil
}
}
return "", ErrHeaderNotFound

Check failure on line 25 in pkg/http/result.go

View workflow job for this annotation

GitHub Actions / Lint

return statements should not be cuddled if block has more than two lines (wsl)
}

0 comments on commit 2a5347c

Please sign in to comment.