Skip to content

Commit 31c9c98

Browse files
authored
Moved devportalservice from v1 (#253)
1 parent b8359d9 commit 31c9c98

File tree

6 files changed

+834
-0
lines changed

6 files changed

+834
-0
lines changed
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
package devportalservice
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"regexp"
11+
"strings"
12+
"text/template"
13+
"time"
14+
15+
"github.com/bitrise-io/go-utils/fileutil"
16+
"github.com/bitrise-io/go-utils/log"
17+
)
18+
19+
const appleDeveloperConnectionPath = "apple_developer_portal_data.json"
20+
21+
type cookie struct {
22+
Name string `json:"name"`
23+
Path string `json:"path"`
24+
Value string `json:"value"`
25+
Domain string `json:"domain"`
26+
Secure bool `json:"secure"`
27+
Expires string `json:"expires,omitempty"`
28+
MaxAge int `json:"max_age,omitempty"`
29+
Httponly bool `json:"httponly"`
30+
ForDomain *bool `json:"for_domain,omitempty"`
31+
}
32+
33+
// AppleIDConnection represents a Bitrise.io Apple ID-based Apple Developer connection.
34+
type AppleIDConnection struct {
35+
AppleID string `json:"apple_id"`
36+
Password string `json:"password"`
37+
AppSpecificPassword string `json:"app_specific_password"`
38+
SessionExpiryDate *time.Time `json:"connection_expiry_date"`
39+
SessionCookies map[string][]cookie `json:"session_cookies"`
40+
}
41+
42+
// APIKeyConnection represents a Bitrise.io API key-based Apple Developer connection.
43+
type APIKeyConnection struct {
44+
KeyID string `json:"key_id"`
45+
IssuerID string `json:"issuer_id"`
46+
PrivateKey string `json:"private_key"`
47+
}
48+
49+
// IsEqualUDID compares two UDIDs (stored in the DeviceID field of TestDevice)
50+
func IsEqualUDID(UDID string, otherUDID string) bool {
51+
return normalizeDeviceUDID(UDID) == normalizeDeviceUDID(otherUDID)
52+
}
53+
54+
// AppleDeveloperConnection represents a Bitrise.io Apple Developer connection.
55+
// https://devcenter.bitrise.io/getting-started/configuring-bitrise-steps-that-require-apple-developer-account-data/
56+
type AppleDeveloperConnection struct {
57+
AppleIDConnection *AppleIDConnection
58+
APIKeyConnection *APIKeyConnection
59+
TestDevices, DuplicatedTestDevices []TestDevice
60+
}
61+
62+
type httpClient interface {
63+
Do(req *http.Request) (*http.Response, error)
64+
}
65+
66+
// AppleDeveloperConnectionProvider ...
67+
type AppleDeveloperConnectionProvider interface {
68+
GetAppleDeveloperConnection() (*AppleDeveloperConnection, error)
69+
}
70+
71+
// BitriseClient implements AppleDeveloperConnectionProvider through the Bitrise.io API.
72+
type BitriseClient struct {
73+
httpClient httpClient
74+
buildURL, buildAPIToken string
75+
76+
readBytesFromFile func(pth string) ([]byte, error)
77+
}
78+
79+
// NewBitriseClient creates a new instance of BitriseClient.
80+
func NewBitriseClient(client httpClient, buildURL, buildAPIToken string) *BitriseClient {
81+
return &BitriseClient{
82+
httpClient: client,
83+
buildURL: buildURL,
84+
buildAPIToken: buildAPIToken,
85+
readBytesFromFile: fileutil.ReadBytesFromFile,
86+
}
87+
}
88+
89+
func privateKeyWithHeader(privateKey string) string {
90+
if strings.HasPrefix(privateKey, "-----BEGIN PRIVATE KEY----") {
91+
return privateKey
92+
}
93+
94+
return fmt.Sprint(
95+
"-----BEGIN PRIVATE KEY-----\n",
96+
privateKey,
97+
"\n-----END PRIVATE KEY-----",
98+
)
99+
}
100+
101+
// GetAppleDeveloperConnection fetches the Bitrise.io Apple Developer connection.
102+
func (c *BitriseClient) GetAppleDeveloperConnection() (*AppleDeveloperConnection, error) {
103+
var rawCreds []byte
104+
var err error
105+
106+
if strings.HasPrefix(c.buildURL, "file://") {
107+
rawCreds, err = c.readBytesFromFile(strings.TrimPrefix(c.buildURL, "file://"))
108+
} else {
109+
rawCreds, err = c.download()
110+
}
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to fetch authentication credentials: %v", err)
113+
}
114+
115+
type data struct {
116+
*AppleIDConnection
117+
*APIKeyConnection
118+
TestDevices []TestDevice `json:"test_devices"`
119+
}
120+
var d data
121+
if err := json.Unmarshal([]byte(rawCreds), &d); err != nil {
122+
return nil, fmt.Errorf("failed to unmarshal authentication credentials from response (%s): %s", rawCreds, err)
123+
}
124+
125+
if d.APIKeyConnection != nil {
126+
if d.APIKeyConnection.IssuerID == "" {
127+
return nil, fmt.Errorf("invalid authentication credentials, empty issuer_id in response (%s)", rawCreds)
128+
}
129+
if d.APIKeyConnection.KeyID == "" {
130+
return nil, fmt.Errorf("invalid authentication credentials, empty key_id in response (%s)", rawCreds)
131+
}
132+
if d.APIKeyConnection.PrivateKey == "" {
133+
return nil, fmt.Errorf("invalid authentication credentials, empty private_key in response (%s)", rawCreds)
134+
}
135+
136+
d.APIKeyConnection.PrivateKey = privateKeyWithHeader(d.APIKeyConnection.PrivateKey)
137+
}
138+
139+
testDevices, duplicatedDevices := validateTestDevice(d.TestDevices)
140+
141+
return &AppleDeveloperConnection{
142+
AppleIDConnection: d.AppleIDConnection,
143+
APIKeyConnection: d.APIKeyConnection,
144+
TestDevices: testDevices,
145+
DuplicatedTestDevices: duplicatedDevices,
146+
}, nil
147+
}
148+
149+
func (c *BitriseClient) download() ([]byte, error) {
150+
url := fmt.Sprintf("%s/%s", c.buildURL, appleDeveloperConnectionPath)
151+
req, err := http.NewRequest(http.MethodGet, url, nil)
152+
if err != nil {
153+
return nil, fmt.Errorf("failed to create request for URL (%s): %s", url, err)
154+
}
155+
req.Header.Add("BUILD_API_TOKEN", c.buildAPIToken)
156+
157+
resp, err := c.httpClient.Do(req)
158+
if err != nil {
159+
// On error, any Response can be ignored
160+
return nil, fmt.Errorf("failed to perform request: %s", err)
161+
}
162+
163+
// The client must close the response body when finished with it
164+
defer func() {
165+
if cerr := resp.Body.Close(); cerr != nil {
166+
log.Warnf("Failed to close response body: %s", cerr)
167+
}
168+
}()
169+
170+
body, err := io.ReadAll(resp.Body)
171+
if err != nil {
172+
return nil, fmt.Errorf("failed to read response body: %s", err)
173+
}
174+
175+
if resp.StatusCode != http.StatusOK {
176+
return nil, NetworkError{Status: resp.StatusCode}
177+
}
178+
179+
return body, nil
180+
}
181+
182+
// FastlaneLoginSession returns the Apple ID login session in a ruby/object:HTTP::Cookie format.
183+
// The session can be used as a value for FASTLANE_SESSION environment variable: https://docs.fastlane.tools/best-practices/continuous-integration/#two-step-or-two-factor-auth.
184+
func (c *AppleIDConnection) FastlaneLoginSession() (string, error) {
185+
var rubyCookies []string
186+
for _, cookie := range c.SessionCookies["https://idmsa.apple.com"] {
187+
if rubyCookies == nil {
188+
rubyCookies = append(rubyCookies, "---"+"\n")
189+
}
190+
191+
if cookie.ForDomain == nil {
192+
b := true
193+
cookie.ForDomain = &b
194+
}
195+
196+
tmpl, err := template.New("").Parse(`- !ruby/object:HTTP::Cookie
197+
name: {{.Name}}
198+
value: {{.Value}}
199+
domain: {{.Domain}}
200+
for_domain: {{.ForDomain}}
201+
path: "{{.Path}}"
202+
`)
203+
if err != nil {
204+
return "", fmt.Errorf("failed to parse template: %s", err)
205+
}
206+
207+
var b bytes.Buffer
208+
err = tmpl.Execute(&b, cookie)
209+
if err != nil {
210+
return "", fmt.Errorf("failed to execute template on cookie: %s: %s", cookie.Name, err)
211+
}
212+
213+
rubyCookies = append(rubyCookies, b.String()+"\n")
214+
}
215+
return strings.Join(rubyCookies, ""), nil
216+
}
217+
218+
func validDeviceUDID(udid string) string {
219+
r := regexp.MustCompile("[^a-zA-Z0-9-]")
220+
return r.ReplaceAllLiteralString(udid, "")
221+
}
222+
223+
func normalizeDeviceUDID(udid string) string {
224+
return strings.ToLower(strings.ReplaceAll(validDeviceUDID(udid), "-", ""))
225+
}
226+
227+
// validateTestDevice filters out duplicated devices
228+
// it does not change UDID casing or remove '-' separator, only to filter out whitespace or unsupported characters
229+
func validateTestDevice(deviceList []TestDevice) (validDevices, duplicatedDevices []TestDevice) {
230+
bitriseDevices := make(map[string]bool)
231+
for _, device := range deviceList {
232+
normalizedID := normalizeDeviceUDID(device.DeviceID)
233+
if _, ok := bitriseDevices[normalizedID]; ok {
234+
duplicatedDevices = append(duplicatedDevices, device)
235+
236+
continue
237+
}
238+
239+
bitriseDevices[normalizedID] = true
240+
device.DeviceID = validDeviceUDID(device.DeviceID)
241+
validDevices = append(validDevices, device)
242+
}
243+
244+
return validDevices, duplicatedDevices
245+
}
246+
247+
// WritePrivateKeyToFile writes the contents of the private key to a temporary file and returns its path
248+
func (c *APIKeyConnection) WritePrivateKeyToFile() (string, error) {
249+
privatekeyFile, err := os.CreateTemp("", fmt.Sprintf("AuthKey_%s_*.p8", c.KeyID))
250+
if err != nil {
251+
return "", fmt.Errorf("failed to create private key file: %s", err)
252+
}
253+
254+
if _, err := privatekeyFile.Write([]byte(c.PrivateKey)); err != nil {
255+
return "", fmt.Errorf("failed to write private key: %s", err)
256+
}
257+
258+
if err := privatekeyFile.Close(); err != nil {
259+
return "", fmt.Errorf("failed to close private key file: %s", err)
260+
}
261+
262+
return privatekeyFile.Name(), nil
263+
}

0 commit comments

Comments
 (0)