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

View as plain text