mirror of
https://github.com/blacktop/ipsw.git
synced 2026-06-07 12:27:36 +00:00
- Add `--platform` flag (ios, macos, tvos, catalyst) to select the correct certificate and profile types per platform - Map each platform/type combination to the appropriate ProfileType - Extract `profileTypeNeedsDevices` helper to cover all dev/adhoc profile types across platforms - Auto-register the host Mac as a development device when no devices are found and platform is macos/catalyst - Fix `DeviceCreateRequest` to omit `deviceClass` and `model` fields rather than hard-coding iPhone values - Initialize profile relationship slices before appending so json.Marshal emits `[]` instead of `null`
505 lines
16 KiB
Go
505 lines
16 KiB
Go
package appstore
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/blacktop/ipsw/internal/download"
|
|
)
|
|
|
|
type ProfileType string
|
|
|
|
const (
|
|
IOS_APP_DEVELOPMENT ProfileType = "IOS_APP_DEVELOPMENT"
|
|
IOS_APP_STORE ProfileType = "IOS_APP_STORE"
|
|
IOS_APP_ADHOC ProfileType = "IOS_APP_ADHOC"
|
|
IOS_APP_INHOUSE ProfileType = "IOS_APP_INHOUSE"
|
|
MAC_APP_DEVELOPMENT ProfileType = "MAC_APP_DEVELOPMENT"
|
|
MAC_APP_STORE ProfileType = "MAC_APP_STORE"
|
|
MAC_APP_DIRECT ProfileType = "MAC_APP_DIRECT"
|
|
TVOS_APP_DEVELOPMENT ProfileType = "TVOS_APP_DEVELOPMENT"
|
|
TVOS_APP_STORE ProfileType = "TVOS_APP_STORE"
|
|
TVOS_APP_ADHOC ProfileType = "TVOS_APP_ADHOC"
|
|
TVOS_APP_INHOUSE ProfileType = "TVOS_APP_INHOUSE"
|
|
MAC_CATALYST_APP_DEVELOPMENT ProfileType = "MAC_CATALYST_APP_DEVELOPMENT"
|
|
MAC_CATALYST_APP_STORE ProfileType = "MAC_CATALYST_APP_STORE"
|
|
MAC_CATALYST_APP_DIRECT ProfileType = "MAC_CATALYST_APP_DIRECT"
|
|
)
|
|
|
|
var ProfileTypes = []string{
|
|
string(IOS_APP_DEVELOPMENT),
|
|
string(IOS_APP_STORE),
|
|
string(IOS_APP_ADHOC),
|
|
string(IOS_APP_INHOUSE),
|
|
string(MAC_APP_DEVELOPMENT),
|
|
string(MAC_APP_STORE),
|
|
string(MAC_APP_DIRECT),
|
|
string(TVOS_APP_DEVELOPMENT),
|
|
string(TVOS_APP_STORE),
|
|
string(TVOS_APP_ADHOC),
|
|
string(TVOS_APP_INHOUSE),
|
|
string(MAC_CATALYST_APP_DEVELOPMENT),
|
|
string(MAC_CATALYST_APP_STORE),
|
|
string(MAC_CATALYST_APP_DIRECT),
|
|
}
|
|
|
|
type ProfileState string
|
|
|
|
const (
|
|
PS_ACTIVE ProfileState = "ACTIVE"
|
|
PS_INVALID ProfileState = "INVALID"
|
|
)
|
|
|
|
type Profile struct {
|
|
ID string `json:"id"`
|
|
Type ProfileType `json:"type"` // profiles
|
|
Attributes struct {
|
|
Name string `json:"name"`
|
|
CreatedDate Date `json:"createdDate"`
|
|
ExpirationDate Date `json:"expirationDate"`
|
|
ProfileContent []byte `json:"profileContent"`
|
|
ProfileState ProfileState `json:"profileState"`
|
|
ProfileType ProfileType `json:"profileType"`
|
|
UUID string `json:"uuid"`
|
|
Platform BundleIdPlatform `json:"platform"`
|
|
OfflineProfile bool `json:"isOfflineProfile"`
|
|
} `json:"attributes"`
|
|
Relationships struct {
|
|
BundleID struct {
|
|
Data struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
Links struct {
|
|
Self string `json:"self"`
|
|
Related string `json:"related"`
|
|
} `json:"links"`
|
|
} `json:"bundleId"`
|
|
Certificates struct {
|
|
Meta struct {
|
|
Paging struct {
|
|
Total int `json:"total"`
|
|
Limit int64 `json:"limit"`
|
|
} `json:"paging"`
|
|
} `json:"meta"`
|
|
Data []struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
Links struct {
|
|
Self string `json:"self"`
|
|
Related string `json:"related"`
|
|
} `json:"links"`
|
|
} `json:"certificates"`
|
|
Devices struct {
|
|
Meta struct {
|
|
Paging struct {
|
|
Total int `json:"total"`
|
|
Limit int64 `json:"limit"`
|
|
} `json:"paging"`
|
|
} `json:"meta"`
|
|
Data []struct {
|
|
Type string `json:"type"`
|
|
ID string `json:"id"`
|
|
} `json:"data"`
|
|
Links struct {
|
|
Self string `json:"self"`
|
|
Related string `json:"related"`
|
|
} `json:"links"`
|
|
} `json:"devices"`
|
|
} `json:"relationships"`
|
|
Links Links `json:"links"`
|
|
}
|
|
|
|
func (p Profile) IsInvalid() bool {
|
|
return p.Attributes.ProfileState == "INVALID"
|
|
}
|
|
func (p Profile) IsExpired() bool {
|
|
return time.Time(p.Attributes.ExpirationDate).Before(time.Now())
|
|
}
|
|
|
|
type Data struct {
|
|
ID string `json:"id,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
}
|
|
|
|
type ProfileCreateRequest struct {
|
|
Data struct {
|
|
Type string `json:"type"` // profiles
|
|
Attributes struct {
|
|
Name string `json:"name"`
|
|
ProfileType ProfileType `json:"profileType"`
|
|
TemplateName string `json:"templateName,omitempty"`
|
|
OfflineProfile bool `json:"isOfflineProfile,omitempty"`
|
|
} `json:"attributes"`
|
|
Relationships struct {
|
|
BundleID struct {
|
|
Data Data `json:"data"`
|
|
} `json:"bundleId"`
|
|
Certificates struct {
|
|
Data []Data `json:"data"`
|
|
} `json:"certificates"`
|
|
Devices struct {
|
|
Data []Data `json:"data"`
|
|
} `json:"devices"`
|
|
} `json:"relationships"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
type ProfileResponse struct {
|
|
Data Profile `json:"data"`
|
|
Links DocumentLinks `json:"links"`
|
|
}
|
|
type ProfilesResponse struct {
|
|
Data []Profile `json:"data"`
|
|
Links Links `json:"links"`
|
|
}
|
|
|
|
// GetProfiles returns a list provisioning profiles and download their data.
|
|
func (as *AppStore) GetProfiles() ([]Profile, error) {
|
|
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", profilesURL, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create http GET request: %v", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var profiles ProfilesResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&profiles); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return profiles.Data, nil
|
|
}
|
|
|
|
// GetProfile returns a provisioning profile and download their data.
|
|
func (as *AppStore) GetProfile(id string) (*Profile, error) {
|
|
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", profilesURL+"/"+id, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create http GET request: %v", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var profile ProfileResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&profile); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return &profile.Data, nil
|
|
}
|
|
|
|
// GetProfileBundleID returns the bundle ID information for a specific provisioning profile.
|
|
func (as *AppStore) GetProfileBundleID(id string) (*BundleID, error) {
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/bundleId", id), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create http GET request: %v", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var bundle BundleIdResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&bundle); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return &bundle.Data, nil
|
|
}
|
|
|
|
// GetProfileDevices returns a list of all devices for a specific provisioning profile
|
|
func (as *AppStore) GetProfileDevices(id string) ([]Device, error) {
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/devices", id), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create http GET request: %v", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var devices DevicesResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&devices); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return devices.Data, nil
|
|
}
|
|
|
|
// GetProfileCerts returns a list of all certificates and their data for a specific provisioning profile.
|
|
func (as *AppStore) GetProfileCerts(id string) ([]Certificate, error) {
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("GET", profilesURL+fmt.Sprintf("/%s/certificates", id), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create http GET request: %v", err)
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var certs CertificatesResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&certs); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return certs.Data, nil
|
|
}
|
|
|
|
// CreateProfile creates a new profile
|
|
func (as *AppStore) CreateProfile(name string, ptype string, bundleID string, cerIDs, devicesIDs []string, offline bool) (*ProfileResponse, error) {
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return nil, fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
var profileCreateRequest ProfileCreateRequest
|
|
profileCreateRequest.Data.Type = "profiles"
|
|
profileCreateRequest.Data.Attributes.Name = name
|
|
profileCreateRequest.Data.Attributes.ProfileType = ProfileType(ptype)
|
|
profileCreateRequest.Data.Attributes.OfflineProfile = offline
|
|
profileCreateRequest.Data.Relationships.BundleID.Data.Type = "bundleIds"
|
|
profileCreateRequest.Data.Relationships.BundleID.Data.ID = bundleID
|
|
// Initialize slices so json.Marshal produces [] instead of null when empty
|
|
profileCreateRequest.Data.Relationships.Certificates.Data = make([]Data, 0, len(cerIDs))
|
|
for _, cerID := range cerIDs {
|
|
profileCreateRequest.Data.Relationships.Certificates.Data = append(profileCreateRequest.Data.Relationships.Certificates.Data, Data{
|
|
ID: cerID,
|
|
Type: "certificates",
|
|
})
|
|
}
|
|
profileCreateRequest.Data.Relationships.Devices.Data = make([]Data, 0, len(devicesIDs))
|
|
for _, devicesID := range devicesIDs {
|
|
profileCreateRequest.Data.Relationships.Devices.Data = append(profileCreateRequest.Data.Relationships.Devices.Data, Data{
|
|
ID: devicesID,
|
|
Type: "devices",
|
|
})
|
|
}
|
|
|
|
jsonStr, err := json.Marshal(&profileCreateRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// os.WriteFile("req.json", jsonStr, 0644)
|
|
req, err := http.NewRequest("POST", profilesURL, bytes.NewBuffer(jsonStr))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 201 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return nil, fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
var profileResponse ProfileResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&profileResponse); err != nil {
|
|
return nil, fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
|
|
return &profileResponse, nil
|
|
}
|
|
|
|
// DeleteProfile deletes a provisioning profile that is used for app development or distribution.
|
|
func (as *AppStore) DeleteProfile(id string) error {
|
|
if err := as.createToken(defaultJWTLife); err != nil {
|
|
return fmt.Errorf("failed to create token: %v", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("DELETE", profilesURL+"/"+id, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+as.token)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: download.GetProxy(as.Proxy),
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: as.Insecure},
|
|
},
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to send http request: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 204 {
|
|
var eresp ErrorResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&eresp); err != nil {
|
|
return fmt.Errorf("failed to JSON decode http response: %v", err)
|
|
}
|
|
var errOut strings.Builder
|
|
for idx, e := range eresp.Errors {
|
|
errOut.WriteString(fmt.Sprintf("%s%s: %s (%s)\n", strings.Repeat("\t", idx), e.Code, e.Title, e.Detail))
|
|
}
|
|
return fmt.Errorf("%s: %s", resp.Status, errOut.String())
|
|
}
|
|
|
|
return nil
|
|
}
|