diff options
Diffstat (limited to 'bridge')
-rw-r--r-- | bridge/github/config.go | 212 | ||||
-rw-r--r-- | bridge/gitlab/import.go | 10 |
2 files changed, 126 insertions, 96 deletions
diff --git a/bridge/github/config.go b/bridge/github/config.go index 130b0ad1..2b5af7fb 100644 --- a/bridge/github/config.go +++ b/bridge/github/config.go @@ -1,16 +1,16 @@ package github import ( - "bytes" "context" "encoding/json" "fmt" - "io" "io/ioutil" "math/rand" "net/http" + "net/url" "regexp" "sort" + "strconv" "strings" "time" @@ -23,6 +23,8 @@ import ( "github.com/MichaelMure/git-bug/repository" ) +const githubClientID = "ce3600aa56c2e69f18a5" // git-bug org + var ( ErrBadProjectURL = errors.New("bad project url") ) @@ -169,63 +171,142 @@ func (*Github) ValidateConfig(conf core.Configuration) error { return nil } -func requestToken(note, login, password string, scope string) (*http.Response, error) { - return requestTokenWith2FA(note, login, password, "", scope) +func requestToken() (string, error) { + scope, err := promptUserForProjectVisibility() + if err != nil { + return "", errors.WithStack(err) + } + resp, err := requestUserVerificationCode(scope) + if err != nil { + return "", err + } + promptUserToGoToBrowser(resp.uri, resp.userCode) + return pollGithubForAuthorization(resp.deviceCode, resp.interval) } -func requestTokenWith2FA(note, login, password, otpCode string, scope 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{scope}, - Note: note, - Fingerprint: randomFingerprint(), +func promptUserForProjectVisibility() (string, error) { + fmt.Println("git-bug will now generate an access token in your Github profile. The token is stored in the global git config.") + fmt.Println() + fmt.Println("The access scope depend on the type of repository.") + fmt.Println("Public:") + fmt.Println(" - 'public_repo': to be able to read public repositories") + fmt.Println("Private:") + fmt.Println(" - 'repo' : to be able to read private repositories") + fmt.Println() + index, err := input.PromptChoice("repository visibility", []string{"public", "private"}) + if err != nil { + return "", err + } + return []string{"public_repo", "repo"}[index], nil +} + +type githRespT struct { + uri string + userCode string + deviceCode string + interval int64 +} + +func requestUserVerificationCode(scope string) (*githRespT, error) { + params := url.Values{} + params.Set("client_id", githubClientID) + params.Set("scope", scope) + client := &http.Client{ + Timeout: defaultTimeout, } - data, err := json.Marshal(params) + resp, err := client.PostForm("https://github.com/login/device/code", params) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error requesting user verification code") + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected response status code %d from Github API", resp.StatusCode) } - req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) + data, err := ioutil.ReadAll(resp.Body) if err != nil { - return nil, err + return nil, errors.Wrap(err, "error requesting user verification code") } - req.SetBasicAuth(login, password) - req.Header.Set("Content-Type", "application/json") + vals, err := url.ParseQuery(string(data)) + if err != nil { + return nil, errors.Wrap(err, "error decoding Github API response") + } - if otpCode != "" { - req.Header.Set("X-GitHub-OTP", otpCode) + interval, err := strconv.ParseInt(vals.Get("interval"), 10, 64) // base 10, bitSize 64 + if err != nil { + return nil, errors.Wrap(err, "Error parsing integer received from Github API") } + return &githRespT{ + uri: vals.Get("verification_uri"), + userCode: vals.Get("user_code"), + deviceCode: vals.Get("device_code"), + interval: interval, + }, nil +} + +func promptUserToGoToBrowser(url, userCode string) { + fmt.Println("Please visit the following Github URL in a browser and enter your user authentication code.") + fmt.Println() + fmt.Println(" URL:", url) + fmt.Println(" user authentiation code:", userCode) + fmt.Println() +} + +func pollGithubForAuthorization(deviceCode string, intervalSec int64) (string, error) { + params := url.Values{} + params.Set("client_id", githubClientID) + params.Set("device_code", deviceCode) + params.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") // fixed by RFC 8628 client := &http.Client{ Timeout: defaultTimeout, } + interval := time.Duration(intervalSec * 1100) // milliseconds, add 10% margin - return client.Do(req) -} + for { + resp, err := client.PostForm("https://github.com/login/oauth/access_token", params) + if err != nil { + return "", errors.Wrap(err, "error polling the Github API") + } + if resp.StatusCode != http.StatusOK { + _ = resp.Body.Close() + return "", fmt.Errorf("unexpected response status code %d from Github API", resp.StatusCode) + } -func decodeBody(body io.ReadCloser) (string, error) { - data, _ := ioutil.ReadAll(body) + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + _ = resp.Body.Close() + return "", errors.Wrap(err, "error polling the Github API") + } + _ = resp.Body.Close() - aux := struct { - Token string `json:"token"` - }{} + values, err := url.ParseQuery(string(data)) + if err != nil { + return "", errors.Wrap(err, "error decoding Github API response") + } - err := json.Unmarshal(data, &aux) - if err != nil { - return "", err - } + if token := values.Get("access_token"); token != "" { + return token, nil + } - if aux.Token == "" { - return "", fmt.Errorf("no token found in response: %s", string(data)) + switch apiError := values.Get("error"); apiError { + case "slow_down": + interval += 5500 // add 5 seconds (RFC 8628), plus some margin + time.Sleep(interval * time.Millisecond) + continue + case "authorization_pending": + time.Sleep(interval * time.Millisecond) + continue + case "": + return "", errors.New("unexpected response from Github API") + default: + // apiError should equal one of: "expired_token", "unsupported_grant_type", + // "incorrect_client_credentials", "incorrect_device_code", or "access_denied" + return "", fmt.Errorf("error creating token: %v, %v", apiError, values.Get("error_description")) + } } - - return aux.Token, nil } func randomFingerprint() string { @@ -261,7 +342,7 @@ func promptTokenOptions(repo repository.RepoKeyring, login, owner, project strin case index == 0: return promptToken() case index == 1: - value, err := loginAndRequestToken(login, owner, project) + value, err := requestToken() if err != nil { return nil, err } @@ -310,61 +391,6 @@ func promptToken() (*auth.Token, error) { return token, nil } -func loginAndRequestToken(login, owner, project string) (string, error) { - fmt.Println("git-bug will now 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 global git config.") - fmt.Println() - fmt.Println("The access scope depend on the type of repository.") - fmt.Println("Public:") - fmt.Println(" - 'public_repo': to be able to read public repositories") - fmt.Println("Private:") - fmt.Println(" - 'repo' : to be able to read private repositories") - fmt.Println() - - // prompt project visibility to know the token scope needed for the repository - index, err := input.PromptChoice("repository visibility", []string{"public", "private"}) - if err != nil { - return "", err - } - scope := []string{"public_repo", "repo"}[index] - - password, err := input.PromptPassword("Password", "password", input.Required) - if err != nil { - return "", err - } - - // Attempt to authenticate and create a token - note := fmt.Sprintf("git-bug - %s/%s", owner, project) - resp, err := requestToken(note, login, password, scope) - if err != nil { - return "", err - } - - defer resp.Body.Close() - - // Handle 2FA is needed - OTPHeader := resp.Header.Get("X-GitHub-OTP") - if resp.StatusCode == http.StatusUnauthorized && OTPHeader != "" { - otpCode, err := input.PromptPassword("Two-factor authentication code", "code", input.Required) - if err != nil { - return "", err - } - - resp, err = requestTokenWith2FA(note, login, password, otpCode, scope) - if err != nil { - return "", err - } - - defer resp.Body.Close() - } - - if resp.StatusCode == http.StatusCreated { - return decodeBody(resp.Body) - } - - b, _ := ioutil.ReadAll(resp.Body) - return "", fmt.Errorf("error creating token %v: %v", resp.StatusCode, string(b)) -} - func promptURL(repo repository.RepoCommon) (string, string, error) { validRemotes, err := getValidGithubRemoteURLs(repo) if err != nil { diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index 8aeab1ce..897d65de 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -138,7 +138,11 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue } // if bug was never imported - cleanText, err := text.Cleanup(issue.Description) + cleanTitle, err := text.Cleanup(issue.Title) + if err != nil { + return nil, err + } + cleanDesc, err := text.Cleanup(issue.Description) if err != nil { return nil, err } @@ -147,8 +151,8 @@ func (gi *gitlabImporter) ensureIssue(repo *cache.RepoCache, issue *gitlab.Issue b, _, err = repo.NewBugRaw( author, issue.CreatedAt.Unix(), - issue.Title, - cleanText, + cleanTitle, + cleanDesc, nil, map[string]string{ core.MetaKeyOrigin: target, |