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  }