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. Your credential are not stored and are only used to generate the token. The token is stored in the repository git config.")
 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}