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

View as plain text