// cache build and test caching
// environment environment variables
// filetype file types
+// goauth GOAUTH environment variable
// go.mod the go.mod file
// gopath GOPATH environment variable
// goproxy module proxy protocol
// The architecture, or processor, for which to compile code.
// Examples are amd64, 386, arm, ppc64.
// GOAUTH
-// A semicolon-separated list of authentication commands for go-import and
-// HTTPS module mirror interactions. Currently supports
-// "off" (disables authentication),
-// "netrc" (uses credentials from NETRC or the .netrc file in your home directory),
-// "git dir" (runs 'git credential fill' in dir and uses its credentials).
-// The default is netrc.
+// Controls authentication for go-import and HTTPS module mirror interactions.
+// See 'go help goauth'.
// GOBIN
// The directory where 'go install' will install a command.
// GOCACHE
// line comment. See the go/build package documentation for
// more details.
//
+// # GOAUTH environment variable
+//
+// GOAUTH is a semicolon-separated list of authentication commands for go-import and
+// HTTPS module mirror interactions. The default is netrc.
+//
+// The supported authentication commands are:
+//
+// off
+//
+// Disables authentication.
+//
+// netrc
+//
+// Uses credentials from NETRC or the .netrc file in your home directory.
+//
+// git dir
+//
+// Runs 'git credential fill' in dir and uses its credentials. The
+// go command will run 'git credential approve/reject' to update
+// the credential helper's cache.
+//
+// command
+//
+// Executes the given command (a space-separated argument list) and attaches
+// the provided headers to HTTPS requests.
+// The command must produce output in the following format:
+// Response = { CredentialSet } .
+// CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine .
+// URLLine = /* URL that starts with "https://" */ '\n' .
+// HeaderLine = /* HTTP Request header */ '\n' .
+// BlankLine = '\n' .
+//
+// Example:
+// https://example.com/
+// https://example.net/api/
+//
+// Authorization: Basic <token>
+//
+// https://another-example.org/
+//
+// Example: Data
+//
+// If the server responds with any 4xx code, the go command will write the
+// following to the programs' stdin:
+// Response = StatusLine { HeaderLine } BlankLine .
+// StatusLine = Protocol Space Status '\n' .
+// Protocol = /* HTTP protocol */ .
+// Space = ' ' .
+// Status = /* HTTP status code */ .
+// BlankLine = '\n' .
+// HeaderLine = /* HTTP Response's header */ '\n' .
+//
+// Example:
+// HTTP/1.1 401 Unauthorized
+// Content-Length: 19
+// Content-Type: text/plain; charset=utf-8
+// Date: Thu, 07 Nov 2024 18:43:09 GMT
+//
+// Note: at least for HTTP 1.1, the contents written to stdin can be parsed
+// as an HTTP response.
+//
+// Before the first HTTPS fetch, the go command will invoke each GOAUTH
+// command in the list with no additional arguments and no input.
+// If the server responds with any 4xx code, the go command will invoke the
+// GOAUTH commands again with the URL as an additional command-line argument
+// and the HTTP Response to the program's stdin.
+// If the server responds with an error again, the fetch fails: a URL-specific
+// GOAUTH will only be attempted once per fetch.
+//
// # The go.mod file
//
// A module version is defined by a tree of source files, with a go.mod
// as specified by the GOAUTH environment variable.
// It returns whether any matching credentials were found.
// req must use HTTPS or this function will panic.
-func AddCredentials(client *http.Client, req *http.Request, prefix string) bool {
+// 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")
}
}
// Run all GOAUTH commands at least once.
authOnce.Do(func() {
- runGoAuth(client, "")
+ runGoAuth(client, res, "")
})
- if prefix != "" {
- // First fetch must have failed; re-invoke GOAUTH commands with prefix.
- runGoAuth(client, prefix)
+ if url != "" {
+ // First fetch must have failed; re-invoke GOAUTH commands with url.
+ runGoAuth(client, res, url)
}
- currentPrefix := strings.TrimPrefix(req.URL.String(), "https://")
- // Iteratively try prefixes, moving up the path hierarchy.
- for currentPrefix != "/" && currentPrefix != "." && currentPrefix != "" {
- if loadCredential(req, currentPrefix) {
- return true
- }
-
- // Move to the parent directory.
- currentPrefix = path.Dir(currentPrefix)
- }
- return false
+ 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, prefix string) {
+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 _, cmdStr := range goAuthCmds {
- cmdStr = strings.TrimSpace(cmdStr)
- cmdParts := strings.Fields(cmdStr)
- if len(cmdParts) == 0 {
+ for _, command := range goAuthCmds {
+ command = strings.TrimSpace(command)
+ words := strings.Fields(command)
+ if len(words) == 0 {
base.Fatalf("GOAUTH encountered an empty command (GOAUTH=%s)", cfg.GOAUTH)
}
- switch cmdParts[0] {
+ switch words[0] {
case "off":
if len(goAuthCmds) != 1 {
base.Fatalf("GOAUTH=off cannot be combined with other authentication commands (GOAUTH=%s)", cfg.GOAUTH)
for _, l := range lines {
r := http.Request{Header: make(http.Header)}
r.SetBasicAuth(l.login, l.password)
- storeCredential([]string{l.machine}, r.Header)
+ storeCredential(l.machine, r.Header)
}
case "git":
- if len(cmdParts) != 2 {
+ if len(words) != 2 {
base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory")
}
- dir := cmdParts[1]
+ dir := words[1]
if !filepath.IsAbs(dir) {
base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory, dir is not absolute")
}
base.Fatalf("GOAUTH=git dir method requires an absolute path to the git working directory, dir is not a directory")
}
- if prefix == "" {
+ if url == "" {
// Skip the initial GOAUTH run since we need to provide an
- // explicit prefix to runGitAuth.
+ // explicit url to runGitAuth.
continue
}
- prefix, header, err := runGitAuth(client, dir, prefix)
+ 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", cmdStr, err))
+ cmdErrs = append(cmdErrs, fmt.Errorf("GOAUTH=%s: %v", command, err))
} else {
- storeCredential([]string{strings.TrimPrefix(prefix, "https://")}, header)
+ storeCredential(prefix, header)
}
default:
- base.Fatalf("unimplemented: %s", cmdStr)
+ 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 prefix
+ // If no GOAUTH command provided a credential for the given url
// and an error occurred, log the error.
- if cfg.BuildX && prefix != "" {
- if _, ok := credentialCache.Load(prefix); !ok && len(cmdErrs) > 0 {
- log.Printf("GOAUTH encountered errors for %s:", prefix)
+ if cfg.BuildX && url != "" {
+ if ok := loadCredential(&http.Request{}, 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 prefix and adds
+// loadCredential retrieves cached credentials for the given url and adds
// them to the request headers.
-func loadCredential(req *http.Request, prefix string) bool {
- headers, ok := credentialCache.Load(prefix)
- if !ok {
- return false
- }
- for key, values := range headers.(http.Header) {
- for _, value := range values {
- req.Header.Add(key, value)
+func loadCredential(req *http.Request, url string) bool {
+ currentPrefix := strings.TrimPrefix(url, "https://")
+ // Iteratively try prefixes, moving up the path hierarchy.
+ for currentPrefix != "/" && currentPrefix != "." && currentPrefix != "" {
+ headers, ok := credentialCache.Load(currentPrefix)
+ if !ok {
+ // Move to the parent directory.
+ currentPrefix = path.Dir(currentPrefix)
+ continue
+ }
+ for key, values := range headers.(http.Header) {
+ for _, value := range values {
+ req.Header.Add(key, value)
+ }
}
+ return true
}
- return true
+ return false
}
// storeCredential caches or removes credentials (represented by HTTP headers)
// associated with given URL prefixes.
-func storeCredential(prefixes []string, header http.Header) {
- for _, prefix := range prefixes {
- if len(header) == 0 {
- credentialCache.Delete(prefix)
- } else {
- credentialCache.Store(prefix, header)
- }
+func storeCredential(prefix string, header http.Header) {
+ // Trim "https://" prefix to match the format used in .netrc files.
+ prefix = strings.TrimPrefix(prefix, "https://")
+ if len(header) == 0 {
+ credentialCache.Delete(prefix)
+ } else {
+ credentialCache.Store(prefix, header)
}
}
for _, tc := range testCases {
want := http.Request{Header: make(http.Header)}
want.SetBasicAuth(tc.login, tc.password)
- storeCredential([]string{tc.machine}, want.Header)
+ storeCredential(tc.machine, want.Header)
got := &http.Request{Header: make(http.Header)}
ok := loadCredential(got, tc.machine)
if !ok || !reflect.DeepEqual(got.Header, want.Header) {
// Store a credential for api.github.com
want := http.Request{Header: make(http.Header)}
want.SetBasicAuth("user", "pwd")
- storeCredential([]string{"api.github.com"}, want.Header)
+ storeCredential("api.github.com", want.Header)
got := &http.Request{Header: make(http.Header)}
ok := loadCredential(got, "api.github.com")
if !ok || !reflect.DeepEqual(got.Header, want.Header) {
}
// Providing an empty header for api.github.com should clear credentials.
want = http.Request{Header: make(http.Header)}
- storeCredential([]string{"api.github.com"}, want.Header)
+ storeCredential("api.github.com", want.Header)
got = &http.Request{Header: make(http.Header)}
ok = loadCredential(got, "api.github.com")
if ok {
const maxTries = 3
-// runGitAuth retrieves credentials for the given prefix using
+// runGitAuth retrieves credentials for the given url using
// 'git credential fill', validates them with a HEAD request
// (using the provided client) and updates the credential helper's cache.
// It returns the matching credential prefix, the http.Header with the
// Basic Authentication header set, or an error.
// The caller must not mutate the header.
-func runGitAuth(client *http.Client, dir, prefix string) (string, http.Header, error) {
- if prefix == "" {
- // No explicit prefix was passed, but 'git credential'
+func runGitAuth(client *http.Client, dir, url string) (string, http.Header, error) {
+ if url == "" {
+ // No explicit url was passed, but 'git credential'
// provides no way to enumerate existing credentials.
- // Wait for a request for a specific prefix.
- return "", nil, fmt.Errorf("no explicit prefix was passed")
+ // Wait for a request for a specific url.
+ return "", nil, fmt.Errorf("no explicit url was passed")
}
if dir == "" {
// Prevent config-injection attacks by requiring an explicit working directory.
}
cmd := exec.Command("git", "credential", "fill")
cmd.Dir = dir
- cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", prefix))
+ cmd.Stdin = strings.NewReader(fmt.Sprintf("url=%s\n", url))
out, err := cmd.CombinedOutput()
if err != nil {
- return "", nil, fmt.Errorf("'git credential fill' failed (url=%s): %w\n%s", prefix, err, out)
+ return "", nil, fmt.Errorf("'git credential fill' failed (url=%s): %w\n%s", url, err, out)
}
parsedPrefix, username, password := parseGitAuth(out)
if parsedPrefix == "" {
- return "", nil, fmt.Errorf("'git credential fill' failed for url=%s, could not parse url\n", prefix)
+ return "", nil, fmt.Errorf("'git credential fill' failed for url=%s, could not parse url\n", url)
}
// Check that the URL Git gave us is a prefix of the one we requested.
- if !strings.HasPrefix(prefix, parsedPrefix) {
- return "", nil, fmt.Errorf("requested a credential for %s, but 'git credential fill' provided one for %s\n", prefix, parsedPrefix)
+ if !strings.HasPrefix(url, parsedPrefix) {
+ return "", nil, fmt.Errorf("requested a credential for %s, but 'git credential fill' provided one for %s\n", url, parsedPrefix)
}
req, err := http.NewRequest("HEAD", parsedPrefix, nil)
if err != nil {
// The request is intercepted for testing purposes to simulate interactions
// with the credential helper.
intercept.Request(req)
- go updateCredentialHelper(client, req, out)
+ go updateGitCredentialHelper(client, req, out)
// Return the parsed prefix and headers, even if credential validation fails.
// The caller is responsible for the primary validation.
return prefix.String(), username, password
}
-// updateCredentialHelper validates the given credentials by sending a HEAD request
+// updateGitCredentialHelper validates the given credentials by sending a HEAD request
// and updates the git credential helper's cache accordingly. It retries the
// request up to maxTries times.
-func updateCredentialHelper(client *http.Client, req *http.Request, credentialOutput []byte) {
+func updateGitCredentialHelper(client *http.Client, req *http.Request, credentialOutput []byte) {
for range maxTries {
release, err := base.AcquireNet()
if err != nil {
--- /dev/null
+// 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 (
+ "bufio"
+ "bytes"
+ "cmd/internal/quoted"
+ "fmt"
+ "io"
+ "maps"
+ "net/http"
+ "net/textproto"
+ "os/exec"
+ "strings"
+)
+
+// runAuthCommand executes a user provided GOAUTH command, parses its output, and
+// returns a mapping of prefix → http.Header.
+// It uses the client to verify the credential and passes the status to the
+// command's stdin.
+// res is used for the GOAUTH command's stdin.
+func runAuthCommand(command string, url string, res *http.Response) (map[string]http.Header, error) {
+ if command == "" {
+ panic("GOAUTH invoked an empty authenticator command:" + command) // This should be caught earlier.
+ }
+ cmd, err := buildCommand(command)
+ if err != nil {
+ return nil, err
+ }
+ if url != "" {
+ cmd.Args = append(cmd.Args, url)
+ }
+ cmd.Stderr = new(strings.Builder)
+ if res != nil && writeResponseToStdin(cmd, res) != nil {
+ return nil, fmt.Errorf("could not run command %s: %v\n%s", command, err, cmd.Stderr)
+ }
+ out, err := cmd.Output()
+ if err != nil {
+ return nil, fmt.Errorf("could not run command %s: %v\n%s", command, err, cmd.Stderr)
+ }
+ credentials, err := parseUserAuth(bytes.NewReader(out))
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse output of GOAUTH command %s: %v", command, err)
+ }
+ return credentials, nil
+}
+
+// parseUserAuth parses the output from a GOAUTH command and
+// returns a mapping of prefix → http.Header without the leading "https://"
+// or an error if the data does not follow the expected format.
+// Returns an nil error and an empty map if the data is empty.
+// See the expected format in 'go help goauth'.
+func parseUserAuth(data io.Reader) (map[string]http.Header, error) {
+ credentials := make(map[string]http.Header)
+ reader := textproto.NewReader(bufio.NewReader(data))
+ for {
+ // Return the processed credentials if the reader is at EOF.
+ if _, err := reader.R.Peek(1); err == io.EOF {
+ return credentials, nil
+ }
+ urls, err := readURLs(reader)
+ if err != nil {
+ return nil, err
+ }
+ if len(urls) == 0 {
+ return nil, fmt.Errorf("invalid format: expected url prefix")
+ }
+ mimeHeader, err := reader.ReadMIMEHeader()
+ if err != nil {
+ return nil, err
+ }
+ header := http.Header(mimeHeader)
+ // Process the block (urls and headers).
+ credentialMap := mapHeadersToPrefixes(urls, header)
+ maps.Copy(credentials, credentialMap)
+ }
+}
+
+// readURLs reads URL prefixes from the given reader until an empty line
+// is encountered or an error occurs. It returns the list of URLs or an error
+// if the format is invalid.
+func readURLs(reader *textproto.Reader) (urls []string, err error) {
+ for {
+ line, err := reader.ReadLine()
+ if err != nil {
+ return nil, err
+ }
+ trimmedLine := strings.TrimSpace(line)
+ if trimmedLine != line {
+ return nil, fmt.Errorf("invalid format: leading or trailing white space")
+ }
+ if strings.HasPrefix(line, "https://") {
+ urls = append(urls, line)
+ } else if line == "" {
+ return urls, nil
+ } else {
+ return nil, fmt.Errorf("invalid format: expected url prefix or empty line")
+ }
+ }
+}
+
+// mapHeadersToPrefixes returns a mapping of prefix → http.Header without
+// the leading "https://".
+func mapHeadersToPrefixes(prefixes []string, header http.Header) map[string]http.Header {
+ prefixToHeaders := make(map[string]http.Header, len(prefixes))
+ for _, p := range prefixes {
+ p = strings.TrimPrefix(p, "https://")
+ prefixToHeaders[p] = header.Clone() // Clone the header to avoid sharing
+ }
+ return prefixToHeaders
+}
+
+func buildCommand(command string) (*exec.Cmd, error) {
+ words, err := quoted.Split(command)
+ if err != nil {
+ return nil, fmt.Errorf("cannot parse GOAUTH command %s: %v", command, err)
+ }
+ cmd := exec.Command(words[0], words[1:]...)
+ return cmd, nil
+}
+
+// writeResponseToStdin writes the HTTP response to the command's stdin.
+func writeResponseToStdin(cmd *exec.Cmd, res *http.Response) error {
+ var output strings.Builder
+ output.WriteString(res.Proto + " " + res.Status + "\n")
+ if err := res.Header.Write(&output); err != nil {
+ return err
+ }
+ output.WriteString("\n")
+ cmd.Stdin = strings.NewReader(output.String())
+ return nil
+}
--- /dev/null
+// Copyright 2018 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
+
+import (
+ "net/http"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestParseUserAuth(t *testing.T) {
+ data := `https://example.com
+
+Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+Authorization: Basic jpvcGVuc2VzYW1lYWxhZGRpb
+
+https://hello.com
+
+Authorization: Basic GVuc2VzYW1lYWxhZGRpbjpvc
+Authorization: Basic 1lYWxhZGRplW1lYWxhZGRpbs
+Data: Test567
+
+`
+ // Build the expected header
+ header1 := http.Header{
+ "Authorization": []string{
+ "Basic YWxhZGRpbjpvcGVuc2VzYW1l",
+ "Basic jpvcGVuc2VzYW1lYWxhZGRpb",
+ },
+ }
+ header2 := http.Header{
+ "Authorization": []string{
+ "Basic GVuc2VzYW1lYWxhZGRpbjpvc",
+ "Basic 1lYWxhZGRplW1lYWxhZGRpbs",
+ },
+ "Data": []string{
+ "Test567",
+ },
+ }
+ credentials, err := parseUserAuth(strings.NewReader(data))
+ if err != nil {
+ t.Errorf("parseUserAuth(%s): %v", data, err)
+ }
+ gotHeader, ok := credentials["example.com"]
+ if !ok || !reflect.DeepEqual(gotHeader, header1) {
+ t.Errorf("parseUserAuth(%s):\nhave %q\nwant %q", data, gotHeader, header1)
+ }
+ gotHeader, ok = credentials["hello.com"]
+ if !ok || !reflect.DeepEqual(gotHeader, header2) {
+ t.Errorf("parseUserAuth(%s):\nhave %q\nwant %q", data, gotHeader, header2)
+ }
+}
+
+func TestParseUserAuthInvalid(t *testing.T) {
+ testCases := []string{
+ // Missing new line after url.
+ `https://example.com
+Authorization: Basic AVuc2VzYW1lYWxhZGRpbjpvc
+
+`,
+ // Missing url.
+ `Authorization: Basic AVuc2VzYW1lYWxhZGRpbjpvc
+
+`,
+ // Missing url.
+ `https://example.com
+
+Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+Authorization: Basic jpvcGVuc2VzYW1lYWxhZGRpb
+
+Authorization: Basic GVuc2VzYW1lYWxhZGRpbjpvc
+Authorization: Basic 1lYWxhZGRplW1lYWxhZGRpbs
+Data: Test567
+
+`,
+ // Wrong order.
+ `Authorization: Basic AVuc2VzYW1lYWxhZGRpbjpvc
+
+https://example.com
+
+`,
+ // Missing new lines after URL.
+ `https://example.com
+`,
+ // Missing new line after empty header.
+ `https://example.com
+
+`,
+ // Missing new line between blocks.
+ `https://example.com
+
+Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+Authorization: Basic jpvcGVuc2VzYW1lYWxhZGRpb
+https://hello.com
+
+Authorization: Basic GVuc2VzYW1lYWxhZGRpbjpvc
+Authorization: Basic 1lYWxhZGRplW1lYWxhZGRpbs
+Data: Test567
+
+`,
+ }
+ for _, tc := range testCases {
+ if credentials, err := parseUserAuth(strings.NewReader(tc)); err == nil {
+ t.Errorf("parseUserAuth(%s) should have failed, but got: %v", tc, credentials)
+ }
+ }
+}
+
+func TestParseUserAuthDuplicated(t *testing.T) {
+ data := `https://example.com
+
+Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+Authorization: Basic jpvcGVuc2VzYW1lYWxhZGRpb
+
+https://example.com
+
+Authorization: Basic GVuc2VzYW1lYWxhZGRpbjpvc
+Authorization: Basic 1lYWxhZGRplW1lYWxhZGRpbs
+Data: Test567
+
+`
+ // Build the expected header
+ header := http.Header{
+ "Authorization": []string{
+ "Basic GVuc2VzYW1lYWxhZGRpbjpvc",
+ "Basic 1lYWxhZGRplW1lYWxhZGRpbs",
+ },
+ "Data": []string{
+ "Test567",
+ },
+ }
+ credentials, err := parseUserAuth(strings.NewReader(data))
+ if err != nil {
+ t.Errorf("parseUserAuth(%s): %v", data, err)
+ }
+ gotHeader, ok := credentials["example.com"]
+ if !ok || !reflect.DeepEqual(gotHeader, header) {
+ t.Errorf("parseUserAuth(%s):\nhave %q\nwant %q", data, gotHeader, header)
+ }
+}
+
+func TestParseUserAuthEmptyHeader(t *testing.T) {
+ data := "https://example.com\n\n\n"
+ // Build the expected header
+ header := http.Header{}
+ credentials, err := parseUserAuth(strings.NewReader(data))
+ if err != nil {
+ t.Errorf("parseUserAuth(%s): %v", data, err)
+ }
+ gotHeader, ok := credentials["example.com"]
+ if !ok || !reflect.DeepEqual(gotHeader, header) {
+ t.Errorf("parseUserAuth(%s):\nhave %q\nwant %q", data, gotHeader, header)
+ }
+}
+
+func TestParseUserAuthEmpty(t *testing.T) {
+ data := ``
+ // Build the expected header
+ credentials, err := parseUserAuth(strings.NewReader(data))
+ if err != nil {
+ t.Errorf("parseUserAuth(%s) should have succeeded", data)
+ }
+ if credentials == nil {
+ t.Errorf("parseUserAuth(%s) should have returned a non-nil credential map, but got %v", data, credentials)
+ }
+}
The architecture, or processor, for which to compile code.
Examples are amd64, 386, arm, ppc64.
GOAUTH
- A semicolon-separated list of authentication commands for go-import and
- HTTPS module mirror interactions. Currently supports
- "off" (disables authentication),
- "netrc" (uses credentials from NETRC or the .netrc file in your home directory),
- "git dir" (runs 'git credential fill' in dir and uses its credentials).
- The default is netrc.
+ Controls authentication for go-import and HTTPS module mirror interactions.
+ See 'go help goauth'.
GOBIN
The directory where 'go install' will install a command.
GOCACHE
the file will be the minimum version implied by the build constraint.
`,
}
+
+var HelpGoAuth = &base.Command{
+ UsageLine: "goauth",
+ Short: "GOAUTH environment variable",
+ Long: `
+GOAUTH is a semicolon-separated list of authentication commands for go-import and
+HTTPS module mirror interactions. The default is netrc.
+
+The supported authentication commands are:
+
+off
+ Disables authentication.
+netrc
+ Uses credentials from NETRC or the .netrc file in your home directory.
+git dir
+ Runs 'git credential fill' in dir and uses its credentials. The
+ go command will run 'git credential approve/reject' to update
+ the credential helper's cache.
+command
+ Executes the given command (a space-separated argument list) and attaches
+ the provided headers to HTTPS requests.
+ The command must produce output in the following format:
+ Response = { CredentialSet } .
+ CredentialSet = URLLine { URLLine } BlankLine { HeaderLine } BlankLine .
+ URLLine = /* URL that starts with "https://" */ '\n' .
+ HeaderLine = /* HTTP Request header */ '\n' .
+ BlankLine = '\n' .
+
+ Example:
+ https://example.com/
+ https://example.net/api/
+
+ Authorization: Basic <token>
+
+ https://another-example.org/
+
+ Example: Data
+
+ If the server responds with any 4xx code, the go command will write the
+ following to the programs' stdin:
+ Response = StatusLine { HeaderLine } BlankLine .
+ StatusLine = Protocol Space Status '\n' .
+ Protocol = /* HTTP protocol */ .
+ Space = ' ' .
+ Status = /* HTTP status code */ .
+ BlankLine = '\n' .
+ HeaderLine = /* HTTP Response's header */ '\n' .
+
+ Example:
+ HTTP/1.1 401 Unauthorized
+ Content-Length: 19
+ Content-Type: text/plain; charset=utf-8
+ Date: Thu, 07 Nov 2024 18:43:09 GMT
+
+ Note: at least for HTTP 1.1, the contents written to stdin can be parsed
+ as an HTTP response.
+
+Before the first HTTPS fetch, the go command will invoke each GOAUTH
+command in the list with no additional arguments and no input.
+If the server responds with any 4xx code, the go command will invoke the
+GOAUTH commands again with the URL as an additional command-line argument
+and the HTTP Response to the program's stdin.
+If the server responds with an error again, the fetch fails: a URL-specific
+GOAUTH will only be attempted once per fetch.
+`,
+}
}
if url.Scheme == "https" {
// Use initial GOAUTH credentials.
- auth.AddCredentials(client, req, "")
+ auth.AddCredentials(client, req, nil, "")
}
if intercepted {
req.Host = req.URL.Host
if err != nil {
return nil, err
}
- auth.AddCredentials(client, req, url.String())
+ auth.AddCredentials(client, req, res, url.String())
intercept.Request(req)
res, err = client.Do(req)
}
help.HelpCache,
help.HelpEnvironment,
help.HelpFileType,
+ help.HelpGoAuth,
modload.HelpGoMod,
help.HelpGopath,
modfetch.HelpGoproxy,
--- /dev/null
+# This test covers the HTTP authentication mechanism over GOAUTH by using a custom authenticator.
+# See golang.org/issue/26232
+
+env GOPROXY=direct
+env GOSUMDB=off
+
+# Use a custom authenticator to provide custom credentials
+mkdir $WORK/bin
+env PATH=$WORK/bin${:}$PATH
+cd auth
+go build -o $WORK/bin/my-auth$GOEXE .
+cd ..
+
+# Without credentials, downloading a module from a path that requires HTTPS
+# basic auth should fail.
+env GOAUTH=off
+cp go.mod.orig go.mod
+! go get vcs-test.golang.org/auth/or401
+stderr '^\tserver response: ACCESS DENIED, buddy$'
+# go imports should fail as well.
+! go mod tidy
+stderr '^\tserver response: ACCESS DENIED, buddy$'
+
+# With credentials from the my-auth binary, it should succeed.
+env GOAUTH='my-auth'$GOEXE' --arg1 "value with spaces"'
+cp go.mod.orig go.mod
+go get vcs-test.golang.org/auth/or401
+# go imports should resolve correctly as well.
+go mod tidy
+go list all
+stdout vcs-test.golang.org/auth/or401
+
+-- auth/main.go --
+package main
+
+import(
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+)
+
+func main() {
+ arg1 := flag.String("arg1", "", "")
+ flag.Parse()
+ if *arg1 != "value with spaces" {
+ log.Fatal("argument with spaces does not work")
+ }
+ // wait for re-invocation
+ if !strings.HasPrefix(flag.Arg(0), "https://vcs-test.golang.org") {
+ return
+ }
+ input, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ log.Fatal("unexpected error while reading from stdin")
+ }
+ reader := bufio.NewReader(strings.NewReader(string(input)))
+ resp, err := http.ReadResponse(reader, nil)
+ if err != nil {
+ log.Fatal("could not parse HTTP response")
+ }
+ if resp.StatusCode != 401 {
+ log.Fatal("expected 401 error code")
+ }
+ fmt.Printf("https://vcs-test.golang.org\n\nAuthorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l\n\n")
+}
+
+-- auth/go.mod --
+module my-auth
+-- go.mod.orig --
+module private.example.com
+-- main.go --
+package useprivate
+
+import "vcs-test.golang.org/auth/or401"