diff --git a/ee/server/service/maintained_apps.go b/ee/server/service/maintained_apps.go index 34497b6e90..c874999988 100644 --- a/ee/server/service/maintained_apps.go +++ b/ee/server/service/maintained_apps.go @@ -5,7 +5,10 @@ import ( "context" "crypto/sha256" "encoding/hex" + "os" + "time" + "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/fleet" @@ -28,7 +31,13 @@ func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, app } // Download installer from the URL - installerBytes, filename, err := maintainedapps.DownloadInstaller(ctx, app.InstallerURL) + timeout := maintainedapps.InstallerTimeout + if v := os.Getenv("FLEET_DEV_MAINTAINED_APPS_INSTALLER_TIMEOUT"); v != "" { + timeout, _ = time.ParseDuration(v) + } + + client := fleethttp.NewClient(fleethttp.WithTimeout(timeout)) + installerBytes, filename, err := maintainedapps.DownloadInstaller(ctx, app.InstallerURL, client) if err != nil { return ctxerr.Wrap(ctx, err, "downloading app installer") } diff --git a/server/mdm/maintainedapps/installers.go b/server/mdm/maintainedapps/installers.go index 35f5ef4ffc..e9b6e145ce 100644 --- a/server/mdm/maintainedapps/installers.go +++ b/server/mdm/maintainedapps/installers.go @@ -10,13 +10,15 @@ import ( "path" "time" - "github.com/fleetdm/fleet/v4/pkg/fleethttp" "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) ([]byte, string, error) { +func DownloadInstaller(ctx context.Context, installerURL string, client *http.Client) ([]byte, string, error) { // validate the URL before doing the request _, err := url.ParseRequestURI(installerURL) if err != nil { @@ -26,8 +28,6 @@ func DownloadInstaller(ctx context.Context, installerURL string) ([]byte, string ) } - client := fleethttp.NewClient(fleethttp.WithTimeout(30 * time.Second)) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, installerURL, nil) if err != nil { return nil, "", ctxerr.Wrapf(ctx, err, "creating request for URL %s", installerURL) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index e534117dd5..6e4f32e898 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -14235,11 +14235,15 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { ctx := context.Background() installerBytes := []byte("abc") + // Mock server to serve the "installers" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/badinstaller": _, _ = w.Write([]byte("badinstaller")) + case "/timeout": + time.Sleep(3 * time.Second) + _, _ = w.Write([]byte("timeout")) default: _, _ = w.Write(installerBytes) } @@ -14270,6 +14274,8 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { _, err = q.ExecContext(ctx, "UPDATE fleet_library_apps SET sha256 = ?, installer_url = ?", spoofedSHA, srv.URL+"/installer.zip") require.NoError(t, err) _, err = q.ExecContext(ctx, "UPDATE fleet_library_apps SET installer_url = ? WHERE id = 2", srv.URL+"/badinstaller") + require.NoError(t, err) + _, err = q.ExecContext(ctx, "UPDATE fleet_library_apps SET installer_url = ? WHERE id = 3", srv.URL+"/timeout") return err }) @@ -14383,10 +14389,16 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { r := s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", &addFleetMaintainedAppRequest{AppID: 2}, http.StatusInternalServerError) require.Contains(t, extractServerErrorText(r.Body), "mismatch in maintained app SHA256 hash") + // Should timeout + os.Setenv("FLEET_DEV_MAINTAINED_APPS_INSTALLER_TIMEOUT", "1s") + r = s.Do("POST", "/api/latest/fleet/software/fleet_maintained_apps", &addFleetMaintainedAppRequest{AppID: 3}, http.StatusGatewayTimeout) + os.Unsetenv("FLEET_DEV_MAINTAINED_APPS_INSTALLER_TIMEOUT") + require.Contains(t, extractServerErrorText(r.Body), "Couldn't upload. Request timeout. Please make sure your server and load balancer timeout is long enough.") + // Add a maintained app to no team req = &addFleetMaintainedAppRequest{ - AppID: 3, + AppID: 4, SelfService: true, PreInstallQuery: "SELECT 1", InstallScript: "echo foo", @@ -14409,7 +14421,7 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { "team_id", "0", ) - mapp, err = s.ds.GetMaintainedAppByID(ctx, 3) + mapp, err = s.ds.GetMaintainedAppByID(ctx, 4) require.NoError(t, err) require.Equal(t, 1, resp.Count) title = resp.SoftwareTitles[0] @@ -14418,9 +14430,9 @@ func (s *integrationEnterpriseTestSuite) TestMaintainedApps() { require.Equal(t, mapp.Version, title.SoftwarePackage.Version) require.Equal(t, "installer.zip", title.SoftwarePackage.Name) - i, err = s.ds.GetSoftwareInstallerMetadataByID(context.Background(), getSoftwareInstallerIDByMAppID(3)) + i, err = s.ds.GetSoftwareInstallerMetadataByID(context.Background(), getSoftwareInstallerIDByMAppID(4)) require.NoError(t, err) - require.Equal(t, ptr.Uint(3), i.FleetLibraryAppID) + require.Equal(t, ptr.Uint(4), i.FleetLibraryAppID) require.Equal(t, mapp.SHA256, i.StorageID) require.Equal(t, "darwin", i.Platform) require.NotEmpty(t, i.InstallScriptContentID) diff --git a/server/service/maintained_apps.go b/server/service/maintained_apps.go index ef89ca0ac1..ed785033bc 100644 --- a/server/service/maintained_apps.go +++ b/server/service/maintained_apps.go @@ -2,8 +2,10 @@ package service import ( "context" + "errors" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" ) type addFleetMaintainedAppRequest struct { @@ -23,8 +25,14 @@ func (r addFleetMaintainedAppResponse) error() error { return r.Err } func addFleetMaintainedAppEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { req := request.(*addFleetMaintainedAppRequest) + ctx, cancel := context.WithTimeout(ctx, maintainedapps.InstallerTimeout) + defer cancel() err := svc.AddFleetMaintainedApp(ctx, req.TeamID, req.AppID, req.InstallScript, req.PreInstallQuery, req.PostInstallScript, req.SelfService) if err != nil { + if errors.Is(err, context.DeadlineExceeded) { + err = fleet.NewGatewayTimeoutError("Couldn't upload. Request timeout. Please make sure your server and load balancer timeout is long enough.", err) + } + return &addFleetMaintainedAppResponse{Err: err}, nil } return &addFleetMaintainedAppResponse{}, nil