2024-01-04 10:12:16 +00:00
|
|
|
package taskfile
|
|
|
|
|
|
|
|
|
|
import (
|
2024-03-25 19:05:21 +00:00
|
|
|
"context"
|
2025-04-19 12:12:08 +01:00
|
|
|
"fmt"
|
2024-02-13 01:07:00 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/url"
|
|
|
|
|
"slices"
|
|
|
|
|
"strings"
|
2024-01-04 10:12:16 +00:00
|
|
|
|
|
|
|
|
"github.com/go-task/task/v3/errors"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
2025-12-07 17:29:51 -03:00
|
|
|
// DefaultTaskfiles is the list of Taskfile file names supported by default.
|
|
|
|
|
DefaultTaskfiles = []string{
|
2024-01-04 10:12:16 +00:00
|
|
|
"Taskfile.yml",
|
|
|
|
|
"taskfile.yml",
|
|
|
|
|
"Taskfile.yaml",
|
|
|
|
|
"taskfile.yaml",
|
|
|
|
|
"Taskfile.dist.yml",
|
|
|
|
|
"taskfile.dist.yml",
|
|
|
|
|
"Taskfile.dist.yaml",
|
|
|
|
|
"taskfile.dist.yaml",
|
|
|
|
|
}
|
2024-02-13 01:07:00 +00:00
|
|
|
allowedContentTypes = []string{
|
|
|
|
|
"text/plain",
|
|
|
|
|
"text/yaml",
|
|
|
|
|
"text/x-yaml",
|
|
|
|
|
"application/yaml",
|
|
|
|
|
"application/x-yaml",
|
2025-12-02 20:36:35 +01:00
|
|
|
"application/octet-stream",
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|
2024-01-04 10:12:16 +00:00
|
|
|
)
|
|
|
|
|
|
2024-02-13 01:07:00 +00:00
|
|
|
// RemoteExists will check if a file at the given URL Exists. If it does, it
|
|
|
|
|
// will return its URL. If it does not, it will search the search for any files
|
|
|
|
|
// at the given URL with any of the default Taskfile files names. If any of
|
|
|
|
|
// these match a file, the first matching path will be returned. If no files are
|
|
|
|
|
// found, an error will be returned.
|
2026-01-25 18:51:30 +01:00
|
|
|
func RemoteExists(ctx context.Context, u url.URL, client *http.Client) (*url.URL, error) {
|
2024-02-13 01:07:00 +00:00
|
|
|
// Create a new HEAD request for the given URL to check if the resource exists
|
2024-12-12 01:42:04 +01:00
|
|
|
req, err := http.NewRequestWithContext(ctx, "HEAD", u.String(), nil)
|
2024-02-13 01:07:00 +00:00
|
|
|
if err != nil {
|
2025-05-24 13:38:18 +01:00
|
|
|
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Request the given URL
|
2026-01-25 18:51:30 +01:00
|
|
|
resp, err := client.Do(req)
|
2024-02-13 01:07:00 +00:00
|
|
|
if err != nil {
|
2025-04-19 12:12:08 +01:00
|
|
|
if ctx.Err() != nil {
|
|
|
|
|
return nil, fmt.Errorf("checking remote file: %w", ctx.Err())
|
2024-09-07 21:54:05 +02:00
|
|
|
}
|
2025-05-24 13:38:18 +01:00
|
|
|
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
// If the request was successful and the content type is allowed, return the
|
|
|
|
|
// URL The content type check is to avoid downloading files that are not
|
|
|
|
|
// Taskfiles It means we can try other files instead of downloading
|
|
|
|
|
// something that is definitely not a Taskfile
|
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
|
|
|
if resp.StatusCode == http.StatusOK && slices.ContainsFunc(allowedContentTypes, func(s string) bool {
|
|
|
|
|
return strings.Contains(contentType, s)
|
|
|
|
|
}) {
|
2025-05-24 13:38:18 +01:00
|
|
|
return &u, nil
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|
|
|
|
|
|
fix(remote): report a 401 instead of a missing Taskfile
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.
2026-08-20 16:50:43 +02:00
|
|
|
// 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.
|
|
|
|
|
if resp.StatusCode == http.StatusUnauthorized {
|
|
|
|
|
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted(), HTTPStatusCode: resp.StatusCode}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-13 01:07:00 +00:00
|
|
|
// If the request was not successful, append the default Taskfile names to
|
|
|
|
|
// the URL and return the URL of the first successful request
|
2025-12-07 17:29:51 -03:00
|
|
|
for _, taskfile := range DefaultTaskfiles {
|
2024-02-13 01:07:00 +00:00
|
|
|
// Fixes a bug with JoinPath where a leading slash is not added to the
|
|
|
|
|
// path if it is empty
|
|
|
|
|
if u.Path == "" {
|
|
|
|
|
u.Path = "/"
|
|
|
|
|
}
|
|
|
|
|
alt := u.JoinPath(taskfile)
|
|
|
|
|
req.URL = alt
|
|
|
|
|
|
|
|
|
|
// Try the alternative URL
|
2026-01-25 18:51:30 +01:00
|
|
|
resp, err = client.Do(req)
|
2024-02-13 01:07:00 +00:00
|
|
|
if err != nil {
|
2025-05-24 13:38:18 +01:00
|
|
|
return nil, errors.TaskfileFetchFailedError{URI: u.Redacted()}
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
|
|
// If the request was successful, return the URL
|
|
|
|
|
if resp.StatusCode == http.StatusOK {
|
|
|
|
|
return alt, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-24 13:38:18 +01:00
|
|
|
return nil, errors.TaskfileNotFoundError{URI: u.Redacted(), Walk: false}
|
2024-02-13 01:07:00 +00:00
|
|
|
}
|