Source file src/cmd/go/internal/auth/userauth.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/internal/quoted"
    10  	"fmt"
    11  	"maps"
    12  	"net/http"
    13  	"net/url"
    14  	"os/exec"
    15  	"strings"
    16  )
    17  
    18  // runAuthCommand executes a user provided GOAUTH command, parses its output, and
    19  // returns a mapping of prefix → http.Header.
    20  // It uses the client to verify the credential and passes the status to the
    21  // command's stdin.
    22  // res is used for the GOAUTH command's stdin.
    23  func runAuthCommand(command string, url string, res *http.Response) (map[string]http.Header, error) {
    24  	if command == "" {
    25  		panic("GOAUTH invoked an empty authenticator command:" + command) // This should be caught earlier.
    26  	}
    27  	cmd, err := buildCommand(command)
    28  	if err != nil {
    29  		return nil, err
    30  	}
    31  	if url != "" {
    32  		cmd.Args = append(cmd.Args, url)
    33  	}
    34  	cmd.Stderr = new(strings.Builder)
    35  	if res != nil && writeResponseToStdin(cmd, res) != nil {
    36  		return nil, fmt.Errorf("could not run command %s: %v\n%s", command, err, cmd.Stderr)
    37  	}
    38  	out, err := cmd.Output()
    39  	if err != nil {
    40  		return nil, fmt.Errorf("could not run command %s: %v\n%s", command, err, cmd.Stderr)
    41  	}
    42  	credentials, err := parseUserAuth(string(out))
    43  	if err != nil {
    44  		return nil, fmt.Errorf("cannot parse output of GOAUTH command %s: %v", command, err)
    45  	}
    46  	return credentials, nil
    47  }
    48  
    49  // parseUserAuth parses the output from a GOAUTH command and
    50  // returns a mapping of prefix → http.Header without the leading "https://"
    51  // or an error if the data does not follow the expected format.
    52  // Returns an nil error and an empty map if the data is empty.
    53  // See the expected format in 'go help goauth'.
    54  func parseUserAuth(data string) (map[string]http.Header, error) {
    55  	credentials := make(map[string]http.Header)
    56  	for data != "" {
    57  		var line string
    58  		var ok bool
    59  		var urls []string
    60  		// Parse URLS first.
    61  		for {
    62  			line, data, ok = strings.Cut(data, "\n")
    63  			if !ok {
    64  				return nil, fmt.Errorf("invalid format: missing empty line after URLs")
    65  			}
    66  			if line == "" {
    67  				break
    68  			}
    69  			u, err := url.ParseRequestURI(line)
    70  			if err != nil {
    71  				return nil, fmt.Errorf("could not parse URL %s: %v", line, err)
    72  			}
    73  			urls = append(urls, u.String())
    74  		}
    75  		// Parse Headers second.
    76  		header := make(http.Header)
    77  		for {
    78  			line, data, ok = strings.Cut(data, "\n")
    79  			if !ok {
    80  				return nil, fmt.Errorf("invalid format: missing empty line after headers")
    81  			}
    82  			if line == "" {
    83  				break
    84  			}
    85  			name, value, ok := strings.Cut(line, ": ")
    86  			value = strings.TrimSpace(value)
    87  			if !ok || !validHeaderFieldName(name) || !validHeaderFieldValue(value) {
    88  				return nil, fmt.Errorf("invalid format: invalid header line")
    89  			}
    90  			header.Add(name, value)
    91  		}
    92  		maps.Copy(credentials, mapHeadersToPrefixes(urls, header))
    93  	}
    94  	return credentials, nil
    95  }
    96  
    97  // mapHeadersToPrefixes returns a mapping of prefix → http.Header without
    98  // the leading "https://".
    99  func mapHeadersToPrefixes(prefixes []string, header http.Header) map[string]http.Header {
   100  	prefixToHeaders := make(map[string]http.Header, len(prefixes))
   101  	for _, p := range prefixes {
   102  		p = strings.TrimPrefix(p, "https://")
   103  		prefixToHeaders[p] = header.Clone() // Clone the header to avoid sharing
   104  	}
   105  	return prefixToHeaders
   106  }
   107  
   108  func buildCommand(command string) (*exec.Cmd, error) {
   109  	words, err := quoted.Split(command)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("cannot parse GOAUTH command %s: %v", command, err)
   112  	}
   113  	cmd := exec.Command(words[0], words[1:]...)
   114  	return cmd, nil
   115  }
   116  
   117  // writeResponseToStdin writes the HTTP response to the command's stdin.
   118  func writeResponseToStdin(cmd *exec.Cmd, res *http.Response) error {
   119  	var output strings.Builder
   120  	output.WriteString(res.Proto + " " + res.Status + "\n")
   121  	for k, v := range res.Header {
   122  		output.WriteString(k + ": " + strings.Join(v, ", ") + "\n")
   123  	}
   124  	output.WriteString("\n")
   125  	cmd.Stdin = strings.NewReader(output.String())
   126  	return nil
   127  }
   128  

View as plain text