1package github
2
3import (
4 "bufio"
5 "bytes"
6 "encoding/json"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "math/rand"
11 "net/http"
12 "os"
13 "strings"
14 "syscall"
15 "time"
16
17 "golang.org/x/crypto/ssh/terminal"
18)
19
20const githubV3Url = "https://api.github.com"
21
22func Configure() (map[string]string, error) {
23 fmt.Println("git-bug will generate an access token in your Github profile.")
24 fmt.Println("TODO: describe token")
25 fmt.Println()
26
27 tokenName, err := promptTokenName()
28 if err != nil {
29 return nil, err
30 }
31
32 fmt.Println()
33
34 username, err := promptUsername()
35 if err != nil {
36 return nil, err
37 }
38
39 fmt.Println()
40
41 password, err := promptPassword()
42 if err != nil {
43 return nil, err
44 }
45
46 fmt.Println()
47
48 // Attempt to authenticate and create a token
49
50 var note string
51 if tokenName == "" {
52 note = "git-bug"
53 } else {
54 note = fmt.Sprintf("git-bug - %s", tokenName)
55 }
56
57 url := fmt.Sprintf("%s/authorizations", githubV3Url)
58 params := struct {
59 Scopes []string `json:"scopes"`
60 Note string `json:"note"`
61 Fingerprint string `json:"fingerprint"`
62 }{
63 Scopes: []string{"repo"},
64 Note: note,
65 Fingerprint: randomFingerprint(),
66 }
67
68 data, err := json.Marshal(params)
69 if err != nil {
70 return nil, err
71 }
72
73 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
74 if err != nil {
75 return nil, err
76 }
77 req.SetBasicAuth(username, password)
78 req.Header.Set("Content-Type", "application/json")
79 client := http.Client{}
80
81 resp, err := client.Do(req)
82 if err != nil {
83 return nil, err
84 }
85
86 defer resp.Body.Close()
87
88 if resp.StatusCode == http.StatusCreated {
89 return decodeBody(resp.Body)
90 }
91
92 // Handle 2FA is needed
93 OTPHeader := resp.Header.Get("X-GitHub-OTP")
94 if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
95 otpCode, err := prompt2FA()
96 if err != nil {
97 return nil, err
98 }
99
100 req.Header.Set("X-GitHub-OTP", otpCode)
101
102 resp2, err := client.Do(req)
103 if err != nil {
104 return nil, err
105 }
106
107 defer resp2.Body.Close()
108
109 if resp2.StatusCode == http.StatusCreated {
110 return decodeBody(resp.Body)
111 }
112 }
113
114 b, _ := ioutil.ReadAll(resp.Body)
115 fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
116
117 return nil, nil
118}
119
120func decodeBody(body io.ReadCloser) (map[string]string, error) {
121 data, _ := ioutil.ReadAll(body)
122
123 aux := struct {
124 Token string `json:"token"`
125 }{}
126
127 err := json.Unmarshal(data, &aux)
128 if err != nil {
129 return nil, err
130 }
131
132 return map[string]string{
133 "token": aux.Token,
134 }, nil
135}
136
137func randomFingerprint() string {
138 // Doesn't have to be crypto secure, it's just to avoid token collision
139 rand.Seed(time.Now().UnixNano())
140 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
141 b := make([]rune, 32)
142 for i := range b {
143 b[i] = letterRunes[rand.Intn(len(letterRunes))]
144 }
145 return string(b)
146}
147
148func promptUsername() (string, error) {
149 for {
150 fmt.Println("Username:")
151
152 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
153 if err != nil {
154 return "", err
155 }
156
157 line = strings.TrimRight(line, "\n")
158
159 ok, err := validateUsername(line)
160 if err != nil {
161 return "", err
162 }
163 if ok {
164 return line, nil
165 }
166
167 fmt.Println("invalid username")
168 }
169}
170
171func promptTokenName() (string, error) {
172 fmt.Println("To help distinguish the token, you can optionally provide a description")
173 fmt.Println("Token name:")
174
175 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
176 if err != nil {
177 return "", err
178 }
179
180 return strings.TrimRight(line, "\n"), nil
181}
182
183func validateUsername(username string) (bool, error) {
184 url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
185
186 resp, err := http.Get(url)
187 if err != nil {
188 return false, err
189 }
190
191 err = resp.Body.Close()
192 if err != nil {
193 return false, err
194 }
195
196 return resp.StatusCode == http.StatusOK, nil
197}
198
199func promptPassword() (string, error) {
200 for {
201 fmt.Println("Password:")
202
203 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
204 if err != nil {
205 return "", err
206 }
207
208 if len(bytePassword) > 0 {
209 return string(bytePassword), nil
210 }
211
212 fmt.Println("password is empty")
213 }
214}
215
216func prompt2FA() (string, error) {
217 for {
218 fmt.Println("Two-factor authentication code:")
219
220 byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
221 if err != nil {
222 return "", err
223 }
224
225 if len(byte2fa) > 0 {
226 return string(byte2fa), nil
227 }
228
229 fmt.Println("code is empty")
230 }
231}