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