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()
 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}