forked from majd/ipatool
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
663 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,8 @@ | ||
package appstore | ||
|
||
type Account struct { | ||
Email string `json:"email,omitempty"` | ||
PasswordToken string `json:"passwordToken,omitempty"` | ||
DirectoryServicesID string `json:"directoryServicesIdentifier,omitempty"` | ||
Name string `json:"name,omitempty"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,38 @@ | ||
package appstore | ||
|
||
import ( | ||
"github.com/majd/ipatool/pkg/http" | ||
"github.com/majd/ipatool/pkg/keychain" | ||
"github.com/majd/ipatool/pkg/util" | ||
"io" | ||
"os" | ||
) | ||
|
||
type AppStore interface { | ||
Login(email, password, authCode string) error | ||
Info() error | ||
Revoke() error | ||
} | ||
|
||
type appstore struct { | ||
keychain keychain.Keychain | ||
loginClient http.Client[LoginResult] | ||
ioReader io.Reader | ||
machine util.Machine | ||
} | ||
|
||
type Args struct { | ||
Keychain keychain.Keychain | ||
CookieJar http.CookieJar | ||
} | ||
|
||
func NewAppStore(args *Args) AppStore { | ||
return &appstore{ | ||
keychain: args.Keychain, | ||
loginClient: http.NewClient[LoginResult](&http.Args{ | ||
CookieJar: args.CookieJar, | ||
}), | ||
ioReader: os.Stdin, | ||
machine: util.NewMachine(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,28 @@ | ||
package appstore | ||
|
||
import ( | ||
"encoding/json" | ||
"github.com/majd/ipatool/pkg/log" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
func (a *appstore) Info() error { | ||
data, err := a.keychain.Get("account") | ||
if err != nil { | ||
return errors.Wrap(err, "account was not found") | ||
} | ||
|
||
var account Account | ||
err = json.Unmarshal(data, &account) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to unmarshall account data") | ||
} | ||
|
||
log.Info(). | ||
Str("name", account.Name). | ||
Str("email", account.Email). | ||
Bool("succes", true). | ||
Send() | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,77 @@ | ||
package appstore | ||
|
||
import ( | ||
"github.com/golang/mock/gomock" | ||
"github.com/majd/ipatool/pkg/keychain" | ||
"github.com/majd/ipatool/pkg/log" | ||
. "github.com/onsi/ginkgo/v2" | ||
. "github.com/onsi/gomega" | ||
"github.com/pkg/errors" | ||
"github.com/rs/zerolog" | ||
) | ||
|
||
var _ = Describe("AppStore (Info)", Ordered, func() { | ||
var ( | ||
ctrl *gomock.Controller | ||
appstore AppStore | ||
mockKeychain *keychain.MockKeychain | ||
) | ||
|
||
BeforeAll(func() { | ||
log.Logger = log.Output(log.NewWriter()).Level(zerolog.Disabled) | ||
}) | ||
|
||
BeforeEach(func() { | ||
ctrl = gomock.NewController(GinkgoT()) | ||
mockKeychain = keychain.NewMockKeychain(ctrl) | ||
appstore = NewAppStore(&Args{ | ||
Keychain: mockKeychain, | ||
}) | ||
}) | ||
|
||
AfterEach(func() { | ||
ctrl.Finish() | ||
}) | ||
|
||
When("keychain returns error", func() { | ||
var testErr = errors.New("test error") | ||
|
||
BeforeEach(func() { | ||
mockKeychain.EXPECT(). | ||
Get("account"). | ||
Return([]byte{}, testErr) | ||
}) | ||
|
||
It("returns wrapped error", func() { | ||
err := appstore.Info() | ||
Expect(err).To(MatchError(ContainSubstring(testErr.Error()))) | ||
Expect(err).To(MatchError(ContainSubstring("account was not found"))) | ||
}) | ||
}) | ||
|
||
When("keychain returns invalid data", func() { | ||
BeforeEach(func() { | ||
mockKeychain.EXPECT(). | ||
Get("account"). | ||
Return([]byte("..."), nil) | ||
}) | ||
|
||
It("fails to unmarshall JSON data", func() { | ||
err := appstore.Info() | ||
Expect(err).To(MatchError(ContainSubstring("failed to unmarshall account data"))) | ||
}) | ||
}) | ||
|
||
When("keychain returns valid data", func() { | ||
BeforeEach(func() { | ||
mockKeychain.EXPECT(). | ||
Get("account"). | ||
Return([]byte("{}"), nil) | ||
}) | ||
|
||
It("returns nil", func() { | ||
err := appstore.Info() | ||
Expect(err).ToNot(HaveOccurred()) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,161 @@ | ||
package appstore | ||
|
||
import ( | ||
"bufio" | ||
"encoding/json" | ||
"fmt" | ||
"github.com/majd/ipatool/pkg/http" | ||
"github.com/majd/ipatool/pkg/log" | ||
"github.com/majd/ipatool/pkg/util" | ||
"github.com/pkg/errors" | ||
"strings" | ||
) | ||
|
||
type LoginAddressResult struct { | ||
FirstName string `plist:"firstName,omitempty"` | ||
LastName string `plist:"lastName,omitempty"` | ||
} | ||
|
||
type LoginAccountResult struct { | ||
Email string `plist:"appleId,omitempty"` | ||
Address LoginAddressResult `plist:"address,omitempty"` | ||
} | ||
|
||
type LoginResult struct { | ||
FailureType string `plist:"failureType,omitempty"` | ||
CustomerMessage string `plist:"customerMessage,omitempty"` | ||
Account LoginAccountResult `plist:"accountInfo,omitempty"` | ||
DirectoryServicesID string `plist:"dsPersonId,omitempty"` | ||
PasswordToken string `plist:"passwordToken,omitempty"` | ||
} | ||
|
||
func (a *appstore) Login(email, password, authCode string) error { | ||
macAddr, err := a.machine.MacAddress() | ||
if err != nil { | ||
return errors.Wrap(err, "failed to read MAC address") | ||
} | ||
|
||
guid := strings.ReplaceAll(strings.ToUpper(macAddr), ":", "") | ||
log.Debug(). | ||
Str("mac", macAddr). | ||
Str("guid", guid). | ||
Send() | ||
|
||
return a.login(email, password, authCode, guid, 0) | ||
} | ||
|
||
func (a *appstore) login(email, password, authCode, guid string, attempt int) error { | ||
log.Debug(). | ||
Int("attempt", attempt). | ||
Str("password", password). | ||
Str("email", email). | ||
Str("authCode", util.IfEmpty(authCode, "<nil>")). | ||
Msg("sending login request") | ||
|
||
request := a.loginRequest(email, password, authCode, guid) | ||
res, err := a.loginClient.Send(request) | ||
if err != nil { | ||
return errors.Wrap(err, "request failed") | ||
} | ||
|
||
if attempt == 0 && res.Data.FailureType == FailureTypeInvalidCredentials { | ||
return a.login(email, password, authCode, guid, 1) | ||
} | ||
|
||
if res.Data.FailureType != "" && res.Data.CustomerMessage != "" { | ||
log.Debug(). | ||
Interface("response", res). | ||
Send() | ||
return errors.New(res.Data.CustomerMessage) | ||
} | ||
|
||
if res.Data.FailureType != "" { | ||
log.Debug(). | ||
Interface("response", res). | ||
Send() | ||
return errors.New("unknown error occurred") | ||
} | ||
|
||
if res.Data.FailureType == "" && authCode == "" && res.Data.CustomerMessage == CustomerMessageBadLogin { | ||
log.Warn().Msg("enter 2FA code:") | ||
authCode, err = a.promptForAuthCode() | ||
if err != nil { | ||
return errors.Wrap(err, "failed to prompt for 2FA code") | ||
} | ||
|
||
return a.login(email, password, authCode, guid, 0) | ||
} | ||
|
||
addr := res.Data.Account.Address | ||
name := strings.Join([]string{addr.FirstName, addr.LastName}, " ") | ||
|
||
data, err := json.Marshal(Account{ | ||
Name: name, | ||
Email: res.Data.Account.Email, | ||
PasswordToken: res.Data.PasswordToken, | ||
DirectoryServicesID: res.Data.DirectoryServicesID, | ||
}) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to marshall account data") | ||
} | ||
|
||
err = a.keychain.Set("account", data) | ||
if err != nil { | ||
return errors.Wrap(err, "failed to save account data in keychain") | ||
} | ||
|
||
log.Info(). | ||
Str("name", name). | ||
Str("email", res.Data.Account.Email). | ||
Bool("success", true). | ||
Send() | ||
|
||
return nil | ||
} | ||
|
||
func (a *appstore) loginRequest(email, password, authCode, guid string) http.Request { | ||
attempt := "4" | ||
if authCode != "" { | ||
attempt = "2" | ||
} | ||
|
||
return http.Request{ | ||
Method: http.MethodPOST, | ||
URL: a.authDomain(authCode, guid), | ||
Headers: map[string]string{ | ||
"User-Agent": "Configurator/2.15 (Macintosh; OS X 11.0.0; 16G29) AppleWebKit/2603.3.8", | ||
"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", | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func (a *appstore) promptForAuthCode() (string, error) { | ||
reader := bufio.NewReader(a.ioReader) | ||
authCode, err := reader.ReadString('\n') | ||
if err != nil { | ||
return "", errors.Wrap(err, "failed to read string from stdin") | ||
} | ||
|
||
return strings.Trim(authCode, "\n"), nil | ||
} | ||
|
||
func (*appstore) authDomain(authCode, guid string) string { | ||
prefix := PriavteAppStoreAPIDomainPrefixWithoutAuthCode | ||
if authCode != "" { | ||
prefix = PriavteAppStoreAPIDomainPrefixWithAuthCode | ||
} | ||
|
||
return fmt.Sprintf( | ||
"https://%s-%s%s?guid=%s", prefix, PrivateAppStoreAPIDomain, PrivateAppStoreAPIPathAuthenticate, guid) | ||
} |
Oops, something went wrong.