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