1 package ghappman
2
3 import (
4 "bytes"
5 "context"
6 "crypto/rand"
7 "encoding/base64"
8 "encoding/json"
9 "fmt"
10 "io"
11 "net/http"
12 "os"
13 "time"
14
15 credentials "cloud.google.com/go/iam/credentials/apiv1"
16 "cloud.google.com/go/iam/credentials/apiv1/credentialspb"
17 "golang.org/x/oauth2"
18 )
19
20
21
22 type Client struct {
23 *http.Client
24 }
25
26
27
28 func CreateGitHubApp(ctx context.Context, installSuffix string) (*AppConfig, error) {
29 b, err := generatePassword()
30 if err != nil {
31 return nil, err
32 }
33 userKey := base64.StdEncoding.EncodeToString(b)
34
35
36
37
38 client, err := NewIAPClient(ctx)
39 if err != nil {
40 return nil, err
41 }
42
43 if installSuffix == "*" {
44 installSuffix = RandAN(8)
45 }
46 req, err := json.Marshal(&CreateAppReq{UserKey: userKey, InstallSuffix: installSuffix})
47 if err != nil {
48 return nil, err
49 }
50 res, err := client.Post(
51 fmt.Sprintf("%s/%s", ghamURL, "new-app"),
52 "application/json",
53 bytes.NewReader(req),
54 )
55 if err != nil {
56 return nil, err
57 }
58 resByte, _ := io.ReadAll(res.Body)
59 createAppRes := &CreateAppRes{}
60 err = json.Unmarshal(resByte, createAppRes)
61 if err != nil {
62 return nil, err
63 }
64
65 fmt.Println("=== beginning github app creation")
66 fmt.Printf("visit %v to initiate app creation\n", createAppRes.CreateURL)
67 waitTimeout, cancel := context.WithTimeout(ctx, createAppRes.ExpiresIn)
68 app, err := client.waitForAppInstallation(waitTimeout, createAppRes.State, userKey)
69 cancel()
70 if err != nil {
71 return nil, err
72 }
73 fmt.Printf("app '%s' has been created, but you may still need to finish the installation dialog\n", app.Name)
74
75
76 return app, nil
77 }
78
79
80
81
82
83 func NewIAPClient(ctx context.Context) (*Client, error) {
84 c, err := credentials.NewIamCredentialsClient(ctx)
85 if err != nil {
86 return nil, fmt.Errorf("credentials.NewIamCredentialsClient: %v", err)
87 }
88
89
90 req := &credentialspb.GenerateIdTokenRequest{
91 Name: "projects/-/serviceAccounts/114355029064852695579",
92 Audience: "886862789596-v6bsskt8f7o4v0ql32trt354h7plcta7.apps.googleusercontent.com",
93 IncludeEmail: true,
94 }
95
96 resp, err := c.GenerateIdToken(ctx, req)
97 if err != nil {
98 return nil, fmt.Errorf("GenerateIdToken: %v", err)
99 }
100
101
102
103
104 ts := oauth2.StaticTokenSource(&oauth2.Token{
105 TokenType: "Bearer",
106 AccessToken: resp.Token,
107 })
108 client := oauth2.NewClient(ctx, ts)
109
110 return &Client{client}, nil
111 }
112
113 func generatePassword() ([]byte, error) {
114 n := 32
115 b := make([]byte, n)
116 _, err := rand.Read(b)
117 if err != nil {
118 return nil, err
119 }
120 return b, nil
121 }
122
123 func (c *Client) waitForAppInstallation(ctx context.Context, state string, key string) (*AppConfig, error) {
124 interval := time.Second * 5
125 for {
126 select {
127 case <-ctx.Done():
128 if ctx.Err() == context.DeadlineExceeded {
129 return nil, fmt.Errorf("waiting for app creation timed out")
130 }
131 case <-time.After(interval):
132 retryable, app := c.makeAppInstallationRequest(state, key)
133 if !retryable && app != nil {
134
135
136 return app, nil
137 } else if !retryable && app == nil {
138 fmt.Println("something went wrong.")
139 os.Exit(1)
140 }
141 }
142 }
143 }
144
145 func (c *Client) makeAppInstallationRequest(state string, key string) (bool, *AppConfig) {
146 url := fmt.Sprintf("%s/new-app/%s/app", ghamURL, state)
147 appReq, err := http.NewRequest("GET", url, nil)
148 if err != nil {
149 fmt.Printf("failed to create request. err: %v\n", err.Error())
150 os.Exit(1)
151 }
152 appReq.Header.Add("X-Appkey", key)
153
154 appRes, err := c.Do(appReq)
155 if err != nil {
156 fmt.Printf("failed to make request. err: %v\n", err.Error())
157 os.Exit(1)
158 }
159
160 switch appRes.StatusCode {
161 case http.StatusBadRequest:
162 return true, nil
163 case http.StatusNotFound:
164 return false, nil
165 }
166
167 appResBody, _ := io.ReadAll(appRes.Body)
168 _ = appRes.Body.Close()
169 app := &AppConfig{}
170 err = json.Unmarshal(appResBody, app)
171 if err != nil {
172 fmt.Printf("failed to parse response. err: %v\nresponse: %s\n", err.Error(), appResBody)
173 os.Exit(1)
174 }
175 return false, app
176 }
177
View as plain text