mirror of
https://github.com/fleetdm/fleet
synced 2026-05-22 08:28:52 +00:00
fixes: #30325 Related to incorrect behavior introduced at https://github.com/fleetdm/fleet/pull/28945 - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Added/updated automated tests - [x] Manual QA for all new/changed functionality <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * When uploading software batches, if the installer SHA is set to "no_check," the system will now automatically generate and use the SHA256 checksum of the installer file. * **Bug Fixes** * Fixed an issue ensuring the latest Google Chrome version is pulled during Fleet-maintained app updates. * Corrected the display of the SHA256 hash in the UI and API to show valid values. * Improved handling of installer uploads to ensure a valid SHA256 checksum is always applied, even when "no_check" is specified. * **Tests** * Added a test to verify correct SHA256 hash calculation for installer files. * Extended integration tests to validate batch software installer operations for maintained apps with SHA256 hash checks. * Added tests covering behavior when SHA256 checksum is marked as "no_check" for maintained apps. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
108 lines
3.2 KiB
Go
108 lines
3.2 KiB
Go
package maintained_apps
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
// InstallerTimeout is the timeout duration for downloading and adding a maintained app.
|
|
const InstallerTimeout = 15 * time.Minute
|
|
|
|
// DownloadInstaller downloads the maintained app installer located at the given URL.
|
|
func DownloadInstaller(ctx context.Context, installerURL string, client *http.Client) (*fleet.TempFileReader, string, error) {
|
|
// validate the URL before doing the request
|
|
_, err := url.ParseRequestURI(installerURL)
|
|
if err != nil {
|
|
return nil, "", fleet.NewInvalidArgumentError(
|
|
"fleet_maintained_app.url",
|
|
fmt.Sprintf("Couldn't download maintained app installer. URL (%q) is invalid", installerURL),
|
|
)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, installerURL, nil)
|
|
if err != nil {
|
|
return nil, "", ctxerr.Wrapf(ctx, err, "creating request for URL %s", installerURL)
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, "", ctxerr.Wrapf(ctx, err, "performing request for URL %s", installerURL)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == http.StatusNotFound {
|
|
return nil, "", fleet.NewInvalidArgumentError(
|
|
"fleet_maintained_app.url",
|
|
fmt.Sprintf("Couldn't download maintained app installer. URL (%q) doesn't exist. Please make sure that URLs are publicy accessible to the internet.", installerURL),
|
|
)
|
|
}
|
|
|
|
// Allow all 2xx and 3xx status codes in this pass.
|
|
if resp.StatusCode > 400 {
|
|
return nil, "", fleet.NewInvalidArgumentError(
|
|
"fleet_maintained_app.url",
|
|
fmt.Sprintf("Couldn't download maintained app installer. URL (%q) received response status code %d.", installerURL, resp.StatusCode),
|
|
)
|
|
}
|
|
|
|
tfr, err := fleet.NewTempFileReader(resp.Body, nil)
|
|
if err != nil {
|
|
return nil, "", ctxerr.Wrapf(ctx, err, "reading installer %q contents", installerURL)
|
|
}
|
|
|
|
return tfr, FilenameFromResponse(resp), nil
|
|
}
|
|
|
|
func FilenameFromResponse(resp *http.Response) string {
|
|
var filename string
|
|
cdh, ok := resp.Header["Content-Disposition"]
|
|
if ok && len(cdh) > 0 {
|
|
_, params, err := mime.ParseMediaType(cdh[0])
|
|
if err == nil {
|
|
filename = params["filename"]
|
|
} else {
|
|
// fallback for responses that include a filename in their content-disposition header
|
|
// but the header isn't technically RFC compliant
|
|
cdhParts := strings.Split(cdh[0], "filename=")
|
|
if len(cdhParts) > 1 {
|
|
unescapedFilename, err := url.QueryUnescape(cdhParts[1])
|
|
if err == nil {
|
|
filename = unescapedFilename
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back on extracting the filename from the URL
|
|
// This is OK for the first 20 apps we support, but we should do something more robust once we
|
|
// support more apps.
|
|
if filename == "" && resp.Request.URL.Path != "" {
|
|
filename = path.Base(resp.Request.URL.Path)
|
|
}
|
|
|
|
return filename
|
|
}
|
|
|
|
func SHA256FromInstallerFile(installerTFR *fleet.TempFileReader) (string, error) {
|
|
h := sha256.New()
|
|
_, _ = io.Copy(h, installerTFR) // writes to a Hash can never fail
|
|
gotHash := hex.EncodeToString(h.Sum(nil))
|
|
|
|
if err := installerTFR.Rewind(); err != nil {
|
|
return "", fmt.Errorf("rewind installer reader: %w", err)
|
|
}
|
|
|
|
return gotHash, nil
|
|
}
|