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 "regexp"
14 "strings"
15 "syscall"
16 "time"
17
18 "github.com/MichaelMure/git-bug/bridge/core"
19 "github.com/MichaelMure/git-bug/repository"
20 "golang.org/x/crypto/ssh/terminal"
21)
22
23const githubV3Url = "https://api.github.com"
24const keyUser = "user"
25const keyProject = "project"
26const keyToken = "token"
27
28func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) {
29 conf := make(core.Configuration)
30
31 fmt.Println()
32 fmt.Println("git-bug will generate an access token in your Github profile.")
33 // 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 :-|")
34 fmt.Println()
35
36 projectUser, projectName, err := promptURL()
37 if err != nil {
38 return nil, err
39 }
40
41 conf[keyUser] = projectUser
42 conf[keyProject] = projectName
43
44 username, err := promptUsername()
45 if err != nil {
46 return nil, err
47 }
48
49 password, err := promptPassword()
50 if err != nil {
51 return nil, err
52 }
53
54 // Attempt to authenticate and create a token
55
56 note := fmt.Sprintf("git-bug - %s/%s", projectUser, projectName)
57
58 resp, err := requestToken(note, username, password)
59 if err != nil {
60 return nil, err
61 }
62
63 defer resp.Body.Close()
64
65 // Handle 2FA is needed
66 OTPHeader := resp.Header.Get("X-GitHub-OTP")
67 if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
68 otpCode, err := prompt2FA()
69 if err != nil {
70 return nil, err
71 }
72
73 resp, err = requestTokenWith2FA(note, username, password, otpCode)
74 if err != nil {
75 return nil, err
76 }
77
78 defer resp.Body.Close()
79 }
80
81 if resp.StatusCode == http.StatusCreated {
82 token, err := decodeBody(resp.Body)
83 if err != nil {
84 return nil, err
85 }
86 conf[keyToken] = token
87 return conf, nil
88 }
89
90 b, _ := ioutil.ReadAll(resp.Body)
91 fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))
92
93 return nil, nil
94}
95
96func (*Github) ValidateConfig(conf core.Configuration) error {
97 if _, ok := conf[keyToken]; !ok {
98 return fmt.Errorf("missing %s key", keyToken)
99 }
100
101 if _, ok := conf[keyUser]; !ok {
102 return fmt.Errorf("missing %s key", keyUser)
103 }
104
105 if _, ok := conf[keyProject]; !ok {
106 return fmt.Errorf("missing %s key", keyProject)
107 }
108
109 return nil
110}
111
112func requestToken(note, username, password string) (*http.Response, error) {
113 return requestTokenWith2FA(note, username, password, "")
114}
115
116func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
117 url := fmt.Sprintf("%s/authorizations", githubV3Url)
118 params := struct {
119 Scopes []string `json:"scopes"`
120 Note string `json:"note"`
121 Fingerprint string `json:"fingerprint"`
122 }{
123 // Scopes: []string{"repo"},
124 Note: note,
125 Fingerprint: randomFingerprint(),
126 }
127
128 data, err := json.Marshal(params)
129 if err != nil {
130 return nil, err
131 }
132
133 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
134 if err != nil {
135 return nil, err
136 }
137
138 req.SetBasicAuth(username, password)
139 req.Header.Set("Content-Type", "application/json")
140
141 if otpCode != "" {
142 req.Header.Set("X-GitHub-OTP", otpCode)
143 }
144
145 client := http.Client{}
146
147 return client.Do(req)
148}
149
150func decodeBody(body io.ReadCloser) (string, error) {
151 data, _ := ioutil.ReadAll(body)
152
153 aux := struct {
154 Token string `json:"token"`
155 }{}
156
157 err := json.Unmarshal(data, &aux)
158 if err != nil {
159 return "", err
160 }
161
162 if aux.Token == "" {
163 return "", fmt.Errorf("no token found in response: %s", string(data))
164 }
165
166 return aux.Token, nil
167}
168
169func randomFingerprint() string {
170 // Doesn't have to be crypto secure, it's just to avoid token collision
171 rand.Seed(time.Now().UnixNano())
172 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
173 b := make([]rune, 32)
174 for i := range b {
175 b[i] = letterRunes[rand.Intn(len(letterRunes))]
176 }
177 return string(b)
178}
179
180func promptUsername() (string, error) {
181 for {
182 fmt.Print("username: ")
183
184 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
185 if err != nil {
186 return "", err
187 }
188
189 line = strings.TrimRight(line, "\n")
190
191 ok, err := validateUsername(line)
192 if err != nil {
193 return "", err
194 }
195 if ok {
196 return line, nil
197 }
198
199 fmt.Println("invalid username")
200 }
201}
202
203func promptURL() (string, string, error) {
204 for {
205 fmt.Print("Github project URL: ")
206
207 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
208 if err != nil {
209 return "", "", err
210 }
211
212 line = strings.TrimRight(line, "\n")
213
214 if line == "" {
215 fmt.Println("URL is empty")
216 continue
217 }
218
219 projectUser, projectName, err := splitURL(line)
220
221 if err != nil {
222 fmt.Println(err)
223 continue
224 }
225
226 return projectUser, projectName, nil
227 }
228}
229
230func splitURL(url string) (string, string, error) {
231 re, err := regexp.Compile(`github\.com\/([^\/]*)\/([^\/]*)`)
232 if err != nil {
233 panic(err)
234 }
235
236 res := re.FindStringSubmatch(url)
237
238 if res == nil {
239 return "", "", fmt.Errorf("bad github project url")
240 }
241
242 return res[1], res[2], nil
243}
244
245func validateUsername(username string) (bool, error) {
246 url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
247
248 resp, err := http.Get(url)
249 if err != nil {
250 return false, err
251 }
252
253 err = resp.Body.Close()
254 if err != nil {
255 return false, err
256 }
257
258 return resp.StatusCode == http.StatusOK, nil
259}
260
261func promptPassword() (string, error) {
262 for {
263 fmt.Print("password: ")
264
265 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
266 // new line for coherent formatting, ReadPassword clip the normal new line
267 // entered by the user
268 fmt.Println()
269
270 if err != nil {
271 return "", err
272 }
273
274 if len(bytePassword) > 0 {
275 return string(bytePassword), nil
276 }
277
278 fmt.Println("password is empty")
279 }
280}
281
282func prompt2FA() (string, error) {
283 for {
284 fmt.Print("two-factor authentication code: ")
285
286 byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
287 if err != nil {
288 return "", err
289 }
290
291 if len(byte2fa) > 0 {
292 return string(byte2fa), nil
293 }
294
295 fmt.Println("code is empty")
296 }
297}