mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
**Related issue:** Resolves #41379 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [ ] Verified that fleetd runs on macOS, Linux and Windows - [ ] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added EUA token support to Orbit enrollment workflow * Introduced `--eua-token` CLI flag for Windows MDM enrollment * Windows MSI packages now support EUA_TOKEN property (Orbit v1.55.0+) * **Tests** * Added tests for EUA token handling in enrollment and Windows packaging * **Documentation** * Added changelog entry documenting EUA token inclusion in enrollment requests <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
570 lines
18 KiB
Go
570 lines
18 KiB
Go
package packaging
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging/wix"
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
"github.com/fleetdm/fleet/v4/pkg/secure"
|
|
"github.com/josephspurrier/goversioninfo"
|
|
"github.com/rs/zerolog/log"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
const wixDownload = "https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip"
|
|
|
|
// BuildMSI builds a Windows .msi.
|
|
// Note: this function is not safe for concurrent use
|
|
func BuildMSI(opt Options) (string, error) {
|
|
tmpDir, err := initializeTempDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
filesystemRoot := filepath.Join(tmpDir, "root")
|
|
if err := secure.MkdirAll(filesystemRoot, constant.DefaultDirMode); err != nil {
|
|
return "", fmt.Errorf("create root dir: %w", err)
|
|
}
|
|
orbitRoot := filesystemRoot
|
|
if err := secure.MkdirAll(orbitRoot, constant.DefaultDirMode); err != nil {
|
|
return "", fmt.Errorf("create orbit dir: %w", err)
|
|
}
|
|
|
|
// Initialize autoupdate metadata
|
|
|
|
updateOpt := update.DefaultOptions
|
|
|
|
updateOpt.RootDirectory = orbitRoot
|
|
if opt.Architecture == ArchAmd64 {
|
|
updateOpt.Targets = update.WindowsTargets
|
|
} else {
|
|
updateOpt.Targets = update.WindowsArm64Targets
|
|
}
|
|
updateOpt.ServerCertificatePath = opt.UpdateTLSServerCertificate
|
|
|
|
if opt.UpdateTLSClientCertificate != "" {
|
|
updateClientCrt, err := tls.LoadX509KeyPair(opt.UpdateTLSClientCertificate, opt.UpdateTLSClientKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error loading update client certificate and key: %w", err)
|
|
}
|
|
updateOpt.ClientCertificate = &updateClientCrt
|
|
}
|
|
|
|
if opt.Desktop {
|
|
if opt.Architecture == ArchArm64 {
|
|
updateOpt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsArm64Target
|
|
} else {
|
|
updateOpt.Targets[constant.DesktopTUFTargetName] = update.DesktopWindowsTarget
|
|
}
|
|
// Override default channel with the provided value.
|
|
updateOpt.Targets.SetTargetChannel(constant.DesktopTUFTargetName, opt.DesktopChannel)
|
|
}
|
|
|
|
// Override default channels with the provided values.
|
|
updateOpt.Targets.SetTargetChannel(constant.OrbitTUFTargetName, opt.OrbitChannel)
|
|
updateOpt.Targets.SetTargetChannel(constant.OsqueryTUFTargetName, opt.OsquerydChannel)
|
|
|
|
updateOpt.ServerURL = opt.UpdateURL
|
|
if opt.UpdateRoots != "" {
|
|
updateOpt.RootKeys = opt.UpdateRoots
|
|
}
|
|
|
|
updatesData, err := InitializeUpdates(updateOpt)
|
|
if err != nil {
|
|
return "", fmt.Errorf("initialize updates: %w", err)
|
|
}
|
|
log.Debug().Stringer("data", updatesData).Msg("updates initialized")
|
|
if opt.Version == "" {
|
|
// We set the package version to orbit's latest version.
|
|
opt.Version = updatesData.OrbitVersion
|
|
}
|
|
|
|
orbitVersion := updatesData.OrbitVersion
|
|
if !strings.HasPrefix(orbitVersion, "v") {
|
|
orbitVersion = "v" + orbitVersion
|
|
}
|
|
// v1.28.0 introduced configurable END_USER_EMAIL property for MSI package: https://github.com/fleetdm/fleet/issues/19219
|
|
if semver.Compare(orbitVersion, "v1.28.0") >= 0 {
|
|
opt.EnableEndUserEmailProperty = true
|
|
}
|
|
// v1.55.0 introduced EUA_TOKEN property for MSI package: https://github.com/fleetdm/fleet/issues/41379
|
|
if semver.Compare(orbitVersion, "v1.55.0") >= 0 {
|
|
opt.EnableEUATokenProperty = true
|
|
}
|
|
|
|
// Write files
|
|
|
|
if err := writeSecret(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write enroll secret: %w", err)
|
|
}
|
|
|
|
if err := writeOsqueryFlagfile(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write flagfile: %w", err)
|
|
}
|
|
|
|
if err := writeOsqueryCertPEM(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write certs.pem: %w", err)
|
|
}
|
|
|
|
if opt.FleetCertificate != "" {
|
|
if err := writeFleetServerCertificate(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write fleet server certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
if opt.FleetTLSClientCertificate != "" {
|
|
if err := writeFleetClientCertificate(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write fleet client certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
if opt.UpdateTLSServerCertificate != "" {
|
|
if err := writeUpdateServerCertificate(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write update server certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
if opt.UpdateTLSClientCertificate != "" {
|
|
if err := writeUpdateClientCertificate(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write update client certificate: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := writeEventLogFile(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write eventlog file: %w", err)
|
|
}
|
|
|
|
if err := writePowershellInstallerUtilsFile(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write powershell installer utils file: %w", err)
|
|
}
|
|
|
|
if err := writeResourceSyso(opt, orbitRoot); err != nil {
|
|
return "", fmt.Errorf("write VERSIONINFO: %w", err)
|
|
}
|
|
|
|
if err := writeWixFile(opt, tmpDir); err != nil {
|
|
return "", fmt.Errorf("write wix file: %w", err)
|
|
}
|
|
|
|
if runtime.GOOS == "windows" {
|
|
// Explicitly grant read access, otherwise within the Docker
|
|
// container there are permissions errors.
|
|
// "S-1-1-0" is the SID for the World/Everyone group
|
|
// (a group that includes all users).
|
|
out, err := exec.Command(
|
|
"icacls", tmpDir, "/grant", "*S-1-1-0:R", "/t",
|
|
).CombinedOutput()
|
|
if err != nil {
|
|
fmt.Println(string(out))
|
|
return "", fmt.Errorf("icacls: %w", err)
|
|
}
|
|
}
|
|
|
|
absWixDir := opt.LocalWixDir
|
|
wineChecked := false
|
|
|
|
// Download wix for macOS running on arm64, unless a local-wix-dir is provided.
|
|
// We are using native MSI build on macOS arm64, instead of Docker, because the current fleetdm/wix Docker image is unreliable on macOS arm64.
|
|
// We are looking into creating a new Docker image for macOS arm64.
|
|
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" && absWixDir == "" {
|
|
fmt.Println("Detected macOS arm64. fleetctl must use locally installed wine and wix to build the MSI package.")
|
|
|
|
// Ensure wine is installed before downloading wix
|
|
if err = checkWine(false); err != nil {
|
|
return "", err
|
|
}
|
|
wineChecked = true
|
|
|
|
fmt.Printf("Downloading wix from %s\n", wixDownload)
|
|
client := fleethttp.NewClient()
|
|
absWixDir = filepath.Join(tmpDir, "wix")
|
|
err = downloadAndExtractZip(client, wixDownload, absWixDir)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
if absWixDir != "" {
|
|
absWixDir, err = filepath.Abs(absWixDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not get filepath from local-wix-dir %s: %w", opt.LocalWixDir, err)
|
|
}
|
|
if err = checkWine(wineChecked); err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
if err := wix.Heat(tmpDir, opt.NativeTooling, absWixDir); err != nil {
|
|
return "", fmt.Errorf("package root files: %w", err)
|
|
}
|
|
|
|
if err := wix.TransformHeat(filepath.Join(tmpDir, "heat.wxs")); err != nil {
|
|
return "", fmt.Errorf("transform heat: %w", err)
|
|
}
|
|
|
|
if err := wix.Candle(tmpDir, opt.NativeTooling, absWixDir, opt.Architecture); err != nil {
|
|
return "", fmt.Errorf("build package: %w", err)
|
|
}
|
|
|
|
if err := wix.Light(tmpDir, opt.NativeTooling, absWixDir); err != nil {
|
|
return "", fmt.Errorf("build package: %w", err)
|
|
}
|
|
|
|
filename := "fleet-osquery.msi"
|
|
if opt.CustomOutfile != "" {
|
|
filename = opt.CustomOutfile
|
|
}
|
|
if opt.Architecture == ArchArm64 {
|
|
filename = "fleet-osquery-arm64.msi"
|
|
}
|
|
if opt.NativeTooling {
|
|
filename = filepath.Join("build", filename)
|
|
}
|
|
if err := file.Copy(filepath.Join(tmpDir, "orbit.msi"), filename, constant.DefaultFileMode); err != nil {
|
|
return "", fmt.Errorf("rename msi: %w", err)
|
|
}
|
|
log.Info().Str("path", filename).Msg("wrote msi package")
|
|
|
|
return filename, nil
|
|
}
|
|
|
|
func checkWine(wineChecked bool) error {
|
|
if !wineChecked && runtime.GOOS == "darwin" {
|
|
// Ensure wine is installed
|
|
cmd := exec.Command(wix.WineCmd, "--version")
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf(
|
|
"%s failed. Is Wine installed? Creating a fleetd agent for Windows (.msi) requires Wine. To install Wine see the script here: https://fleetdm.com/install-wine %w",
|
|
wix.WineCmd, err,
|
|
)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func writeWixFile(opt Options, rootPath string) error {
|
|
// PackageInfo is metadata for the pkg
|
|
path := filepath.Join(rootPath, "main.wxs")
|
|
if err := secure.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil {
|
|
return fmt.Errorf("mkdir: %w", err)
|
|
}
|
|
|
|
var contents bytes.Buffer
|
|
if err := windowsWixTemplate.Execute(&contents, opt); err != nil {
|
|
return fmt.Errorf("execute template: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(path, contents.Bytes(), 0o666); err != nil {
|
|
return fmt.Errorf("write file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func writeEventLogFile(opt Options, rootPath string) error {
|
|
// Eventlog manifest is going to be built and dumped into working directory
|
|
path := filepath.Join(rootPath, "osquery.man")
|
|
if err := secure.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil {
|
|
return fmt.Errorf("event log manifest creation: %w", err)
|
|
}
|
|
|
|
var contents bytes.Buffer
|
|
if err := windowsOsqueryEventLogTemplate.Execute(&contents, opt); err != nil {
|
|
return fmt.Errorf("event log manifest creation: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(path, contents.Bytes(), constant.DefaultFileMode); err != nil {
|
|
return fmt.Errorf("event log manifest creation: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func writePowershellInstallerUtilsFile(opt Options, rootPath string) error {
|
|
// Powershell installer utils file is going to be built and dumped into working directory
|
|
path := filepath.Join(rootPath, "installer_utils.ps1")
|
|
if err := secure.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil {
|
|
return fmt.Errorf("powershell installer utils location creation: %w", err)
|
|
}
|
|
|
|
var contents bytes.Buffer
|
|
if err := windowsPSInstallerUtils.Execute(&contents, opt); err != nil {
|
|
return fmt.Errorf("powershell installer utils transform: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(path, contents.Bytes(), constant.DefaultFileMode); err != nil {
|
|
return fmt.Errorf("powershell installer utils file write: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeManifestXML creates the manifest.xml file used when generating the 'resource_windows.syso' metadata
|
|
// (see writeResourceSyso). Returns the path of the newly created file.
|
|
func writeManifestXML(vParts []string, orbitPath string, arch string) (string, error) {
|
|
filePath := filepath.Join(orbitPath, "manifest.xml")
|
|
|
|
tmplOpts := struct {
|
|
Version string
|
|
Arch string
|
|
}{
|
|
Version: strings.Join(vParts, "."),
|
|
Arch: arch,
|
|
}
|
|
|
|
var contents bytes.Buffer
|
|
if err := ManifestXMLTemplate.Execute(&contents, tmplOpts); err != nil {
|
|
return "", fmt.Errorf("parsing manifest.xml template: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(filePath, contents.Bytes(), constant.DefaultFileMode); err != nil {
|
|
return "", fmt.Errorf("writing manifest.xml file: %w", err)
|
|
}
|
|
|
|
return filePath, nil
|
|
}
|
|
|
|
// createVersionInfo returns a VersionInfo struct pointer to be used to generate the 'resource_windows.syso'
|
|
// metadata file (see writeResourceSyso).
|
|
func createVersionInfo(vParts []string, manifestPath string) (*goversioninfo.VersionInfo, error) {
|
|
vIntParts := make([]int, 0, len(vParts))
|
|
for _, p := range vParts {
|
|
v, err := strconv.Atoi(p)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error parsing version part %s: %w", p, err)
|
|
}
|
|
vIntParts = append(vIntParts, v)
|
|
}
|
|
version := strings.Join(vParts, ".")
|
|
copyright := fmt.Sprintf("%d Fleet Device Management Inc.", time.Now().Year())
|
|
|
|
// Taken from https://github.com/josephspurrier/goversioninfo/blob/master/testdata/resource/versioninfo.json
|
|
langID, err := strconv.ParseUint("0409", 16, 16)
|
|
if err != nil {
|
|
return nil, errors.New("invalid LangID")
|
|
}
|
|
// Taken from https://github.com/josephspurrier/goversioninfo/blob/master/testdata/resource/versioninfo.json
|
|
charsetID, err := strconv.ParseUint("04B0", 16, 16)
|
|
if err != nil {
|
|
return nil, errors.New("invalid charsetID")
|
|
}
|
|
|
|
result := goversioninfo.VersionInfo{
|
|
FixedFileInfo: goversioninfo.FixedFileInfo{
|
|
FileVersion: goversioninfo.FileVersion{
|
|
Major: vIntParts[0],
|
|
Minor: vIntParts[1],
|
|
Patch: vIntParts[2],
|
|
Build: vIntParts[3],
|
|
},
|
|
ProductVersion: goversioninfo.FileVersion{
|
|
Major: vIntParts[0],
|
|
Minor: vIntParts[1],
|
|
Patch: vIntParts[2],
|
|
Build: vIntParts[3],
|
|
},
|
|
FileFlagsMask: "3f",
|
|
FileFlags: "00",
|
|
FileOS: "040004",
|
|
FileType: "01",
|
|
FileSubType: "00",
|
|
},
|
|
StringFileInfo: goversioninfo.StringFileInfo{
|
|
Comments: "Fleet osquery",
|
|
CompanyName: "Fleet Device Management (fleetdm.com)",
|
|
FileDescription: "Fleet osquery installer",
|
|
FileVersion: version,
|
|
InternalName: "",
|
|
LegalCopyright: copyright,
|
|
LegalTrademarks: "",
|
|
OriginalFilename: "",
|
|
PrivateBuild: "",
|
|
ProductName: "Fleet osquery",
|
|
ProductVersion: version,
|
|
SpecialBuild: "",
|
|
},
|
|
VarFileInfo: goversioninfo.VarFileInfo{
|
|
Translation: goversioninfo.Translation{
|
|
LangID: goversioninfo.LangID(langID),
|
|
CharsetID: goversioninfo.CharsetID(charsetID),
|
|
},
|
|
},
|
|
IconPath: "",
|
|
ManifestPath: manifestPath,
|
|
}
|
|
|
|
return &result, nil
|
|
}
|
|
|
|
// SanitizeVersion returns the version parts (Major, Minor, Patch and Build), filling the Build part
|
|
// with '0' if missing. Will error out if the version string is missing the Major, Minor or
|
|
// Patch part(s).
|
|
// It supports the version with a pre-release part (e.g. 1.2.3-1) and returns it as the Build number.
|
|
func SanitizeVersion(version string) ([]string, error) {
|
|
vParts := strings.Split(version, ".")
|
|
if len(vParts) < 3 {
|
|
return nil, errors.New("invalid version string")
|
|
}
|
|
if len(vParts) == 3 && strings.Contains(vParts[2], "-") {
|
|
parts := strings.SplitN(vParts[2], "-", 2)
|
|
if len(parts) < 2 || parts[0] == "" || parts[1] == "" {
|
|
return nil, fmt.Errorf("invalid patch and pre-release version: %s", vParts[2])
|
|
}
|
|
patch, preRelease := parts[0], parts[1]
|
|
vParts = []string{vParts[0], vParts[1], patch, preRelease}
|
|
}
|
|
|
|
if len(vParts) < 4 {
|
|
vParts = append(vParts, "0")
|
|
}
|
|
|
|
return vParts[:4], nil
|
|
}
|
|
|
|
// writeResourceSyso creates the 'resource_windows.syso' metadata file which contains the required Microsoft
|
|
// Windows Version Information
|
|
func writeResourceSyso(opt Options, orbitPath string) error {
|
|
if err := secure.MkdirAll(orbitPath, constant.DefaultDirMode); err != nil {
|
|
return fmt.Errorf("mkdir: %w", err)
|
|
}
|
|
|
|
vParts, err := SanitizeVersion(opt.Version)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid version %s: %w", opt.Version, err)
|
|
}
|
|
|
|
manifestPath, err := writeManifestXML(vParts, orbitPath, opt.Architecture)
|
|
if err != nil {
|
|
return fmt.Errorf("creating manifest.xml: %w", err)
|
|
}
|
|
defer os.RemoveAll(manifestPath)
|
|
|
|
vi, err := createVersionInfo(vParts, manifestPath)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing versioninfo: %w", err)
|
|
}
|
|
|
|
vi.Build()
|
|
vi.Walk()
|
|
|
|
outPath := filepath.Join(orbitPath, "resource_windows.syso")
|
|
if err := vi.WriteSyso(outPath, opt.Architecture); err != nil {
|
|
return fmt.Errorf("creating syso file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func downloadAndExtractZip(client *http.Client, urlPath string, destPath string) error {
|
|
zipFile, err := os.CreateTemp("", "file.zip")
|
|
if err != nil {
|
|
return fmt.Errorf("create file: %w", err)
|
|
}
|
|
defer zipFile.Close()
|
|
defer os.Remove(zipFile.Name())
|
|
|
|
req, err := http.NewRequest(http.MethodGet, urlPath, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("could not download %s: %w", urlPath, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("could not download %s: received http status code %s", urlPath, resp.Status)
|
|
}
|
|
_, err = io.Copy(zipFile, resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("could not write %s: %w", zipFile.Name(), err)
|
|
}
|
|
|
|
// Open the downloaded file for reading. With zip, we cannot unzip directly from resp.Body
|
|
zipReader, err := zip.OpenReader(zipFile.Name())
|
|
if err != nil {
|
|
return fmt.Errorf("could not open %s: %w", zipFile.Name(), err)
|
|
}
|
|
defer zipReader.Close()
|
|
|
|
err = os.MkdirAll(filepath.Dir(destPath), 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create directory %s: %w", filepath.Dir(destPath), err)
|
|
}
|
|
|
|
// Extract each file in the archive
|
|
for _, archiveReader := range zipReader.File {
|
|
err = extractZipFile(archiveReader, destPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func extractZipFile(archiveReader *zip.File, destPath string) error {
|
|
if archiveReader.FileInfo().Mode()&os.ModeSymlink != 0 {
|
|
// Skip symlinks for security reasons
|
|
return nil
|
|
}
|
|
|
|
// Open the file in the archive
|
|
archiveFile, err := archiveReader.Open()
|
|
if err != nil {
|
|
return fmt.Errorf("could not open archive %s: %w", archiveReader.Name, err)
|
|
}
|
|
defer archiveFile.Close()
|
|
|
|
// Clean the archive path to prevent extracting files outside the destination.
|
|
archivePath := filepath.Clean(archiveReader.Name)
|
|
if strings.HasPrefix(archivePath, ".."+string(filepath.Separator)) {
|
|
// Skip relative paths for security reasons
|
|
return nil
|
|
}
|
|
// Prepare to write the file
|
|
finalPath := filepath.Join(destPath, archivePath)
|
|
|
|
// Check if the file to extract is just a directory
|
|
if archiveReader.FileInfo().IsDir() {
|
|
err = os.MkdirAll(finalPath, 0o755)
|
|
if err != nil {
|
|
return fmt.Errorf("could not create directory %s: %w", finalPath, err)
|
|
}
|
|
} else {
|
|
// Create all needed directories
|
|
if err := os.MkdirAll(filepath.Dir(finalPath), 0o755); err != nil {
|
|
return fmt.Errorf("could not create directory %s: %w", filepath.Dir(finalPath), err)
|
|
}
|
|
|
|
// Prepare to write the destination file
|
|
destinationFile, err := os.OpenFile(finalPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, archiveReader.Mode())
|
|
if err != nil {
|
|
return fmt.Errorf("could not open file %s: %w", finalPath, err)
|
|
}
|
|
defer destinationFile.Close()
|
|
|
|
// Write the destination file
|
|
if _, err = io.Copy(destinationFile, archiveFile); err != nil {
|
|
return fmt.Errorf("could not write file %s: %w", finalPath, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|