github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/utils/auth.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: 2021-Present The Jackal Authors 3 4 // Package utils provides generic utility functions. 5 package utils 6 7 import ( 8 "bufio" 9 "io" 10 "net/url" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/Racer159/jackal/src/pkg/message" 16 "github.com/go-git/go-git/v5/plumbing/transport/http" 17 ) 18 19 // Credential represents authentication for a given host. 20 type Credential struct { 21 Path string 22 Auth http.BasicAuth 23 } 24 25 // FindAuthForHost finds the authentication scheme for a given host using .git-credentials then .netrc. 26 func FindAuthForHost(baseURL string) *Credential { 27 homePath, _ := os.UserHomeDir() 28 29 // Read the ~/.git-credentials file 30 credentialsPath := filepath.Join(homePath, ".git-credentials") 31 // Dogsled the error since we are ok if this file doesn't exist (error message debugged on close) 32 credentialsFile, _ := os.Open(credentialsPath) 33 gitCreds := credentialParser(credentialsFile) 34 35 // Read the ~/.netrc file 36 netrcPath := filepath.Join(homePath, ".netrc") 37 // Dogsled the error since we are ok if this file doesn't exist (error message debugged on close) 38 netrcFile, _ := os.Open(netrcPath) 39 netrcCreds := netrcParser(netrcFile) 40 41 // Combine the creds together (.netrc second because it could have a default) 42 creds := append(gitCreds, netrcCreds...) 43 44 // Look for a match for the given host path in the creds file 45 for _, cred := range creds { 46 // An empty credPath means that we have reached the default from the .netrc 47 hasPath := strings.Contains(baseURL, cred.Path) || cred.Path == "" 48 if hasPath { 49 return &cred 50 } 51 } 52 53 return nil 54 } 55 56 // credentialParser parses a user's .git-credentials file to find git creds for hosts. 57 func credentialParser(file io.ReadCloser) []Credential { 58 var credentials []Credential 59 60 defer func(file io.ReadCloser) { 61 err := file.Close() 62 if err != nil { 63 message.Debugf("Unable to load an existing git credentials file: %s", err.Error()) 64 } 65 }(file) 66 67 scanner := bufio.NewScanner(file) 68 for scanner.Scan() { 69 gitURL, err := url.Parse(scanner.Text()) 70 if err != nil || gitURL.Host == "" { 71 continue 72 } 73 password, _ := gitURL.User.Password() 74 credential := Credential{ 75 Path: gitURL.Host, 76 Auth: http.BasicAuth{ 77 Username: gitURL.User.Username(), 78 Password: password, 79 }, 80 } 81 credentials = append(credentials, credential) 82 } 83 84 return credentials 85 } 86 87 // netrcParser parses a user's .netrc file using the method curl did pre 7.84.0: https://daniel.haxx.se/blog/2022/05/31/netrc-pains/. 88 func netrcParser(file io.ReadCloser) []Credential { 89 var credentials []Credential 90 91 defer func(file io.ReadCloser) { 92 err := file.Close() 93 if err != nil { 94 message.Debugf("Unable to load an existing netrc file: %s", err.Error()) 95 } 96 }(file) 97 98 scanner := bufio.NewScanner(file) 99 100 activeMacro := false 101 activeCommand := "" 102 var activeMachine map[string]string 103 104 for scanner.Scan() { 105 line := scanner.Text() 106 107 // If we are in a macro block, continue 108 if activeMacro { 109 if line == "" { 110 activeMacro = false 111 } 112 continue 113 } 114 115 // Prepare our line to be tokenized 116 line = strings.ReplaceAll(line, "\t", " ") 117 line = strings.TrimSpace(line) 118 119 tokens := strings.Split(line, " ") 120 121 for _, token := range tokens { 122 if activeCommand != "" { 123 // If we are in an active command, process the next token as a value 124 activeMachine[activeCommand] = token 125 activeCommand = "" 126 } else if strings.HasPrefix(token, "#") { 127 // If we have entered into a comment, don't process it 128 // NOTE: We could use a similar technique to this for spaces in the future 129 // by detecting leading " and trailing \. See top of function for more info 130 break 131 } else { 132 switch token { 133 case "machine": 134 // If the token is the start of a machine, append the last machine (if exists) and make a new one 135 if activeMachine != nil { 136 credentials = appendNetrcMachine(activeMachine, credentials) 137 } 138 activeMachine = map[string]string{} 139 activeCommand = token 140 case "macdef": 141 // If the token is the start of a macro, enter macro mode 142 activeMacro = true 143 activeCommand = token 144 case "login", "password", "account": 145 // If the token is a regular command, set the now active command 146 activeCommand = token 147 case "default": 148 // If the token is the default machine, append the last machine (if exists) and make a default one 149 if activeMachine != nil { 150 credentials = appendNetrcMachine(activeMachine, credentials) 151 } 152 activeMachine = map[string]string{"machine": ""} 153 } 154 } 155 } 156 } 157 158 // Append the last machine (if exists) at the end of the file 159 if activeMachine != nil { 160 credentials = appendNetrcMachine(activeMachine, credentials) 161 } 162 163 return credentials 164 } 165 166 func appendNetrcMachine(machine map[string]string, credentials []Credential) []Credential { 167 credential := Credential{ 168 Path: machine["machine"], 169 Auth: http.BasicAuth{ 170 Username: machine["login"], 171 Password: machine["password"], 172 }, 173 } 174 175 return append(credentials, credential) 176 }