Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix authentication #316

Merged
merged 1 commit into from
Dec 7, 2024
Merged
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
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 @@
"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 @@

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 @@
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 @@
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 @@
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 @@
"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 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)
}
Loading