From 921cd18cf98ecfc1f7fa82f57d64f1b1f9077e64 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Fri, 21 Sep 2018 18:23:46 +0200 Subject: bridge: better interfaces, working github configurator --- bridge/github/auth.go | 244 ----------------------------------------- bridge/github/config.go | 282 ++++++++++++++++++++++++++++++++++++++++++++++++ bridge/github/github.go | 28 ++++- 3 files changed, 309 insertions(+), 245 deletions(-) delete mode 100644 bridge/github/auth.go create mode 100644 bridge/github/config.go (limited to 'bridge/github') diff --git a/bridge/github/auth.go b/bridge/github/auth.go deleted file mode 100644 index b721df7f..00000000 --- a/bridge/github/auth.go +++ /dev/null @@ -1,244 +0,0 @@ -package github - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "math/rand" - "net/http" - "os" - "strings" - "syscall" - "time" - - "golang.org/x/crypto/ssh/terminal" -) - -const githubV3Url = "https://api.github.com" - -func Configure() (map[string]string, error) { - 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() - - tokenName, err := promptTokenName() - if err != nil { - return nil, err - } - - 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 - - var note string - if tokenName == "" { - note = "git-bug" - } else { - note = fmt.Sprintf("git-bug - %s", tokenName) - } - - resp, err := requestToken(note, username, password) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - if resp.StatusCode == http.StatusCreated { - return decodeBody(resp.Body) - } - - // 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 { - return decodeBody(resp.Body) - } - } - - 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) (map[string]string, error) { - data, _ := ioutil.ReadAll(body) - - aux := struct { - Token string `json:"token"` - }{} - - err := json.Unmarshal(data, &aux) - if err != nil { - return nil, err - } - - return map[string]string{ - "token": 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 promptTokenName() (string, error) { - fmt.Println("To help distinguish the token, you can optionally provide a description") - fmt.Println("The token will be named \"git-bug - \"") - fmt.Println("description:") - - line, err := bufio.NewReader(os.Stdin).ReadString('\n') - if err != nil { - return "", err - } - - return strings.TrimRight(line, "\n"), 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") - } -} 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") + } +} diff --git a/bridge/github/github.go b/bridge/github/github.go index 0238b4bf..45954e23 100644 --- a/bridge/github/github.go +++ b/bridge/github/github.go @@ -1,4 +1,30 @@ package github -type github struct { +import ( + "github.com/MichaelMure/git-bug/bridge/core" + "github.com/MichaelMure/git-bug/cache" +) + +type Github struct{} + +func (*Github) Name() string { + return "github" +} + +func (*Github) Importer() core.Importer { + return &githubImporter{} +} + +func (*Github) Exporter() core.Exporter { + return nil +} + +type githubImporter struct{} + +func (*githubImporter) ImportAll(repo *cache.RepoCache, conf core.Configuration) error { + panic("implement me") +} + +func (*githubImporter) Import(repo *cache.RepoCache, conf core.Configuration, id string) error { + panic("implement me") } -- cgit