aboutsummaryrefslogblamecommitdiffstats
path: root/bridge/github/config.go
blob: 79025cfb462faf9e384246b22cd2ea0322713645 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                       
                



                 

                                                    


                                          
                                            





                                                                                  
 
                     
                                                                                    
                                                                                                                                                                                   

                     
                                                    



                               


                                      




                                         




                                         

                                                     
                                                                        
 
                                                           





                               







                                                                          
                                                                                  



                                       
                                       
         
 



                                                   
                 

                                      







                                                                















                                                               





































                                                                                            
                                                     







                                           




                                                                                     

         
                             














                                                                                        
                                       



















                                                                       

                                          
                                                 






                                                                       
 

















                                                                    
                       
                          





                                                                   

         
                                  



















                                                                
                                       

                                                                              



                                                                                          













                                                        
                                                             












                                                                         
package github

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/MichaelMure/git-bug/bridge/core"
	"github.com/MichaelMure/git-bug/repository"
	"golang.org/x/crypto/ssh/terminal"
)

const githubV3Url = "https://api.github.com"
const keyUser = "user"
const keyProject = "project"
const keyToken = "token"

func (*Github) Configure(repo repository.RepoCommon) (core.Configuration, error) {
	conf := make(core.Configuration)

	fmt.Println()
	fmt.Println("git-bug will generate an access token in your Github profile.")
	// 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 :-|")
	fmt.Println()

	projectUser, projectName, err := promptURL()
	if err != nil {
		return nil, err
	}

	conf[keyUser] = projectUser
	conf[keyProject] = projectName

	username, err := promptUsername()
	if err != nil {
		return nil, err
	}

	password, err := promptPassword()
	if err != nil {
		return nil, err
	}

	// Attempt to authenticate and create a token

	note := fmt.Sprintf("git-bug - %s/%s", projectUser, projectName)

	resp, err := requestToken(note, username, password)
	if err != nil {
		return nil, err
	}

	defer resp.Body.Close()

	// Handle 2FA is needed
	OTPHeader := resp.Header.Get("X-GitHub-OTP")
	if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" {
		otpCode, err := prompt2FA()
		if err != nil {
			return nil, err
		}

		resp, err = requestTokenWith2FA(note, username, password, otpCode)
		if err != nil {
			return nil, err
		}

		defer resp.Body.Close()
	}

	if resp.StatusCode == http.StatusCreated {
		token, err := decodeBody(resp.Body)
		if err != nil {
			return nil, err
		}
		conf[keyToken] = token
		return conf, nil
	}

	b, _ := ioutil.ReadAll(resp.Body)
	fmt.Printf("Error %v: %v\n", resp.StatusCode, string(b))

	return nil, nil
}

func (*Github) ValidateConfig(conf core.Configuration) error {
	if _, ok := conf[keyToken]; !ok {
		return fmt.Errorf("missing %s key", keyToken)
	}

	if _, ok := conf[keyUser]; !ok {
		return fmt.Errorf("missing %s key", keyUser)
	}

	if _, ok := conf[keyProject]; !ok {
		return fmt.Errorf("missing %s key", keyProject)
	}

	return nil
}

func requestToken(note, username, password string) (*http.Response, error) {
	return requestTokenWith2FA(note, username, password, "")
}

func requestTokenWith2FA(note, username, password, otpCode string) (*http.Response, error) {
	url := fmt.Sprintf("%s/authorizations", githubV3Url)
	params := struct {
		Scopes      []string `json:"scopes"`
		Note        string   `json:"note"`
		Fingerprint string   `json:"fingerprint"`
	}{
		// Scopes:      []string{"repo"},
		Note:        note,
		Fingerprint: randomFingerprint(),
	}

	data, err := json.Marshal(params)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
	if err != nil {
		return nil, err
	}

	req.SetBasicAuth(username, password)
	req.Header.Set("Content-Type", "application/json")

	if otpCode != "" {
		req.Header.Set("X-GitHub-OTP", otpCode)
	}

	client := http.Client{}

	return client.Do(req)
}

func decodeBody(body io.ReadCloser) (string, error) {
	data, _ := ioutil.ReadAll(body)

	aux := struct {
		Token string `json:"token"`
	}{}

	err := json.Unmarshal(data, &aux)
	if err != nil {
		return "", err
	}

	if aux.Token == "" {
		return "", fmt.Errorf("no token found in response: %s", string(data))
	}

	return aux.Token, nil
}

func randomFingerprint() string {
	// Doesn't have to be crypto secure, it's just to avoid token collision
	rand.Seed(time.Now().UnixNano())
	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
	b := make([]rune, 32)
	for i := range b {
		b[i] = letterRunes[rand.Intn(len(letterRunes))]
	}
	return string(b)
}

func promptUsername() (string, error) {
	for {
		fmt.Print("username: ")

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			return "", err
		}

		line = strings.TrimRight(line, "\n")

		ok, err := validateUsername(line)
		if err != nil {
			return "", err
		}
		if ok {
			return line, nil
		}

		fmt.Println("invalid username")
	}
}

func promptURL() (string, string, error) {
	for {
		fmt.Print("Github project URL: ")

		line, err := bufio.NewReader(os.Stdin).ReadString('\n')
		if err != nil {
			return "", "", err
		}

		line = strings.TrimRight(line, "\n")

		if line == "" {
			fmt.Println("URL is empty")
			continue
		}

		projectUser, projectName, err := splitURL(line)

		if err != nil {
			fmt.Println(err)
			continue
		}

		return projectUser, projectName, nil
	}
}

func splitURL(url string) (string, string, error) {
	re, err := regexp.Compile(`github\.com\/([^\/]*)\/([^\/]*)`)
	if err != nil {
		panic(err)
	}

	res := re.FindStringSubmatch(url)

	if res == nil {
		return "", "", fmt.Errorf("bad github project url")
	}

	return res[1], res[2], nil
}

func validateUsername(username string) (bool, error) {
	url := fmt.Sprintf("%s/users/%s", githubV3Url, username)

	resp, err := http.Get(url)
	if err != nil {
		return false, err
	}

	err = resp.Body.Close()
	if err != nil {
		return false, err
	}

	return resp.StatusCode == http.StatusOK, nil
}

func promptPassword() (string, error) {
	for {
		fmt.Print("password: ")

		bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
		// new line for coherent formatting, ReadPassword clip the normal new line
		// entered by the user
		fmt.Println()

		if err != nil {
			return "", err
		}

		if len(bytePassword) > 0 {
			return string(bytePassword), nil
		}

		fmt.Println("password is empty")
	}
}

func prompt2FA() (string, error) {
	for {
		fmt.Print("two-factor authentication code: ")

		byte2fa, err := terminal.ReadPassword(int(syscall.Stdin))
		if err != nil {
			return "", err
		}

		if len(byte2fa) > 0 {
			return string(byte2fa), nil
		}

		fmt.Println("code is empty")
	}
}