| // 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 |
| } |