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"
)
2018-09-24 16:25:15 +03:00
const githubV3Url = "https://api.github.com"
2018-09-21 19:23:46 +03:00
const keyUser = "user"
const keyProject = "project"
const keyToken = "token"
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-09-21 13:54:48 +03:00
fmt . Println ( "git-bug will generate an access token in your Github profile." )
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-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" `
} {
// 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 )
}
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" )
}
}