mirror of
https://github.com/go-task/task.git
synced 2026-09-02 04:02:08 +02:00
RemoteExists treated every non-200 as an absent file, so a server refusing the credentials ended up as "No Taskfile found", sending the user to check the URL rather than the token. A 401 now stops the search and reports the status code; the default names need the same credentials, so trying them would only add rejected requests. A 403 is left alone: it is also what a server without directory listing answers for a readable directory. That message being correct, the expansion no longer needs to refuse an undefined variable: os.ExpandEnv is inlined and expandEnv is gone. The `$$` escape goes with it, so a literal value can no longer hold a `$` followed by a name; a secret carried in an environment variable is unaffected, as os.Expand never rescans what it substituted. Header names are validated with httpguts.ValidHeaderFieldName, the table net/http itself uses, rather than a denylist that let X-Foo(bar) through. golang.org/x/net was already in the module graph, so tidy only moves it to the direct block. Finally, node_http_auth.go becomes http_auth.go: the node_ prefix is for files defining a Node type, and this one holds the auth concern of HTTPNode plus hostMatches, which reader.go uses for trusted hosts.
98 lines
2.7 KiB
Go
98 lines
2.7 KiB
Go
package taskfile
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"maps"
|
|
"net/http"
|
|
"os"
|
|
"slices"
|
|
|
|
"golang.org/x/net/http/httpguts"
|
|
)
|
|
|
|
// HostHeaders maps a host to the HTTP headers to send when fetching a remote
|
|
// Taskfile from it. Values may reference environment variables.
|
|
type HostHeaders map[string]map[string]string
|
|
|
|
type authTransport struct {
|
|
base http.RoundTripper
|
|
host string
|
|
headers map[string]string
|
|
}
|
|
|
|
func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// Re-checked per request: a redirect goes through this same transport, and
|
|
// Go only strips Authorization, WWW-Authenticate and Cookie on its own.
|
|
if !hostMatches(t.host, req.URL.Host) {
|
|
return t.base.RoundTrip(req)
|
|
}
|
|
req = req.Clone(req.Context())
|
|
for name, value := range t.headers {
|
|
req.Header.Set(name, value)
|
|
}
|
|
return t.base.RoundTrip(req)
|
|
}
|
|
|
|
// authenticatedClient resolves on each read, not at build time, so a cached
|
|
// run needs no credentials.
|
|
func (node *HTTPNode) authenticatedClient() (*http.Client, error) {
|
|
headers, err := resolveAuthHeaders(node.authHeaders, node.url.Host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(headers) == 0 {
|
|
return node.client, nil
|
|
}
|
|
return withAuthHeaders(node.client, node.url.Host, headers), nil
|
|
}
|
|
|
|
// withAuthHeaders copies rather than mutates: buildHTTPClient returns the
|
|
// shared http.DefaultClient when no TLS option is set.
|
|
func withAuthHeaders(client *http.Client, host string, headers map[string]string) *http.Client {
|
|
authenticated := *client
|
|
authenticated.Transport = &authTransport{
|
|
base: cmp.Or(client.Transport, http.DefaultTransport),
|
|
host: host,
|
|
headers: headers,
|
|
}
|
|
return &authenticated
|
|
}
|
|
|
|
// resolveAuthHeaders returns the expanded headers for host, or nil if none.
|
|
func resolveAuthHeaders(hostHeaders HostHeaders, host string) (map[string]string, error) {
|
|
var headers map[string]string
|
|
for pattern, patternHeaders := range hostHeaders {
|
|
if hostMatches(pattern, host) {
|
|
headers = patternHeaders
|
|
break
|
|
}
|
|
}
|
|
if len(headers) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
resolved := make(map[string]string, len(headers))
|
|
for _, name := range slices.Sorted(maps.Keys(headers)) {
|
|
if err := validateHeaderName(name); err != nil {
|
|
return nil, fmt.Errorf(`remote auth for host %q: %w`, host, err)
|
|
}
|
|
resolved[name] = os.ExpandEnv(headers[name])
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
// validateHeaderName names the offending header; ReadContext discards the
|
|
// transport's own error.
|
|
func validateHeaderName(name string) error {
|
|
if !httpguts.ValidHeaderFieldName(name) {
|
|
return fmt.Errorf("invalid header name %q", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// hostMatches compares exactly, port included, as trusted hosts do.
|
|
func hostMatches(pattern, host string) bool {
|
|
return pattern == host
|
|
}
|