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 - <description>\"")
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")
}
}