mirror of
https://github.com/MichaelMure/git-bug.git
synced 2024-12-15 18:23:08 +03:00
282 lines
5.5 KiB
Go
282 lines
5.5 KiB
Go
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 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 {
|
|
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.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")
|
|
}
|
|
}
|