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 requestToken(note, username, password string) (*http.Response, error) {
97 return requestTokenWith2FA(note, username, password, "")
98}
99
100func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
101 url := fmt.Sprintf("%s/authorizations", githubV3Url)
102 params := struct {
103 Scopes []string `json:"scopes"`
104 Note string `json:"note"`
105 Fingerprint string `json:"fingerprint"`
106 }{
107 // Scopes: []string{"repo"},
108 Note: note,
109 Fingerprint: randomFingerprint(),
110 }
111
112 data, err := json.Marshal(params)
113 if err != nil {
114 return nil, err
115 }
116
117 req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
118 if err != nil {
119 return nil, err
120 }
121
122 req.SetBasicAuth(username, password)
123 req.Header.Set("Content-Type", "application/json")
124
125 if otpCode != "" {
126 req.Header.Set("X-GitHub-OTP", otpCode)
127 }
128
129 client := http.Client{}
130
131 return client.Do(req)
132}
133
134func decodeBody(body io.ReadCloser) (string, error) {
135 data, _ := ioutil.ReadAll(body)
136
137 aux := struct {
138 Token string `json:"token"`
139 }{}
140
141 err := json.Unmarshal(data, &aux)
142 if err != nil {
143 return "", err
144 }
145
146 if aux.Token == "" {
147 return "", fmt.Errorf("no token found in response: %s", string(data))
148 }
149
150 return aux.Token, nil
151}
152
153func randomFingerprint() string {
154 // Doesn't have to be crypto secure, it's just to avoid token collision
155 rand.Seed(time.Now().UnixNano())
156 var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
157 b := make([]rune, 32)
158 for i := range b {
159 b[i] = letterRunes[rand.Intn(len(letterRunes))]
160 }
161 return string(b)
162}
163
164func promptUsername() (string, error) {
165 for {
166 fmt.Print("username: ")
167
168 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
169 if err != nil {
170 return "", err
171 }
172
173 line = strings.TrimRight(line, "\n")
174
175 ok, err := validateUsername(line)
176 if err != nil {
177 return "", err
178 }
179 if ok {
180 return line, nil
181 }
182
183 fmt.Println("invalid username")
184 }
185}
186
187func promptURL() (string, string, error) {
188 for {
189 fmt.Print("Github project URL: ")
190
191 line, err := bufio.NewReader(os.Stdin).ReadString('\n')
192 if err != nil {
193 return "", "", err
194 }
195
196 line = strings.TrimRight(line, "\n")
197
198 if line == "" {
199 fmt.Println("URL is empty")
200 continue
201 }
202
203 projectUser, projectName, err := splitURL(line)
204
205 if err != nil {
206 fmt.Println(err)
207 continue
208 }
209
210 return projectUser, projectName, nil
211 }
212}
213
214func splitURL(url string) (string, string, error) {
215 re, err := regexp.Compile(`github\.com\/([^\/]*)\/([^\/]*)`)
216 if err != nil {
217 panic(err)
218 }
219
220 res := re.FindStringSubmatch(url)
221
222 if res == nil {
223 return "", "", fmt.Errorf("bad github project url")
224 }
225
226 return res[1], res[2], nil
227}
228
229func validateUsername(username string) (bool, error) {
230 url := fmt.Sprintf("%s/users/%s", githubV3Url, username)
231
232 resp, err := http.Get(url)
233 if err != nil {
234 return false, err
235 }
236
237 err = resp.Body.Close()
238 if err != nil {
239 return false, err
240 }
241
242 return resp.StatusCode == http.StatusOK, nil
243}
244
245func promptPassword() (string, error) {
246 for {
247 fmt.Print("password: ")
248
249 bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
250 // new line for coherent formatting, ReadPassword clip the normal new line
251 // entered by the user
252 fmt.Println()
253
254 if err != nil {
255 return "", err
256 }
257
258 if len(bytePassword) > 0 {
259 return string(bytePassword), nil
260 }
261
262 fmt.Println("password is empty")
263 }
264}
265
266func prompt2FA() (string, error) {
267 for {
268 fmt.Print("two-factor authentication code: ")
269
270 byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
271 if err != nil {
272 return "", err
273 }
274
275 if len(byte2fa) > 0 {
276 return string(byte2fa), nil
277 }
278
279 fmt.Println("code is empty")
280 }
281}