| // Copyright 2019 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package auth provides access to user-provided authentication credentials. |
| package auth |
| |
| import ( |
| "cmd/go/internal/base" |
| "cmd/go/internal/cfg" |
| "fmt" |
| "log" |
| "net/http" |
| "os" |
| "path/filepath" |
| "slices" |
| "strings" |
| "sync" |
| ) |
| |
| var ( |
| credentialCache sync.Map // prefix → http.Header |
| authOnce sync.Once |
| ) |
| |
| // AddCredentials populates the request header with the user's credentials |
| // as specified by the GOAUTH environment variable. |
| // It returns whether any matching credentials were found. |
| // req must use HTTPS or this function will panic. |
| // res is used for the custom GOAUTH command's stdin. |
| func AddCredentials(client *http.Client, req *http.Request, res *http.Response, url string) bool { |
| if req.URL.Scheme != "https" { |
| panic("GOAUTH called without https") |
| } |
| if cfg.GOAUTH == "off" { |
| return false |
| } |
| // Run all GOAUTH commands at least once. |
| authOnce.Do(func() { |
| runGoAuth(client, res, "") |
| }) |
| if url != "" { |
| // First fetch must have failed; re-invoke GOAUTH commands with url. |
| runGoAuth(client, res, url) |
| } |
| return loadCredential(req, req.URL.String()) |
| } |
| |
| // runGoAuth executes authentication commands specified by the GOAUTH |
| // environment variable handling 'off', 'netrc', and 'git' methods specially, |
| // and storing retrieved credentials for future access. |
| func runGoAuth(client *http.Client, res *http.Response, url string) { |
| var cmdErrs []error // store GOAUTH command errors to log later. |
| goAuthCmds := strings.Split(cfg.GOAUTH, ";") |
| // The GOAUTH commands are processed in reverse order to prioritize |
| // credentials in the order they were specified. |
| slices.Reverse(goAuthCmds) |
| for _, command := range goAuthCmds { |
| command = strings.TrimSpace(command) |
| words := strings.Fields(command) |
| if len(words) == 0 { |
| base.Fatalf("go: GOAUTH encountered an empty command (GOAUTH=%s)", cfg.GOAUTH) |
| } |
| switch words[0] { |
| case "off": |
| if len(goAuthCmds) != 1 { |
| base.Fatalf("go: GOAUTH=off cannot be combined with other authentication commands (GOAUTH=%s)", cfg.GOAUTH) |
| } |
| return |
| case "netrc": |
| lines, err := readNetrc() |
| if err != nil { |
| cmdErrs = append(cmdErrs, fmt.Errorf("GOAUTH=%s: %v", command, err)) |
| continue |
| } |
| // Process lines in reverse so that if the same machine is listed |
| // multiple times, we end up saving the earlier one |
| // (overwriting later ones). This matches the way the go command |
| // worked before GOAUTH. |
| for i := len(lines) - 1; i >= 0; i-- { |
| l := lines[i] |
| r := http.Request{Header: make(http.Header)} |
| r.SetBasicAuth(l.login, l.password) |
| storeCredential(l.machine, r.Header) |
| } |
| case "git": |
| if len(words) != 2 { |
| base.Fatalf("go: GOAUTH=git dir method requires an absolute path to the git working directory") |
| } |
| dir := words[1] |
| if !filepath.IsAbs(dir) { |
| base.Fatalf("go: GOAUTH=git dir method requires an absolute path to the git working directory, dir is not absolute") |
| } |
| fs, err := os.Stat(dir) |
| if err != nil { |
| base.Fatalf("go: GOAUTH=git encountered an error; cannot stat %s: %v", dir, err) |
| } |
| if !fs.IsDir() { |
| base.Fatalf("go: GOAUTH=git dir method requires an absolute path to the git working directory, dir is not a directory") |
| } |
| |
| if url == "" { |
| // Skip the initial GOAUTH run since we need to provide an |
| // explicit url to runGitAuth. |
| continue |
| } |
| prefix, header, err := runGitAuth(client, dir, url) |
| if err != nil { |
| // Save the error, but don't print it yet in case another |
| // GOAUTH command might succeed. |
| cmdErrs = append(cmdErrs, fmt.Errorf("GOAUTH=%s: %v", command, err)) |
| } else { |
| storeCredential(prefix, header) |
| } |
| default: |
| credentials, err := runAuthCommand(command, url, res) |
| if err != nil { |
| // Save the error, but don't print it yet in case another |
| // GOAUTH command might succeed. |
| cmdErrs = append(cmdErrs, fmt.Errorf("GOAUTH=%s: %v", command, err)) |
| continue |
| } |
| for prefix := range credentials { |
| storeCredential(prefix, credentials[prefix]) |
| } |
| } |
| } |
| // If no GOAUTH command provided a credential for the given url |
| // and an error occurred, log the error. |
| if cfg.BuildX && url != "" { |
| req := &http.Request{Header: make(http.Header)} |
| if ok := loadCredential(req, url); !ok && len(cmdErrs) > 0 { |
| log.Printf("GOAUTH encountered errors for %s:", url) |
| for _, err := range cmdErrs { |
| log.Printf(" %v", err) |
| } |
| } |
| } |
| } |
| |
| // loadCredential retrieves cached credentials for the given url and adds |
| // them to the request headers. |
| func loadCredential(req *http.Request, url string) bool { |
| currentPrefix := strings.TrimPrefix(url, "https://") |
| currentPrefix = strings.TrimSuffix(currentPrefix, "/") |
| |
| // Iteratively try prefixes, moving up the path hierarchy. |
| // E.g. example.com/foo/bar, example.com/foo, example.com |
| for { |
| headers, ok := credentialCache.Load(currentPrefix) |
| if !ok { |
| lastSlash := strings.LastIndexByte(currentPrefix, '/') |
| if lastSlash == -1 { |
| return false |
| } |
| currentPrefix = currentPrefix[:lastSlash] |
| continue |
| } |
| for key, values := range headers.(http.Header) { |
| for _, value := range values { |
| req.Header.Add(key, value) |
| } |
| } |
| return true |
| } |
| } |
| |
| // storeCredential caches or removes credentials (represented by HTTP headers) |
| // associated with given URL prefixes. |
| func storeCredential(prefix string, header http.Header) { |
| // Trim "https://" prefix to match the format used in .netrc files. |
| prefix = strings.TrimPrefix(prefix, "https://") |
| prefix = strings.TrimSuffix(prefix, "/") |
| if len(header) == 0 { |
| credentialCache.Delete(prefix) |
| } else { |
| credentialCache.Store(prefix, header) |
| } |
| } |