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("The token will have the \"repo\" permission, giving it read/write access to your repositories and issues. There is no narrower scope available, sorry :-|")
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 resp, err := requestToken(note, username, password)
58 if err != nil {
59 return nil, err
60 }
61
62 defer resp.Body.Close()
63
64 if resp.StatusCode == http.StatusCreated {
65 return decodeBody(resp.Body)
66 }
67
68 // Handle 2FA is needed
69 OTPHeader := resp.Header.Get("X-GitHub-OTP")
70 if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
71 otpCode, err := prompt2FA()
72 if err != nil {
73 return nil, err
74 }
75
76 resp, err = requestTokenWith2FA(note, username, password, otpCode)
77 if err != nil {
78 return nil, err
79 }
80
81 defer resp.Body.Close()
82
83 if resp.StatusCode == http.StatusCreated {
84 return decodeBody(resp.Body)
85 }
86 }
87
88 b, _ := ioutil.ReadAll(resp.Body)
89 fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
90
91 return nil, nil
92}
93
94func requestToken(note, username, password string) (*http.Response, error) {
95 return requestTokenWith2FA(note, username, password, "")
96}
97
98func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
99 url := fmt.Sprintf("%s/authorizations", githubV3Url)
100 params := struct {
101 Scopes []string `json:"scopes"`
102 Note string `json:"note"`
103 Fingerprint string `json:"fingerprint"`
104 }{
105 // Scopes: []string{"repo"},
106 Note: note,
107 Fingerprint: randomFingerprint(),
108 }
109
110 data, err := json.Marshal(params)
111 if err != nil {
112 return nil, err
113 }
114
115 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
116 if err != nil {
117 return nil, err
118 }
119
120 req.SetBasicAuth(username, password)
121 req.Header.Set("Content-Type", "application/json")
122
123 if otpCode != "" {
124 req.Header.Set("X-GitHub-OTP", otpCode)
125 }
126
127 client := http.Client{}
128
129 return client.Do(req)
130}
131
132func decodeBody(body io.ReadCloser) (map[string]string, error) {
133 data, _ := ioutil.ReadAll(body)
134
135 aux := struct {
136 Token string `json:"token"`
137 }{}
138
139 err := json.Unmarshal(data, &aux)
140 if err != nil {
141 return nil, err
142 }
143
144 return map[string]string{
145 "token": aux.Token,
146 }, nil
147}
148
149func randomFingerprint() string {
150 // Doesn't have to be crypto secure, it's just to avoid token collision
151 rand.Seed(time.Now().UnixNano())
152 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
153 b := make([]rune, 32)
154 for i := range b {
155 b[i] = letterRunes[rand.Intn(len(letterRunes))]
156 }
157 return string(b)
158}
159
160func promptUsername() (string, error) {
161 for {
162 fmt.Println("username:")
163
164 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
165 if err != nil {
166 return "", err
167 }
168
169 line = strings.TrimRight(line, "\n")
170
171 ok, err := validateUsername(line)
172 if err != nil {
173 return "", err
174 }
175 if ok {
176 return line, nil
177 }
178
179 fmt.Println("invalid username")
180 }
181}
182
183func promptTokenName() (string, error) {
184 fmt.Println("To help distinguish the token, you can optionally provide a description")
185 fmt.Println("The token will be named \"git-bug - <description>\"")
186 fmt.Println("description:")
187
188 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
189 if err != nil {
190 return "", err
191 }
192
193 return strings.TrimRight(line, "\n"), nil
194}
195
196func validateUsername(username string) (bool, error) {
197 url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
198
199 resp, err := http.Get(url)
200 if err != nil {
201 return false, err
202 }
203
204 err = resp.Body.Close()
205 if err != nil {
206 return false, err
207 }
208
209 return resp.StatusCode == http.StatusOK, nil
210}
211
212func promptPassword() (string, error) {
213 for {
214 fmt.Println("password:")
215
216 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
217 if err != nil {
218 return "", err
219 }
220
221 if len(bytePassword) > 0 {
222 return string(bytePassword), nil
223 }
224
225 fmt.Println("password is empty")
226 }
227}
228
229func prompt2FA() (string, error) {
230 for {
231 fmt.Println("two-factor authentication code:")
232
233 byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
234 if err != nil {
235 return "", err
236 }
237
238 if len(byte2fa) > 0 {
239 return string(byte2fa), nil
240 }
241
242 fmt.Println("code is empty")
243 }
244}