config.go

  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}