aboutsummaryrefslogtreecommitdiffstats
path: root/bridge/github/config.go
diff options
context:
space:
mode:
authorMichael Muré <batolettre@gmail.com>2018-09-21 18:23:46 +0200
committerMichael Muré <batolettre@gmail.com>2018-09-21 18:53:44 +0200
commit921cd18cf98ecfc1f7fa82f57d64f1b1f9077e64 (patch)
treedfd7fea5be54c6020ed6773fbdefbaeb5cd2a78e /bridge/github/config.go
parent82eaceffc1d750832a2a66f206749d2dca968cce (diff)
downloadgit-bug-921cd18cf98ecfc1f7fa82f57d64f1b1f9077e64.tar.gz
bridge: better interfaces, working github configurator
Diffstat (limited to 'bridge/github/config.go')
-rw-r--r--bridge/github/config.go282
1 files changed, 282 insertions, 0 deletions
diff --git a/bridge/github/config.go b/bridge/github/config.go
new file mode 100644
index 00000000..3b12d3f9
--- /dev/null
+++ b/bridge/github/config.go
@@ -0,0 +1,282 @@
+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("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
+
+ fmt.Println()
+
+ username, err := promptUsername()
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Println()
+
+ password, err := promptPassword()
+ if err != nil {
+ return nil, err
+ }
+
+ fmt.Println()
+
+ // 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 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.Println("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.Println("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 {
+ return "", "", 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.Println("password:")
+
+ bytePassword, err := terminal.ReadPassword(int(syscall.Stdin))
+ if err != nil {
+ return "", err
+ }
+
+ if len(bytePassword) > 0 {
+ return string(bytePassword), nil
+ }
+
+ fmt.Println("password is empty")
+ }
+}
+
+func prompt2FA() (string, error) {
+ for {
+ fmt.Println("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")
+ }
+}