2025-03-21 02:21:56 +00:00
package maintained_apps
2024-09-18 16:21:53 +00:00
import (
"context"
"fmt"
"mime"
"net/http"
"net/url"
"path"
2025-03-21 02:21:56 +00:00
"strings"
2024-09-18 16:21:53 +00:00
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
2024-09-19 19:42:17 +00:00
// InstallerTimeout is the timeout duration for downloading and adding a maintained app.
const InstallerTimeout = 15 * time . Minute
2024-09-18 16:21:53 +00:00
// DownloadInstaller downloads the maintained app installer located at the given URL.
2024-11-12 14:28:08 +00:00
func DownloadInstaller ( ctx context . Context , installerURL string , client * http . Client ) ( * fleet . TempFileReader , string , error ) {
2024-09-18 16:21:53 +00:00
// 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 ) ,
)
}
2025-05-07 23:16:08 +00:00
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 {
2024-09-18 16:21:53 +00:00
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" ]
2025-03-21 02:21:56 +00:00
} 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
}
}
2024-09-18 16:21:53 +00:00
}
}
// 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.
2025-05-07 23:16:08 +00:00
if filename == "" && resp . Request . URL . Path != "" {
2024-09-18 16:21:53 +00:00
filename = path . Base ( resp . Request . URL . Path )
}
2025-05-07 23:16:08 +00:00
return filename
2024-09-18 16:21:53 +00:00
}