2018-09-21 13:54:48 +03:00
package github
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"math/rand"
"net/http"
"os"
2018-09-21 19:23:46 +03:00
"regexp"
2018-09-21 13:54:48 +03:00
"strings"
"syscall"
"time"
2018-09-21 19:23:46 +03:00
"github.com/MichaelMure/git-bug/bridge/core"
"github.com/MichaelMure/git-bug/repository"
2018-09-21 13:54:48 +03:00
"golang.org/x/crypto/ssh/terminal"
)
2019-04-27 02:15:02 +03:00
const (
githubV3Url = "https://api.github.com"
keyUser = "user"
keyProject = "project"
keyToken = "token"
)
2018-09-21 19:23:46 +03:00
func ( * Github ) Configure ( repo repository . RepoCommon ) ( core . Configuration , error ) {
conf := make ( core . Configuration )
2018-09-21 13:54:48 +03:00
2018-09-24 16:25:15 +03:00
fmt . Println ( )
2018-10-07 19:27:23 +03:00
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 repository git config." )
fmt . Println ( )
fmt . Println ( "The token will have the following scopes:" )
fmt . Println ( " - user:email: to be able to read public-only users email" )
2018-09-21 15:38:44 +03:00
// 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 :-|")
2018-09-21 13:54:48 +03:00
fmt . Println ( )
2018-09-21 19:23:46 +03:00
projectUser , projectName , err := promptURL ( )
2018-09-21 13:54:48 +03:00
if err != nil {
return nil , err
}
2018-09-21 19:23:46 +03:00
conf [ keyUser ] = projectUser
conf [ keyProject ] = projectName
2018-09-21 13:54:48 +03:00
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
2018-09-21 19:23:46 +03:00
note := fmt . Sprintf ( "git-bug - %s/%s" , projectUser , projectName )
2018-09-21 13:54:48 +03:00
2018-09-21 15:38:44 +03:00
resp , err := requestToken ( note , username , password )
2018-09-21 13:54:48 +03:00
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
}
2018-09-21 15:38:44 +03:00
resp , err = requestTokenWith2FA ( note , username , password , otpCode )
2018-09-21 13:54:48 +03:00
if err != nil {
return nil , err
}
2018-09-21 15:38:44 +03:00
defer resp . Body . Close ( )
2018-09-21 19:23:46 +03:00
}
2018-09-21 13:54:48 +03:00
2018-09-21 19:23:46 +03:00
if resp . StatusCode == http . StatusCreated {
token , err := decodeBody ( resp . Body )
if err != nil {
return nil , err
2018-09-21 13:54:48 +03:00
}
2018-09-21 19:23:46 +03:00
conf [ keyToken ] = token
return conf , nil
2018-09-21 13:54:48 +03:00
}
b , _ := ioutil . ReadAll ( resp . Body )
fmt . Printf ( "Error %v: %v\n" , resp . StatusCode , string ( b ) )
return nil , nil
}
2018-09-24 18:21:24 +03:00
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
}
2018-09-21 15:38:44 +03:00
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" `
} {
2018-10-07 19:27:23 +03:00
// user:email is requested to be able to read public emails
// - a private email will stay private, even with this token
Scopes : [ ] string { "user:email" } ,
2018-09-21 15:38:44 +03:00
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 )
}
2018-09-21 19:23:46 +03:00
func decodeBody ( body io . ReadCloser ) ( string , error ) {
2018-09-21 13:54:48 +03:00
data , _ := ioutil . ReadAll ( body )
aux := struct {
Token string ` json:"token" `
} { }
err := json . Unmarshal ( data , & aux )
if err != nil {
2018-09-21 19:23:46 +03:00
return "" , err
}
if aux . Token == "" {
return "" , fmt . Errorf ( "no token found in response: %s" , string ( data ) )
2018-09-21 13:54:48 +03:00
}
2018-09-21 19:23:46 +03:00
return aux . Token , nil
2018-09-21 13:54:48 +03:00
}
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 {
2018-09-24 16:25:15 +03:00
fmt . Print ( "username: " )
2018-09-21 13:54:48 +03:00
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" )
}
}
2018-09-21 19:23:46 +03:00
func promptURL ( ) ( string , string , error ) {
for {
2018-09-24 16:25:15 +03:00
fmt . Print ( "Github project URL: " )
2018-09-21 19:23:46 +03:00
line , err := bufio . NewReader ( os . Stdin ) . ReadString ( '\n' )
if err != nil {
return "" , "" , err
}
line = strings . TrimRight ( line , "\n" )
2018-09-21 13:54:48 +03:00
2018-09-21 19:23:46 +03:00
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\/([^\/]*)\/([^\/]*) ` )
2018-09-21 13:54:48 +03:00
if err != nil {
2018-09-24 17:24:38 +03:00
panic ( err )
2018-09-21 19:23:46 +03:00
}
res := re . FindStringSubmatch ( url )
if res == nil {
return "" , "" , fmt . Errorf ( "bad github project url" )
2018-09-21 13:54:48 +03:00
}
2018-09-21 19:23:46 +03:00
return res [ 1 ] , res [ 2 ] , nil
2018-09-21 13:54:48 +03:00
}
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 {
2018-09-24 16:25:15 +03:00
fmt . Print ( "password: " )
2018-09-21 13:54:48 +03:00
bytePassword , err := terminal . ReadPassword ( int ( syscall . Stdin ) )
2018-09-24 16:25:15 +03:00
// new line for coherent formatting, ReadPassword clip the normal new line
// entered by the user
fmt . Println ( )
2018-09-21 13:54:48 +03:00
if err != nil {
return "" , err
}
if len ( bytePassword ) > 0 {
return string ( bytePassword ) , nil
}
fmt . Println ( "password is empty" )
}
}
func prompt2FA ( ) ( string , error ) {
for {
2018-09-24 16:25:15 +03:00
fmt . Print ( "two-factor authentication code: " )
2018-09-21 13:54:48 +03:00
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" )
}
}