Source file src/cmd/go/internal/auth/auth.go

     1  // Copyright 2019 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package auth provides access to user-provided authentication credentials.
     6  package auth
     7  
     8  import (
     9  	"cmd/go/internal/base"
    10  	"cmd/go/internal/cfg"
    11  	"fmt"
    12  	"log"
    13  	"net/http"
    14  	"os"
    15  	"path"
    16  	"path/filepath"
    17  	"slices"
    18  	"strings"
    19  	"sync"
    20  )
    21  
    22  var (
    23  	credentialCache sync.Map // prefix → http.Header
    24  	authOnce        sync.Once
    25  )
    26  
    27  // AddCredentials populates the request header with the user's credentials
    28  // as specified by the GOAUTH environment variable.
    29  // It returns whether any matching credentials were found.
    30  // req must use HTTPS or this function will panic.
    31  func AddCredentials(client *http.Client, req *http.Request, prefix string) bool {
    32  	if req.URL.Scheme != "https" {
    33  		panic("GOAUTH called without https")
    34  	}
    35  	if cfg.GOAUTH == "off" {
    36  		return false
    37  	}
    38  	// Run all GOAUTH commands at least once.
    39  	authOnce.Do(func() {
    40  		runGoAuth(client, "")
    41  	})
    42  	if prefix != "" {
    43  		// First fetch must have failed; re-invoke GOAUTH commands with prefix.
    44  		runGoAuth(client, prefix)
    45  	}
    46  	currentPrefix := strings.TrimPrefix(req.URL.String(), "https://")
    47  	// Iteratively try prefixes, moving up the path hierarchy.
    48  	for currentPrefix != "/" && currentPrefix != "." && currentPrefix != "" {
    49  		if loadCredential(req, currentPrefix) {
    50  			return true
    51  		}
    52  
    53  		// Move to the parent directory.
    54  		currentPrefix = path.Dir(currentPrefix)
    55  	}
    56  	return false
    57  }
    58  
    59  // runGoAuth executes authentication commands specified by the GOAUTH
    60  // environment variable handling 'off', 'netrc', and 'git' methods specially,
    61  // and storing retrieved credentials for future access.
    62  func runGoAuth(client *http.Client, prefix string) {
    63  	var cmdErrs []error // store GOAUTH command errors to log later.
    64  	goAuthCmds := strings.Split(cfg.GOAUTH, ";")
    65  	// The GOAUTH commands are processed in reverse order to prioritize
    66  	// credentials in the order they were specified.
    67  	slices.Reverse(goAuthCmds)
    68  	for _, cmdStr := range goAuthCmds {
    69  		cmdStr = strings.TrimSpace(cmdStr)
    70  		cmdParts := strings.Fields(cmdStr)
    71  		if len(cmdParts) == 0 {
    72  			base.Fatalf("GOAUTH encountered an empty command (GOAUTH=%s)", cfg.GOAUTH)
    73  		}
    74  		switch cmdParts[0] {
    75  		case "off":
    76  			if len(goAuthCmds) != 1 {
    77  				base.Fatalf("GOAUTH=off cannot be combined with other authentication commands (GOAUTH=%s)", cfg.GOAUTH)
    78  			}
    79  			return
    80  		case "netrc":
    81  			lines, err := readNetrc()
    82  			if err != nil {
    83  				base.Fatalf("could not parse netrc (GOAUTH=%s): %v", cfg.GOAUTH, err)
    84  			}
    85  			for _, l := range lines {
    86  				r := http.Request{Header: make(http.Header)}
    87  				r.SetBasicAuth(l.login, l.password)
    88  				storeCredential([]string{l.machine}, r.Header)
    89  			}
    90  		case "git":
    91  			if len(cmdParts) != 2 {
    92  				base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory")
    93  			}
    94  			dir := cmdParts[1]
    95  			if !filepath.IsAbs(dir) {
    96  				base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory, dir is not absolute")
    97  			}
    98  			fs, err := os.Stat(dir)
    99  			if err != nil {
   100  				base.Fatalf("GOAUTH=git encountered an error; cannot stat %s: %v", dir, err)
   101  			}
   102  			if !fs.IsDir() {
   103  				base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory, dir is not a directory")
   104  			}
   105  
   106  			if prefix == "" {
   107  				// Skip the initial GOAUTH run since we need to provide an
   108  				// explicit prefix to runGitAuth.
   109  				continue
   110  			}
   111  			prefix, header, err := runGitAuth(client, dir, prefix)
   112  			if err != nil {
   113  				// Save the error, but don't print it yet in case another
   114  				// GOAUTH command might succeed.
   115  				cmdErrs = append(cmdErrs, fmt.Errorf("GOAUTH=%s: %v", cmdStr, err))
   116  			} else {
   117  				storeCredential([]string{strings.TrimPrefix(prefix, "https://")}, header)
   118  			}
   119  		default:
   120  			base.Fatalf("unimplemented: %s", cmdStr)
   121  		}
   122  	}
   123  	// If no GOAUTH command provided a credential for the given prefix
   124  	// and an error occurred, log the error.
   125  	if cfg.BuildX && prefix != "" {
   126  		if _, ok := credentialCache.Load(prefix); !ok && len(cmdErrs) > 0 {
   127  			log.Printf("GOAUTH encountered errors for %s:", prefix)
   128  			for _, err := range cmdErrs {
   129  				log.Printf("  %v", err)
   130  			}
   131  		}
   132  	}
   133  }
   134  
   135  // loadCredential retrieves cached credentials for the given url prefix and adds
   136  // them to the request headers.
   137  func loadCredential(req *http.Request, prefix string) bool {
   138  	headers, ok := credentialCache.Load(prefix)
   139  	if !ok {
   140  		return false
   141  	}
   142  	for key, values := range headers.(http.Header) {
   143  		for _, value := range values {
   144  			req.Header.Add(key, value)
   145  		}
   146  	}
   147  	return true
   148  }
   149  
   150  // storeCredential caches or removes credentials (represented by HTTP headers)
   151  // associated with given URL prefixes.
   152  func storeCredential(prefixes []string, header http.Header) {
   153  	for _, prefix := range prefixes {
   154  		if len(header) == 0 {
   155  			credentialCache.Delete(prefix)
   156  		} else {
   157  			credentialCache.Store(prefix, header)
   158  		}
   159  	}
   160  }
   161  

View as plain text