diff options
author | Michael Muré <batolettre@gmail.com> | 2018-09-21 18:23:46 +0200 |
---|---|---|
committer | Michael Muré <batolettre@gmail.com> | 2018-09-21 18:53:44 +0200 |
commit | 921cd18cf98ecfc1f7fa82f57d64f1b1f9077e64 (patch) | |
tree | dfd7fea5be54c6020ed6773fbdefbaeb5cd2a78e /bridge/github/config.go | |
parent | 82eaceffc1d750832a2a66f206749d2dca968cce (diff) | |
download | git-bug-921cd18cf98ecfc1f7fa82f57d64f1b1f9077e64.tar.gz |
bridge: better interfaces, working github configurator
Diffstat (limited to 'bridge/github/config.go')
-rw-r--r-- | bridge/github/config.go | 282 |
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") + } +} |