diff --git a/.gitignore b/.gitignore index af85c7e08..de404ab1a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ vendor build bin .DS_Store +test-cmds ## Below from https://github.com/github/gitignore/blob/master/Go.gitignore ## diff --git a/cmd/package-builder/package-builder.go b/cmd/package-builder/package-builder.go index 459fdfad2..ce41cd6c0 100644 --- a/cmd/package-builder/package-builder.go +++ b/cmd/package-builder/package-builder.go @@ -300,7 +300,7 @@ func main() { func defaultTargets() string { switch runtime.GOOS { case "windows": - return "windows-none-msi" + return "windows-service-msi" case "linux": return "linux-systemd-rpm,linux-systemd-deb" case "darwin": diff --git a/docs/package-builder.md b/docs/package-builder.md index a4649a931..14c0f1907 100644 --- a/docs/package-builder.md +++ b/docs/package-builder.md @@ -163,3 +163,13 @@ As `ioutil.TempFile` respects the `TMPDIR` environmental variable, there is a si ``` shell export TMPDIR=/tmp ``` + +#### Windows + +Windows can be built without a service `windows-none-msi` or with a +service `windows-service-msi`. + +Note that the windows package will only install as `ALLUSERS`. You may +need to use elevated privileges to install it. This will likely be +confusing. `msiexec.exe` will either silently fail, or be +inscrutable. But `start` will work. diff --git a/pkg/packagekit/internal/assets.go b/pkg/packagekit/internal/assets.go index 554d1de5f..325047c5a 100644 --- a/pkg/packagekit/internal/assets.go +++ b/pkg/packagekit/internal/assets.go @@ -46,28 +46,31 @@ func (fi bindataFileInfo) Sys() interface{} { var _internalAssetsMainWxs = []byte(` - - - - - + + + + + + + @@ -82,7 +85,7 @@ var _internalAssetsMainWxs = []byte(` It's up to the caller to decide which directories to use --> - + @@ -112,7 +115,7 @@ func internalAssetsMainWxs() (*asset, error) { return nil, err } - info := bindataFileInfo{name: "internal/assets/main.wxs", size: 1558, mode: os.FileMode(420), modTime: time.Unix(1547607846, 0)} + info := bindataFileInfo{name: "internal/assets/main.wxs", size: 1721, mode: os.FileMode(420), modTime: time.Unix(1550456584, 0)} a := &asset{bytes: bytes, info: info} return a, nil } diff --git a/pkg/packagekit/internal/assets/main.wxs b/pkg/packagekit/internal/assets/main.wxs index 80ba67a16..1dd43811d 100644 --- a/pkg/packagekit/internal/assets/main.wxs +++ b/pkg/packagekit/internal/assets/main.wxs @@ -1,27 +1,30 @@ - - - - - + + + + + + + @@ -36,7 +39,7 @@ It's up to the caller to decide which directories to use --> - + diff --git a/pkg/packagekit/package.go b/pkg/packagekit/package.go index 1f7342a99..72ae2484b 100644 --- a/pkg/packagekit/package.go +++ b/pkg/packagekit/package.go @@ -9,4 +9,5 @@ type PackageOptions struct { Scripts string // directory of packaging scripts (postinst, prerm, etc) SigningKey string // key to sign packages with (platform specific behaviors) Version string // package version + FlagFile string // Path to the flagfile for configuration } diff --git a/pkg/packagekit/package_wix.go b/pkg/packagekit/package_wix.go index fc99652a0..04835c824 100644 --- a/pkg/packagekit/package_wix.go +++ b/pkg/packagekit/package_wix.go @@ -3,6 +3,7 @@ package packagekit import ( "bytes" "context" + "fmt" "io" "runtime" "strings" @@ -17,7 +18,7 @@ import ( //go:generate go-bindata -nocompress -pkg internal -o internal/assets.go internal/assets/ -func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions) error { +func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions, includeService bool) error { ctx, span := trace.StartSpan(ctx, "packagekit.PackageWixMSI") defer span.End() @@ -30,8 +31,8 @@ func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions) error { // little more debugable to do it with go's. This way, we can // inspect the intermediate xml file. // - // This might all be cleaner moved to a marshalled struct. For now, - // just sent the template the PackageOptions struct + // This might all be cleaner moved from a template to a marshalled + // struct. But enumerating the wix options looks very ugly wixTemplateBytes, err := internal.Asset("internal/assets/main.wxs") if err != nil { return errors.Wrap(err, "getting go-bindata main.wxs") @@ -51,7 +52,6 @@ func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions) error { Opts: po, UpgradeCode: generateMicrosoftProductCode("launcher" + po.Identifier), ProductCode: generateMicrosoftProductCode("launcher"+po.Identifier, extraGuidIdentifiers...), - PackageCode: generateMicrosoftProductCode("launcher"+po.Identifier, extraGuidIdentifiers...), } wixTemplate, err := template.New("WixTemplate").Parse(string(wixTemplateBytes)) @@ -64,14 +64,24 @@ func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions) error { return errors.Wrap(err, "executing WixTemplate") } - wixTool, err := wix.New(po.Root, mainWxsContent.Bytes()) + wixArgs := []wix.WixOpt{} + + if includeService { + launcherService := wix.NewService("launcher.exe", + wix.ServiceName(fmt.Sprintf("KolideLauncher%sSvc", strings.Title(po.Identifier))), + wix.ServiceArgs([]string{"svc", "-config", po.FlagFile}), + wix.ServiceDescription(fmt.Sprintf("The Kolide Launcher (%s)", po.Identifier)), + ) + wixArgs = append(wixArgs, wix.WithService(launcherService)) + } + + wixTool, err := wix.New(po.Root, mainWxsContent.Bytes(), wixArgs...) if err != nil { return errors.Wrap(err, "making wixTool") } defer wixTool.Cleanup() - // Run light to compile the msi (and copy the output into our file - // handle) + // Use wix to compile into an MSI if err := wixTool.Package(ctx, w); err != nil { return errors.Wrap(err, "running light") } diff --git a/pkg/packagekit/wix/service.go b/pkg/packagekit/wix/service.go new file mode 100644 index 000000000..792027ada --- /dev/null +++ b/pkg/packagekit/wix/service.go @@ -0,0 +1,201 @@ +package wix + +import ( + "encoding/xml" + "fmt" + "io" + "strings" + + "github.com/pkg/errors" +) + +// http://wixtoolset.org/documentation/manual/v3/xsd/wix/serviceinstall.html +// http://wixtoolset.org/documentation/manual/v3/xsd/wix/servicecontrol.html +// https://helgeklein.com/blog/2014/09/real-world-example-wix-msi-application-installer/ +type YesNoType string + +const ( + Yes YesNoType = "yes" + No = "no" +) + +type ErrorControlType string + +const ( + ErrorControlIgnore ErrorControlType = "ignore" + ErrorControlNormal = "normal" + ErrorControlCritical = "critical" +) + +type StartType string + +const ( + StartAuto StartType = "auto" + StartDemand = "demand" + StartDisabled = "disabled" + StartBoot = "boot" + StartSystem = "system" +) + +type InstallUninstallType string + +const ( + InstallUninstallInstall InstallUninstallType = "install" + InstallUninstallUninstall = "uninstall" + InstallUninstallBoth = "both" +) + +// ServiceInstall implements http://wixtoolset.org/documentation/manual/v3/xsd/wix/serviceinstall.html +type ServiceInstall struct { + Account string `xml:",attr,omitempty"` + Arguments string `xml:",attr,omitempty"` + Description string `xml:",attr,omitempty"` + DisplayName string `xml:",attr,omitempty"` + EraseDescription bool `xml:",attr,omitempty"` + ErrorControl ErrorControlType `xml:",attr,omitempty"` + Id string `xml:",attr,omitempty"` + Interactive YesNoType `xml:",attr,omitempty"` + LoadOrderGroup string `xml:",attr,omitempty"` + Name string `xml:",attr,omitempty"` + Password string `xml:",attr,omitempty"` + Start StartType `xml:",attr,omitempty"` + Type string `xml:",attr,omitempty"` + Vital YesNoType `xml:",attr,omitempty"` +} + +// ServiceControl implements http://wixtoolset.org/documentation/manual/v3/xsd/wix/servicecontrol.html +type ServiceControl struct { + Name string `xml:",attr,omitempty"` + Id string `xml:",attr,omitempty"` + Remove InstallUninstallType `xml:",attr,omitempty"` + Start InstallUninstallType `xml:",attr,omitempty"` + Stop InstallUninstallType `xml:",attr,omitempty"` + Wait YesNoType `xml:",attr,omitempty"` +} + +// Service represents a wix service. It provides an interface to both +// ServiceInstall and ServiceControl. +type Service struct { + matchString string + count int // number of times we've seen this. Used for error handling + expectedCount int + + serviceInstall *ServiceInstall + serviceControl *ServiceControl +} + +type ServiceOpt func(*Service) + +func ServiceName(name string) ServiceOpt { + return func(s *Service) { + s.serviceControl.Id = name + s.serviceControl.Name = name + s.serviceInstall.Id = name + s.serviceInstall.Name = name + } +} + +func ServiceDescription(desc string) ServiceOpt { + return func(s *Service) { + s.serviceInstall.Description = desc + } +} + +// ServiceArgs takes an array of args, wraps them in spaces, then +// joins them into a string. Handling spaces in the arguments is a bit +// gnarly. Some parts of windows use ` as an escape character, but +// that doesn't seem to work here. However, we can use double quotes +// to wrap the whole string. But, in xml quotes are _always_ +// transmitted as html entities -- " or ". Luckily, wix seems +// fine with that. It converts them back to double quotes when it +// makes the service +func ServiceArgs(args []string) ServiceOpt { + return func(s *Service) { + quotedArgs := make([]string, len(args)) + + for i, arg := range args { + if strings.ContainsAny(arg, " ") { + quotedArgs[i] = fmt.Sprintf(`"%s"`, arg) + } else { + quotedArgs[i] = arg + } + } + + s.serviceInstall.Arguments = strings.Join(quotedArgs, " ") + } +} + +// New returns a service +func NewService(matchString string, opts ...ServiceOpt) *Service { + defaultName := strings.TrimSuffix(matchString, ".exe") + "Svc" + + si := &ServiceInstall{ + Name: defaultName, + Id: defaultName, + Account: `NT AUTHORITY\SYSTEM`, + Start: StartAuto, + Type: "ownProcess", + ErrorControl: ErrorControlNormal, + Vital: Yes, + } + + sc := &ServiceControl{ + Name: defaultName, + Id: defaultName, + Stop: InstallUninstallBoth, + Start: InstallUninstallInstall, + Remove: InstallUninstallUninstall, + Wait: No, + } + + s := &Service{ + matchString: matchString, + expectedCount: 1, + count: 0, + serviceInstall: si, + serviceControl: sc, + } + + for _, opt := range opts { + opt(s) + } + + return s +} + +// Match returns a bool if there's a match, and throws an error if we +// have too many matches. This is to ensure the configured regex isn't +// broader than expected. +func (s *Service) Match(line string) (bool, error) { + isMatch := strings.Contains(line, s.matchString) + + if isMatch { + s.count += 1 + } + + if s.count > s.expectedCount { + return isMatch, errors.Errorf("Too many matches. Have %d, expected %d. (on %s)", s.count, s.expectedCount, s.matchString) + } + + return isMatch, nil +} + +// Xml converts a Service resource to Xml suitable for embedding +func (s *Service) Xml(w io.Writer) error { + + enc := xml.NewEncoder(w) + enc.Indent(" ", " ") + if err := enc.Encode(s.serviceInstall); err != nil { + return err + } + if err := enc.Encode(s.serviceControl); err != nil { + return err + } + + if _, err := io.WriteString(w, "\n"); err != nil { + return err + } + + return nil + +} diff --git a/pkg/packagekit/wix/service_test.go b/pkg/packagekit/wix/service_test.go new file mode 100644 index 000000000..bf41e1285 --- /dev/null +++ b/pkg/packagekit/wix/service_test.go @@ -0,0 +1,83 @@ +package wix + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestService(t *testing.T) { + t.Parallel() + + service := NewService("daemon.exe") + + expectFalse, err := service.Match("nomatch") + require.NoError(t, err) + require.False(t, expectFalse) + + expectTrue, err := service.Match("daemon.exe") + require.NoError(t, err) + require.True(t, expectTrue) + + // Should error. count now exceeds expectedCount. + expectTrue2, err := service.Match("daemon.exe") + require.Error(t, err) + require.True(t, expectTrue2) + + expectedXml := ` + ` + + var xmlString bytes.Buffer + + err = service.Xml(&xmlString) + require.NoError(t, err) + require.Equal(t, expectedXml, strings.TrimSpace(xmlString.String())) + +} + +func TestServiceOptions(t *testing.T) { + t.Parallel() + + var tests = []struct { + in *Service + out string + }{ + { + in: NewService("daemon.exe", ServiceName("myDaemon")), + out: ` + `, + }, + { + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first"})), + out: ` + `, + }, + { + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first with spaces"})), + out: ` + `, + }, + + { + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second"})), + out: ` + `, + }, + + { + in: NewService("daemon.exe", ServiceName("myDaemon"), ServiceArgs([]string{"first", "second", "third has spaces"})), + out: ` + `, + }, + } + + for _, tt := range tests { + var xmlString bytes.Buffer + err := tt.in.Xml(&xmlString) + require.NoError(t, err) + require.Equal(t, tt.out, strings.TrimSpace(xmlString.String())) + } + +} diff --git a/pkg/packagekit/wix/wix.go b/pkg/packagekit/wix/wix.go index 4db193430..8204cee6d 100644 --- a/pkg/packagekit/wix/wix.go +++ b/pkg/packagekit/wix/wix.go @@ -17,29 +17,29 @@ import ( "github.com/pkg/errors" ) -type wixOptions struct { - wixPath string // Where is wix installed - packageRoot string // What's the root of the packaging files? - buildDir string // The wix tools want to work in a build dir. - msArch string // What's the microsoft archtecture name? - services []string // array of services. TBD - dockerImage string // If in docker, what image? - skipValidation bool // Skip light validation. Seems to be needed for running in 32bit wine environments. - cleanDirs []string // directories to rm on cleanup +type wixTool struct { + wixPath string // Where is wix installed + packageRoot string // What's the root of the packaging files? + buildDir string // The wix tools want to work in a build dir. + msArch string // What's the microsoft archtecture name? + services []*Service // array of services. + dockerImage string // If in docker, what image? + skipValidation bool // Skip light validation. Seems to be needed for running in 32bit wine environments. + cleanDirs []string // directories to rm on cleanup execCC func(context.Context, string, ...string) *exec.Cmd // Allows test overrides } -type WixOpt func(*wixOptions) +type WixOpt func(*wixTool) func As64bit() WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.msArch = "x64" } } func As32bit() WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.msArch = "x86" } } @@ -47,31 +47,31 @@ func As32bit() WixOpt { // If you're running this in a virtual win environment, you probably // need to skip validation. LGHT0216 is a common error. func SkipValidation() WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.skipValidation = true } } func WithWix(path string) WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.wixPath = path } } -func WithServices(service string) WixOpt { - return func(wo *wixOptions) { +func WithService(service *Service) WixOpt { + return func(wo *wixTool) { wo.services = append(wo.services, service) } } func WithBuildDir(path string) WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.buildDir = path } } func WithDocker(image string) WixOpt { - return func(wo *wixOptions) { + return func(wo *wixTool) { wo.dockerImage = image } @@ -80,8 +80,8 @@ func WithDocker(image string) WixOpt { // New takes a packageRoot of files, and a wxsContent of xml wix // configuration, and will return a struct with methods for building // packages with. -func New(packageRoot string, mainWxsContent []byte, wixOpts ...WixOpt) (*wixOptions, error) { - wo := &wixOptions{ +func New(packageRoot string, mainWxsContent []byte, wixOpts ...WixOpt) (*wixTool, error) { + wo := &wixTool{ wixPath: `C:\wix311`, packageRoot: packageRoot, @@ -125,7 +125,7 @@ func New(packageRoot string, mainWxsContent []byte, wixOpts ...WixOpt) (*wixOpti } // Cleanup removes temp directories. Meant to be called in a defer. -func (wo *wixOptions) Cleanup() { +func (wo *wixTool) Cleanup() { for _, d := range wo.cleanDirs { os.RemoveAll(d) } @@ -134,11 +134,15 @@ func (wo *wixOptions) Cleanup() { // Package will run through the wix steps to produce a resulting // package. This package will be written into the provided io.Writer, // facilitating export to a file, buffer, or other storage backends. -func (wo *wixOptions) Package(ctx context.Context, pkgOutput io.Writer) error { +func (wo *wixTool) Package(ctx context.Context, pkgOutput io.Writer) error { if err := wo.heat(ctx); err != nil { return errors.Wrap(err, "running heat") } + if err := wo.addServices(ctx); err != nil { + return errors.Wrap(err, "adding services") + } + if err := wo.candle(ctx); err != nil { return errors.Wrap(err, "running candle") } @@ -160,14 +164,63 @@ func (wo *wixOptions) Package(ctx context.Context, pkgOutput io.Writer) error { return nil } +// addServices adds service definitions into the wix configs. +// +// In wix parlence, these schema elements are _in_ the Component +// section, which is autogenerated by heat.exe. This presents a +// problem -- How do we mpdify that? We could manually curate the +// files list, we could pass heat an xslt transform, or we can +// post-process the wxs files. I've opted to post-process them. +// +// References: +// * http://windows-installer-xml-wix-toolset.687559.n2.nabble.com/Windows-Service-installation-td7601050.html +// * https://helgeklein.com/blog/2014/09/real-world-example-wix-msi-application-installer/ +func (wo *wixTool) addServices(ctx context.Context) error { + if len(wo.services) == 0 { + return nil + } + + heatFile := filepath.Join(wo.buildDir, "AppFiles.wxs") + heatContent, err := ioutil.ReadFile(heatFile) + if err != nil { + return errors.Wrap(err, "reading AppFiles.wxs") + } + + heatWrite, err := os.Create(heatFile) + if err != nil { + return errors.Wrap(err, "opening AppFiles.wxs for writing") + } + defer heatWrite.Close() + + lines := strings.Split(string(heatContent), "\n") + for _, line := range lines { + heatWrite.WriteString(line) + heatWrite.WriteString("\n") + for _, service := range wo.services { + isMatch, err := service.Match(line) + if err != nil { + return errors.Wrap(err, "match error") + } + if isMatch { + if err := service.Xml(heatWrite); err != nil { + return errors.Wrap(err, "adding service") + } + } + } + } + + return nil +} + // heat invokes wix's heat command. This examines a directory and // "harvests" the files into an xml structure. See // http://wixtoolset.org/documentation/manual/v3/overview/heat.html // // TODO split this into PROGDIR and DATADIR. Perhaps using options? Or // figuring out a way to invoke this multiple times with different dir -// and -cg settings. -func (wo *wixOptions) heat(ctx context.Context) error { +// and -cg settings. Historically this used PROGDIR, and I haven't dug +// into the auto-update code, so it's staying there for now. +func (wo *wixTool) heat(ctx context.Context) error { _, err := wo.execOut(ctx, filepath.Join(wo.wixPath, "heat.exe"), "dir", wo.packageRoot, @@ -178,7 +231,7 @@ func (wo *wixOptions) heat(ctx context.Context) error { "-ke", "-cg", "AppFiles", "-template", "fragment", - "-dr", "DATADIR", + "-dr", "PROGDIR", "-var", "var.SourceDir", "-out", "AppFiles.wxs", ) @@ -188,7 +241,7 @@ func (wo *wixOptions) heat(ctx context.Context) error { // candle invokes wix's candle command. This is the wix compiler, It // preprocesses and compiles WiX source files into object files // (.wixobj). -func (wo *wixOptions) candle(ctx context.Context) error { +func (wo *wixTool) candle(ctx context.Context) error { _, err := wo.execOut(ctx, filepath.Join(wo.wixPath, "candle.exe"), "-nologo", @@ -203,7 +256,7 @@ func (wo *wixOptions) candle(ctx context.Context) error { // light invokes wix's light command. This links and binds one or more // .wixobj files and creates a Windows Installer database (.msi or // .msm). See http://wixtoolset.org/documentation/manual/v3/overview/light.html for options -func (wo *wixOptions) light(ctx context.Context) error { +func (wo *wixTool) light(ctx context.Context) error { args := []string{ "-nologo", "-dcl:high", // compression level @@ -225,7 +278,7 @@ func (wo *wixOptions) light(ctx context.Context) error { } -func (wo *wixOptions) execOut(ctx context.Context, argv0 string, args ...string) (string, error) { +func (wo *wixTool) execOut(ctx context.Context, argv0 string, args ...string) (string, error) { logger := ctxlog.FromContext(ctx) dockerArgs := []string{ diff --git a/pkg/packagekit/wix/wix_test.go b/pkg/packagekit/wix/wix_test.go index f332a0e57..ed20ae224 100644 --- a/pkg/packagekit/wix/wix_test.go +++ b/pkg/packagekit/wix/wix_test.go @@ -52,6 +52,7 @@ func TestWixPackage(t *testing.T) { SkipValidation(), // wine can't validate WithDocker("felfert/wix"), // TODO Use a Kolide distributed Dockerfile WithWix("/opt/wix/bin"), + WithService(NewService("hello.txt")), ) require.NoError(t, err) defer wixTool.Cleanup() @@ -66,7 +67,7 @@ func TestWixPackage(t *testing.T) { // which can mostly read MSI files. func verifyMsi(ctx context.Context, t *testing.T, outMsi *os.File) { // Use the wix struct for its execOut - execWix := &wixOptions{execCC: exec.CommandContext} + execWix := &wixTool{execCC: exec.CommandContext} fileContents, err := execWix.execOut(ctx, "7z", "x", "-so", outMsi.Name()) require.NoError(t, err) diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index 9ca463eb3..ceeffbf15 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -92,59 +92,54 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar return errors.Wrap(err, "setup directories") } - launcherEnv := map[string]string{ - "KOLIDE_LAUNCHER_HOSTNAME": p.Hostname, - "KOLIDE_LAUNCHER_ROOT_DIRECTORY": p.rootDir, - "KOLIDE_LAUNCHER_OSQUERYD_PATH": filepath.Join(p.binDir, "osqueryd"), - "KOLIDE_LAUNCHER_ENROLL_SECRET_PATH": filepath.Join(p.confDir, "secret"), + flagFilePath := filepath.Join(p.confDir, "launcher.flags") + flagFile, err := os.Create(filepath.Join(p.packageRoot, flagFilePath)) + if err != nil { + return errors.Wrap(err, "creating flag file") + } + defer flagFile.Close() + + launcherMapFlags := map[string]string{ + "hostname": p.Hostname, + "root_directory": p.canonicalizePath(p.rootDir), + "osqueryd_path": p.canonicalizePath(filepath.Join(p.binDir, "osqueryd")), + "enroll_secret_path": p.canonicalizePath(filepath.Join(p.confDir, "secret")), } - launcherFlags := []string{} + launcherBoolFlags := []string{} if p.InitialRunner { - launcherFlags = append(launcherFlags, "--with_initial_runner") + launcherBoolFlags = append(launcherBoolFlags, "with_initial_runner") } if p.Control && p.ControlHostname != "" { - launcherEnv["KOLIDE_LAUNCHER_CONTROL_HOSTNAME"] = p.ControlHostname + launcherMapFlags["control_hostname"] = p.ControlHostname } if p.Autoupdate && p.UpdateChannel != "" { - launcherFlags = append(launcherFlags, "--autoupdate") - launcherEnv["KOLIDE_LAUNCHER_UPDATE_CHANNEL"] = p.UpdateChannel + launcherBoolFlags = append(launcherBoolFlags, "autoupdate") + launcherMapFlags["update_channel"] = p.UpdateChannel } if p.CertPins != "" { - launcherEnv["KOLIDE_LAUNCHER_CERT_PINS"] = p.CertPins + launcherMapFlags["cert_pins"] = p.CertPins } if p.DisableControlTLS { - launcherFlags = append(launcherFlags, "--disable_control_tls") + launcherBoolFlags = append(launcherBoolFlags, "disable_control_tls") } if p.InsecureGrpc { - launcherFlags = append(launcherFlags, "--insecure_grpc") + launcherBoolFlags = append(launcherBoolFlags, "insecure_grpc") } if p.Insecure { - launcherFlags = append(launcherFlags, "--insecure") - } - - // Unless we're omitting the secret, write it into the package. - // Note that we _always_ set KOLIDE_LAUNCHER_ENROLL_SECRET_PATH - if !p.OmitSecret { - if err := ioutil.WriteFile( - filepath.Join(p.packageRoot, p.confDir, "secret"), - []byte(p.Secret), - secretPerms, - ); err != nil { - return errors.Wrap(err, "could not write secret string to file for packaging") - } + launcherBoolFlags = append(launcherBoolFlags, "insecure") } if p.RootPEM != "" { rootPemPath := filepath.Join(p.confDir, "roots.pem") - launcherEnv["KOLIDE_LAUNCHER_ROOT_PEM"] = rootPemPath + launcherMapFlags["root_pem"] = p.canonicalizePath(rootPemPath) if err := fs.CopyFile(p.RootPEM, filepath.Join(p.packageRoot, rootPemPath)); err != nil { return errors.Wrap(err, "copy root PEM") @@ -155,6 +150,34 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar } } + // Write the flags to the flagFile + for _, k := range launcherBoolFlags { + if _, err := flagFile.WriteString(fmt.Sprintf("%s\n", k)); err != nil { + return errors.Wrapf(err, "failed to write write %s to flagfile", k) + } + } + for k, v := range launcherMapFlags { + if _, err := flagFile.WriteString(fmt.Sprintf("%s %s\n", k, v)); err != nil { + return errors.Wrapf(err, "failed to write write %s to flagfile", k) + } + } + + // Wixtoolset seems to get unhappy if the flagFile is open, and since + // we're done writing it, may as well close it. + flagFile.Close() + + // Unless we're omitting the secret, write it into the package. + // Note that we _always_ set KOLIDE_LAUNCHER_ENROLL_SECRET_PATH + if !p.OmitSecret { + if err := ioutil.WriteFile( + filepath.Join(p.packageRoot, p.confDir, "secret"), + []byte(p.Secret), + secretPerms, + ); err != nil { + return errors.Wrap(err, "could not write secret string to file for packaging") + } + } + // Install binaries into packageRoot // TODO parallization, osquery-extension.ext // TODO windows file extensions @@ -199,8 +222,8 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar Description: "The Kolide Launcher", Path: filepath.Join(p.binDir, "launcher"), Identifier: p.Identifier, - Flags: launcherFlags, - Environment: launcherEnv, + Flags: []string{"-config", flagFilePath}, + Environment: map[string]string{}, } if err := p.setupInit(ctx); err != nil { @@ -222,6 +245,7 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar Scripts: p.scriptRoot, SigningKey: p.SigningKey, Version: p.PackageVersion, + FlagFile: p.canonicalizePath(flagFilePath), } if err := p.makePackage(ctx); err != nil { @@ -288,7 +312,9 @@ func (p *PackageOptions) makePackage(ctx context.Context) error { return errors.Wrapf(err, "packaging, target %s", p.target.String()) } case p.target.Package == Msi: - if err := packagekit.PackageWixMSI(ctx, p.packageWriter, p.packagekitops); err != nil { + // pass whether to include a service as a bool argument to PackageWixMSI + includeService := p.target.Init == WindowsService + if err := packagekit.PackageWixMSI(ctx, p.packageWriter, p.packagekitops, includeService); err != nil { return errors.Wrapf(err, "packaging, target %s", p.target.String()) } default: @@ -334,6 +360,11 @@ func (p *PackageOptions) renderNewSyslogConfig(ctx context.Context) error { return nil } +// setupInit setups the init scripts. +// +// Note that windows is a special +// case here -- they're not files on disk, instead it's an argument +// passed in to wix. So this is a confusing split. func (p *PackageOptions) setupInit(ctx context.Context) error { if p.target.Init == NoInit { return nil @@ -367,6 +398,9 @@ func (p *PackageOptions) setupInit(ctx context.Context) error { renderFunc = func(ctx context.Context, w io.Writer, io *packagekit.InitOptions) error { return packagekit.RenderUpstart(ctx, w, io) } + case p.target.Platform == Windows && p.target.Init == WindowsService: + // Do nothing, this is handled in the packaging step. + return nil default: return errors.Errorf("Unsupported target %s", p.target.String()) } @@ -555,13 +589,11 @@ func (p *PackageOptions) setupDirectories() error { case Windows: // On Windows, these paths end up rooted not at `c:`, but instead // where the WiX template says. In our case, that's `c:\Program - // Files\Kolide` These do need the identigier, since we need WiX + // Files\Kolide` These do need the identifier, since we need WiX // to take that into account for the guid generation. - // - //FIXME what should these be? p.binDir = filepath.Join("Launcher-"+p.Identifier, "bin") p.confDir = filepath.Join("Launcher-"+p.Identifier, "conf") - p.rootDir = filepath.Join("Launcher-"+p.Identifier, "data", sanitizeHostname(p.Hostname)) + p.rootDir = filepath.Join("Launcher-"+p.Identifier, "data") default: return errors.Errorf("Unknown platform %s", string(p.target.Platform)) } @@ -607,5 +639,26 @@ func (p *PackageOptions) execOut(ctx context.Context, argv0 string, args ...stri return "", errors.Wrapf(err, "run command %s %v, stderr=%s", argv0, args, stderr) } return strings.TrimSpace(stdout.String()), nil +} + +// canonicalizePath takes a path, and makes it into a full, absolute, +// path. It is a hack around how the windows install process works, +// and will likely need to be revisited. +// +// The windows process installs using _relative_ paths, which are +// expanded to full paths inside the wix template. However, the flag +// file needs full paths, and is generated here. Thus, +// canonicalizePath encodes some things that should be left as +// install-time variables controlled by wix and windows. +// +// Likely a longer term approach will involve one of: +// 1. pull all the paths into the golang portion. +// 2. Move flag file generation to wix +// 3. utilize some environmental variable +func (p *PackageOptions) canonicalizePath(path string) string { + if p.target.Package != Msi { + return path + } + return filepath.Join(`C:\Program Files\Kolide`, path) } diff --git a/pkg/packaging/target.go b/pkg/packaging/target.go index e3dc81aa5..e31f43fb2 100644 --- a/pkg/packaging/target.go +++ b/pkg/packaging/target.go @@ -18,11 +18,12 @@ type Target struct { type InitFlavor string const ( - LaunchD InitFlavor = "launchd" - SystemD = "systemd" - Init = "init" - Upstart = "upstart" - NoInit = "none" + LaunchD InitFlavor = "launchd" + SystemD = "systemd" + Init = "init" + Upstart = "upstart" + WindowsService = "service" + NoInit = "none" ) type PlatformFlavor string @@ -79,6 +80,9 @@ func (t *Target) PkgExtension() string { // PlatformExtensionName is a helper to return the platform specific extension name. func (t *Target) PlatformExtensionName(input string) string { + // Remove suffixes. This is order dependand, so slightly fragile. + input = strings.TrimSuffix(input, ".ext") + input = strings.TrimSuffix(input, ".exe") if t.Platform == Windows { return input + ".exe" } else { @@ -88,6 +92,9 @@ func (t *Target) PlatformExtensionName(input string) string { // PlatformBinaryName is a helper to return the platform specific binary suffix. func (t *Target) PlatformBinaryName(input string) string { + // remove trailing .exe + input = strings.TrimSuffix(input, ".exe") + if t.Platform == Windows { return input + ".exe" } @@ -96,7 +103,7 @@ func (t *Target) PlatformBinaryName(input string) string { // InitFromString sets a target's init flavor from string representation func (t *Target) InitFromString(s string) error { - for _, testInit := range []InitFlavor{LaunchD, SystemD, Init, Upstart, NoInit} { + for _, testInit := range []InitFlavor{LaunchD, SystemD, Init, Upstart, NoInit, WindowsService} { if testInit.String() == s { t.Init = testInit return nil diff --git a/pkg/packaging/target_test.go b/pkg/packaging/target_test.go index ccab50bc0..401eb6fed 100644 --- a/pkg/packaging/target_test.go +++ b/pkg/packaging/target_test.go @@ -1,6 +1,7 @@ package packaging import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -82,7 +83,7 @@ func TestPlatformFromString(t *testing.T) { } } -func TestPackageFromString(t *testing.T) { +func TestPackageStrings(t *testing.T) { t.Parallel() var tests = []struct { @@ -120,7 +121,10 @@ func TestPackageFromString(t *testing.T) { target := &Target{} err := target.PackageFromString(tt.in) require.NoError(t, err) - require.Equal(t, string(tt.out), string(target.Package)) + require.Equal(t, tt.out, target.Package) + + // Check the reversal as well. + require.Equal(t, tt.in, target.PkgExtension()) } } @@ -165,3 +169,71 @@ func TestTargetParse(t *testing.T) { } } } + +func TestTargetPlatformBinaryName(t *testing.T) { + t.Parallel() + + var tests = []struct { + in string + out string + outwin string + }{ + { + in: "foo", + out: "foo", + outwin: "foo.exe", + }, + { + in: "foo.app", + out: "foo.app", + outwin: "foo.app.exe", + }, + { + in: "foo.exe", + out: "foo", + outwin: "foo.exe", + }, + } + + target := &Target{Platform: Darwin, Init: LaunchD, Package: Pkg} + targetWin := &Target{Platform: Windows, Init: NoInit, Package: Msi} + + for _, tt := range tests { + require.Equal(t, tt.out, target.PlatformBinaryName(tt.in), fmt.Sprintf("Test: %s", tt.in)) + require.Equal(t, tt.outwin, targetWin.PlatformBinaryName(tt.in), fmt.Sprintf("Test: %s", tt.in)) + } +} +func TestTargetPlatformExtensionName(t *testing.T) { + t.Parallel() + + var tests = []struct { + in string + out string + outwin string + }{ + { + in: "foo", + out: "foo.ext", + outwin: "foo.exe", + }, + { + in: "foo.ext", + out: "foo.ext", + outwin: "foo.exe", + }, + { + in: "foo.exe", + out: "foo.ext", + outwin: "foo.exe", + }, + } + + target := &Target{Platform: Darwin, Init: LaunchD, Package: Pkg} + targetWin := &Target{Platform: Windows, Init: NoInit, Package: Msi} + + for _, tt := range tests { + require.Equal(t, tt.out, target.PlatformExtensionName(tt.in), fmt.Sprintf("Test: %s", tt.in)) + require.Equal(t, tt.outwin, targetWin.PlatformExtensionName(tt.in), tt.in, fmt.Sprintf("Test: %s", tt.in)) + } + +}