blob: 44c0b3cff0993dd51e9fa1498e4f50c7c67f106d [file] [log] [blame] [edit]
// 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
import (
"cmd/internal/quoted"
"fmt"
"maps"
"net/http"
"net/url"
"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(string(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 a nil error and an empty map if the data is empty.
// See the expected format in 'go help goauth'.
func parseUserAuth(data string) (map[string]http.Header, error) {
credentials := make(map[string]http.Header)
for data != "" {
var line string
var ok bool
var urls []string
// Parse URLS first.
for {
line, data, ok = strings.Cut(data, "\n")
if !ok {
return nil, fmt.Errorf("invalid format: missing empty line after URLs")
}
if line == "" {
break
}
u, err := url.ParseRequestURI(line)
if err != nil {
return nil, fmt.Errorf("could not parse URL %s: %v", line, err)
}
urls = append(urls, u.String())
}
// Parse Headers second.
header := make(http.Header)
for {
line, data, ok = strings.Cut(data, "\n")
if !ok {
return nil, fmt.Errorf("invalid format: missing empty line after headers")
}
if line == "" {
break
}
name, value, ok := strings.Cut(line, ": ")
value = strings.TrimSpace(value)
if !ok || !validHeaderFieldName(name) || !validHeaderFieldValue(value) {
return nil, fmt.Errorf("invalid format: invalid header line")
}
header.Add(name, value)
}
maps.Copy(credentials, mapHeadersToPrefixes(urls, header))
}
return credentials, nil
}
// 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")
for k, v := range res.Header {
output.WriteString(k + ": " + strings.Join(v, ", ") + "\n")
}
output.WriteString("\n")
cmd.Stdin = strings.NewReader(output.String())
return nil
}