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 (*Github) ValidateConfig(conf core.Configuration) error {
if _, ok := conf[keyToken]; !ok {
return fmt.Errorf("missing %s key", keyToken)
}
if _, ok := conf[keyUser]; !ok {
return fmt.Errorf("missing %s key", keyUser)
}
if _, ok := conf[keyProject]; !ok {
return fmt.Errorf("missing %s key", keyProject)
}
return 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 {
panic(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")
}
}