From b591e2ef5a6da7f8fbee099c25349dd61433b297 Mon Sep 17 00:00:00 2001 From: Randall Winkhart Date: Wed, 17 Dec 2025 00:36:20 -0500 Subject: [PATCH] Switch config format from ini to json (drop ini dependency since json is now used for syncing) --- cfg/cfg.go | 82 ++++++++++++++++++++++++++++++++++ cfg/configParser.go | 98 ----------------------------------------- cfg/umask_UNIX.go | 12 ----- cfg/umask_WIN.go | 8 ---- core/init.go | 54 +++++++++++++---------- global/2globals_UNIX.go | 2 +- global/2globals_WIN.go | 2 +- go.mod | 1 - go.sum | 2 - syncclient/client.go | 60 +++++++------------------ syncclient/oneOff.go | 16 +++---- 11 files changed, 138 insertions(+), 199 deletions(-) create mode 100644 cfg/cfg.go delete mode 100644 cfg/configParser.go delete mode 100644 cfg/umask_UNIX.go delete mode 100644 cfg/umask_WIN.go diff --git a/cfg/cfg.go b/cfg/cfg.go new file mode 100644 index 0000000..68cb9f2 --- /dev/null +++ b/cfg/cfg.go @@ -0,0 +1,82 @@ +package cfg + +import ( + "encoding/json" + "errors" + "os" + "reflect" + + "github.com/rwinkhart/libmutton/global" +) + +type CfgT struct { + Libmutton struct { + OfflineMode *bool `json:"offlineMode"` + SSHUser *string `json:"sshUser"` + SSHIP *string `json:"sshIP"` + SSHPort *string `json:"sshPort"` + SSHEntryRootPath *string `json:"sshEntryRootPath"` + SSHAgeDirPath *string `json:"sshAgeDirPath"` + SSHKeyPath *string `json:"sshKeyPath"` + SSHKeyProtected *bool `json:"sshKeyProtected"` + SSHIsWindows *bool `json:"sshIsWindows"` + } `json:"libmutton"` + ThirdParty *map[string]any `json:"thirdParty"` +} + +// LoadConfig loads libmuttoncfg.json and returns the configuration. +func LoadConfig() (*CfgT, error) { + cfgBytes, err := os.ReadFile(global.ConfigPath) + if err != nil { + return nil, errors.New("unable to load libmuttoncfg.json: " + err.Error()) + } + var cfg CfgT + err = json.Unmarshal(cfgBytes, &cfg) + if err != nil { + return nil, errors.New("unable to unmarshal libmuttoncfg.json: " + err.Error()) + } + return &cfg, nil +} + +// WriteConfig writes cfg to libmuttoncfg.json. +// If used in append mode, any nil values in the +// input cfg will be substituted with the existing values. +func WriteConfig(cfg *CfgT, appendMode bool) error { + if appendMode { + // check if any fields are nil + var hasNilFields bool + cfgValue := reflect.ValueOf(&cfg.Libmutton).Elem() + for i := 0; i < cfgValue.NumField(); i++ { + field := cfgValue.Field(i) + if field.IsNil() { + hasNilFields = true + break + } + } + + // load old cfg and copy nil fields + if hasNilFields { + oldCfg, err := LoadConfig() + if err != nil { + return err + } + oldValue := reflect.ValueOf(&oldCfg.Libmutton).Elem() + for i := 0; i < cfgValue.NumField(); i++ { + field := cfgValue.Field(i) + if field.IsNil() { + field.Set(oldValue.Field(i)) + } + } + } + } + cfgBytes, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return errors.New("unable to marshal new/updated cfg: " + err.Error()) + } + err = os.WriteFile(global.ConfigPath, cfgBytes, 0600) + if err != nil { + return errors.New("unable to write new/updated cfg to libmuttoncfg.json: " + err.Error()) + } + + return nil +} diff --git a/cfg/configParser.go b/cfg/configParser.go deleted file mode 100644 index e8eaf75..0000000 --- a/cfg/configParser.go +++ /dev/null @@ -1,98 +0,0 @@ -package cfg - -import ( - "errors" - "fmt" - - "github.com/rwinkhart/libmutton/global" - "gopkg.in/ini.v1" -) - -// loadConfig loads the libmutton.ini file and returns the configuration. -// It is a utility function for ParseConfig and WriteConfig; do not call directly. -func loadConfig() (*ini.File, error) { - cfg, err := ini.Load(global.ConfigPath) - if err != nil { - return nil, errors.New("unable to load libmutton.ini: " + err.Error()) - } - return cfg, nil -} - -// ParseConfig reads the libmutton.ini file and returns a slice of values for the specified keys. -// Requires: valuesRequested (a slice of length 2 arrays each containing a section and a key name). -// Returns: config (slice of values for the specified keys). -// If requesting SSH config, request "LIBMUTTON/offlineMode" first to avoid errors. -func ParseConfig(valuesRequested [][2]string) ([]string, error) { - cfg, err := loadConfig() - if err != nil { - return nil, err - } - - var config []string - for _, pair := range valuesRequested { - value := cfg.Section(pair[0]).Key(pair[1]).String() - // ensure specified key has a value - if value == "" { - return nil, fmt.Errorf("unable to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0]) - } - config = append(config, value) - - // notify requester immediately if in offline mode - if pair[1] == "offlineMode" && pair[0] == "LIBMUTTON" && value == "true" { - return config, nil - } - } - - return config, err -} - -// WriteConfig writes the provided key-value pairs under the specified section headers in the libmutton.ini file. -// Requires: valuesToWrite (a slice of length 3 arrays each containing a section, a key name, and a value), -// prune (a slice similar to valuesToWrite to allow removing the specified keys from an existing config), -// append (set to true to append to the existing libmutton.ini file, false to overwrite it). -func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool) error { - var cfg *ini.File - var err error - - if append { - // load existing ini file - cfg, err = loadConfig() - if err != nil { - return errors.New("unable to load existing libmutton.ini: " + err.Error()) - } - } else { - // create empty ini container - cfg = ini.Empty() - } - - // set all specified key-value pairs in their respective sections - var section *ini.Section - for _, trio := range valuesToWrite { - if cfg.Section(trio[0]) == nil { - // create and acquire section if it doesn't exist - section, _ = cfg.NewSection(trio[0]) - } else { - // acquire existing section - section = cfg.Section(trio[0]) - } - - // set key-value pair - section.Key(trio[1]).SetValue(trio[2]) - } - - // prune specified keys from the existing config - if append && len(keysToPrune) > 0 { - // remove specified keys pairs from the existing config - for _, pair := range keysToPrune { - cfg.Section(pair[0]).DeleteKey(pair[1]) - } - } - - // save to libmutton.ini - setUmask(0077) // only give permissions to owner - err = cfg.SaveTo(global.ConfigPath) - if err != nil { - return errors.New("unable to save libmutton.ini: " + err.Error()) - } - return nil -} diff --git a/cfg/umask_UNIX.go b/cfg/umask_UNIX.go deleted file mode 100644 index 0ee5007..0000000 --- a/cfg/umask_UNIX.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package cfg - -import "syscall" - -// setUmask sets the file mode creation mask (umask) for the current process. -// The call to syscall.Umask needs to be embedded in another function to allow -// compilation on Windows. -func setUmask(umask int) { - syscall.Umask(umask) -} diff --git a/cfg/umask_WIN.go b/cfg/umask_WIN.go deleted file mode 100644 index 9c1361a..0000000 --- a/cfg/umask_WIN.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build windows - -package cfg - -// setUmask is a dummy function on Windows. -func setUmask(umask int) { - return -} diff --git a/core/init.go b/core/init.go index e751fab..fd7e2f6 100644 --- a/core/init.go +++ b/core/init.go @@ -3,7 +3,7 @@ package core import ( "cmp" "errors" - "strconv" + "maps" "strings" "github.com/rwinkhart/go-boilerplate/back" @@ -15,7 +15,15 @@ import ( // LibmuttonInit creates the libmutton config structure based on user input. // rcwPassword and clientSpecificIniData can be left blank if not needed. -func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][3]string, rcwPassword []byte, preserveOldConfigDir bool, forceOfflineMode bool) error { +func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData map[string]any, rcwPassword []byte, preserveOldConfigDir bool, forceOfflineMode bool) error { + // handle clientSpecificIniData + newCfg := &cfg.CfgT{} + if clientSpecificIniData != nil { + newThirdPartyMap := make(map[string]any) + maps.Copy(newThirdPartyMap, clientSpecificIniData) + newCfg.ThirdParty = &newThirdPartyMap + } + var r string if !forceOfflineMode { r = strings.ToLower(inputCB("Configure SSH settings (for synchronization)? (Y/n)")) @@ -28,14 +36,13 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ if err != nil { return errors.New("unable to initialize libmutton directories: " + err.Error()) } + // write config file - if len(clientSpecificIniData) > 0 { - err = cfg.WriteConfig(append(clientSpecificIniData, [][3]string{{"LIBMUTTON", "offlineMode", "true"}}...), nil, false) - } else { - err = cfg.WriteConfig([][3]string{{"LIBMUTTON", "offlineMode", "true"}}, nil, false) - } + offlineMode := true + newCfg.Libmutton.OfflineMode = &offlineMode + err = cfg.WriteConfig(newCfg, false) if err != nil { - return errors.New("unable to write config file: " + err.Error()) + return err } } else { // ensure ssh key file exists (and is a file) @@ -63,30 +70,29 @@ func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][ return errors.New("unable to initialize libmutton directories: " + err.Error()) } //// write config file - //// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration - err = cfg.WriteConfig(append( - clientSpecificIniData, - [][3]string{ - {"LIBMUTTON", "offlineMode", "false"}, - {"LIBMUTTON", "sshUser", sshUser}, - {"LIBMUTTON", "sshIP", sshIP}, - {"LIBMUTTON", "sshPort", sshPort}, - {"LIBMUTTON", "sshKey", sshKeyPath}, - {"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)}, - {"LIBMUTTON", "sshEntryRoot", "null"}, - {"LIBMUTTON", "sshAgeDir", "null"}, - {"LIBMUTTON", "sshIsWindows", "false"}}...), nil, false) + //// temporarily leave sshEntryRoot, sshAgeDir, and sshIsWindows as nil to pass initial device ID registration + newCfg.Libmutton.OfflineMode = &forceOfflineMode // forceOfflineMode must be false to reach this point, so we can avoid the extra declaration + newCfg.Libmutton.SSHUser = &sshUser + newCfg.Libmutton.SSHIP = &sshIP + newCfg.Libmutton.SSHPort = &sshPort + newCfg.Libmutton.SSHKeyPath = &sshKeyPath + newCfg.Libmutton.SSHKeyProtected = &sshKeyProtected + err = cfg.WriteConfig(newCfg, false) if err != nil { - return errors.New("unable to write config file: " + err.Error()) + return err } // generate and register device ID sshEntryRoot, sshAgeDir, sshIsWindows, err := syncclient.GenDeviceID(oldDeviceID, "") if err != nil { return errors.New("unable to generate device ID: " + err.Error()) } - err = cfg.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshAgeDir", sshAgeDir}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, nil, true) + // update config file + newCfg.Libmutton.SSHEntryRootPath = &sshEntryRoot + newCfg.Libmutton.SSHAgeDirPath = &sshAgeDir + newCfg.Libmutton.SSHIsWindows = &sshIsWindows + err = cfg.WriteConfig(newCfg, true) if err != nil { - return errors.New("unable to write config file: " + err.Error()) + return err } } // generate rcw sanity check file (if requested) diff --git a/global/2globals_UNIX.go b/global/2globals_UNIX.go index a85d6eb..cae30d4 100644 --- a/global/2globals_UNIX.go +++ b/global/2globals_UNIX.go @@ -7,7 +7,7 @@ import "github.com/rwinkhart/go-boilerplate/back" var ( EntryRoot = back.Home + "/.local/share/libmutton" // Path to libmutton entry directory ConfigDir = back.Home + "/.config/libmutton" // Path to libmutton configuration directory - ConfigPath = ConfigDir + "/libmutton.ini" // Path to libmutton configuration file + ConfigPath = ConfigDir + "/libmuttoncfg.json" // Path to libmutton configuration file AgeDir = ConfigDir + "/age" // Path to libmutton password age directory ) diff --git a/global/2globals_WIN.go b/global/2globals_WIN.go index c90dd29..8a3f678 100644 --- a/global/2globals_WIN.go +++ b/global/2globals_WIN.go @@ -7,7 +7,7 @@ import "github.com/rwinkhart/go-boilerplate/back" var ( EntryRoot = back.Home + "\\AppData\\Local\\libmutton\\entries" // Path to libmutton entry directory ConfigDir = back.Home + "\\AppData\\Local\\libmutton\\config" // Path to libmutton configuration directory - ConfigPath = ConfigDir + "\\libmutton.ini" // Path to libmutton configuration file + ConfigPath = ConfigDir + "\\libmuttoncfg.json" // Path to libmutton configuration file AgeDir = ConfigDir + "\\age" // Path to libmutton password age directory ) diff --git a/go.mod b/go.mod index a1536be..d809d94 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/rwinkhart/go-boilerplate v0.1.1-0.20251211171453-df1fb21b2366 github.com/rwinkhart/rcw v0.2.3 golang.org/x/crypto v0.45.0 - gopkg.in/ini.v1 v1.67.0 ) require ( diff --git a/go.sum b/go.sum index 7d58b2f..4126eae 100644 --- a/go.sum +++ b/go.sum @@ -30,7 +30,5 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/syncclient/client.go b/syncclient/client.go index f3a4225..0ca96a5 100644 --- a/syncclient/client.go +++ b/syncclient/client.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strconv" "strings" "time" @@ -26,70 +25,43 @@ import ( // sshEntryRoot (the root directory for entries on the remote server), // sshAgeDir (the directory housing age files on the remote server), // Only supports key-based authentication (passwords are supported for CLI-based implementations). -func GetSSHClient() (*ssh.Client, bool, bool, string, string, error) { +func GetSSHClient() (*ssh.Client, bool, *bool, *string, *string, error) { // get SSH config info - sshUserConfig, err := cfg.ParseConfig([][2]string{{"LIBMUTTON", "offlineMode"}, {"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshAgeDir"}, {"LIBMUTTON", "sshIsWindows"}}) - if len(sshUserConfig) == 1 { - // offline mode is enabled - return nil, true, false, "", "", nil - } + cfg, err := cfg.LoadConfig() if err != nil { - return nil, false, false, "", "", errors.New("unable to parse SSH config: " + err.Error()) + return nil, false, nil, nil, nil, errors.New("unable to parse SSH config: " + err.Error()) } - - var user, ip, port, keyFile, keyFileProtected, entryRoot, ageDir string - var isWindows bool - for i, key := range sshUserConfig { - switch i { - case 1: - user = key - case 2: - ip = key - case 3: - port = key - case 4: - keyFile = key - case 5: - keyFileProtected = key - case 6: - entryRoot = key - case 7: - ageDir = key - case 8: - isWindows, err = strconv.ParseBool(key) - if err != nil { - return nil, false, false, "", "", errors.New("unable to parse server OS type: " + err.Error()) - } - } + if *cfg.Libmutton.OfflineMode { + return nil, true, nil, nil, nil, nil } // read private key - key, err := os.ReadFile(keyFile) + key, err := os.ReadFile(*cfg.Libmutton.SSHKeyPath) if err != nil { - return nil, false, false, "", "", errors.New("unable to read private key: " + keyFile) + return nil, false, nil, nil, nil, errors.New("unable to read private key: " + *cfg.Libmutton.SSHKeyPath) } // parse private key var parsedKey ssh.Signer - if keyFileProtected != "true" { + if !*cfg.Libmutton.SSHKeyProtected { parsedKey, err = ssh.ParsePrivateKey(key) } else { parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassword("Enter password for your SSH keyfile:")) } if err != nil { - return nil, false, false, "", "", errors.New("unable to parse private key: " + keyFile) + return nil, false, nil, nil, nil, errors.New("unable to parse private key: " + *cfg.Libmutton.SSHKeyPath) } // read known hosts file var hostKeyCallback ssh.HostKeyCallback hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts") if err != nil { - return nil, false, false, "", "", errors.New("unable to read known hosts file: " + err.Error()) + return nil, false, nil, nil, nil, errors.New("unable to read known hosts file: " + err.Error()) } // configure SSH client sshConfig := &ssh.ClientConfig{ - User: user, + User: *cfg.Libmutton.SSHUser, Auth: []ssh.AuthMethod{ ssh.PublicKeys(parsedKey), }, @@ -98,12 +70,12 @@ func GetSSHClient() (*ssh.Client, bool, bool, string, string, error) { } // connect to SSH server - sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig) + sshClient, err := ssh.Dial("tcp", *cfg.Libmutton.SSHIP+":"+*cfg.Libmutton.SSHPort, sshConfig) if err != nil { - return nil, false, false, "", "", errors.New("unable to connect to remote server: " + err.Error()) + return nil, false, nil, nil, nil, errors.New("unable to connect to remote server: " + err.Error()) } - return sshClient, false, isWindows, entryRoot, ageDir, nil + return sshClient, false, cfg.Libmutton.SSHIsWindows, cfg.Libmutton.SSHEntryRootPath, cfg.Libmutton.SSHAgeDirPath, nil } // GetSSHOutput runs a command over SSH and returns the output as a string. @@ -546,7 +518,7 @@ func RunJob(returnLists bool) ([3][]string, error) { // sync new and updated entries var lists [3][]string if returnLists { - lists, err = syncLists(sshClient, sshEntryRoot, sshAgeDir, sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap) + lists, err = syncLists(sshClient, *sshEntryRoot, *sshAgeDir, *sshIsWindows, timeSynced, true, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap) if err != nil { return [3][]string{nil, nil, nil}, errors.New("unable to sync entries: " + err.Error()) } @@ -558,7 +530,7 @@ func RunJob(returnLists bool) ([3][]string, error) { } return lists, nil } - _, err = syncLists(sshClient, sshEntryRoot, sshAgeDir, sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap) + _, err = syncLists(sshClient, *sshEntryRoot, *sshAgeDir, *sshIsWindows, timeSynced, false, localEntryModMap, remoteEntryModMap, localAgeTimestampMap, remoteAgeTimestampMap) if err != nil { return [3][]string{nil, nil, nil}, errors.New("unable to sync entries: " + err.Error()) } diff --git a/syncclient/oneOff.go b/syncclient/oneOff.go index 105c5c7..4ae6f7b 100644 --- a/syncclient/oneOff.go +++ b/syncclient/oneOff.go @@ -167,7 +167,7 @@ end: // Device IDs are guaranteed unique as the current UNIX time is appended to them. // Leave prefix empty to use the current hostname as the prefix. // Returns: the remote EntryRoot, the remote AgeDir, and OS type indicator. -func GenDeviceID(oldDeviceID, prefix string) (string, string, string, error) { +func GenDeviceID(oldDeviceID, prefix string) (string, string, bool, error) { // generate new device ID if prefix == "" { prefix, _ = os.Hostname() @@ -179,7 +179,7 @@ func GenDeviceID(oldDeviceID, prefix string) (string, string, string, error) { oldDeviceIDPath := global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID f, err := os.OpenFile(newDeviceIDPath, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { - return "", "", "", errors.New("unable to create local device ID file: " + err.Error()) + return "", "", false, errors.New("unable to create local device ID file: " + err.Error()) } _ = f.Close() // error ignored; if the file could be created, it can probably be closed @@ -201,22 +201,22 @@ func GenDeviceID(oldDeviceID, prefix string) (string, string, string, error) { sshClient, _, _, _, _, err := GetSSHClient() if err != nil { cleanupOnFail() - return "", "", "", errors.New("unable to connect to SSH client: " + err.Error()) + return "", "", false, errors.New("unable to connect to SSH client: " + err.Error()) } output, err := GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID) if err != nil { cleanupOnFail() - return "", "", "", errors.New("unable to register device ID with server: " + err.Error()) + return "", "", false, errors.New("unable to register device ID with server: " + err.Error()) } var registerResp synccommon.RegisterResp err = json.Unmarshal(output, ®isterResp) if err != nil { cleanupOnFail() - return "", "", "", errors.New("unable to unmarshal server register response: " + err.Error()) + return "", "", false, errors.New("unable to unmarshal server register response: " + err.Error()) } if registerResp.ErrMsg != nil { cleanupOnFail() - return "", "", "", errors.New("unable to complete register; server-side error occurred: " + strings.ReplaceAll(*registerResp.ErrMsg, global.FSSpace, "\n")) + return "", "", false, errors.New("unable to complete register; server-side error occurred: " + strings.ReplaceAll(*registerResp.ErrMsg, global.FSSpace, "\n")) } _ = sshClient.Close() // ignore error; non-critical/unlikely/not much could be done about it @@ -224,8 +224,8 @@ func GenDeviceID(oldDeviceID, prefix string) (string, string, string, error) { err = os.RemoveAll(oldDeviceIDPath) if err != nil { cleanupOnFail() - return "", "", "", errors.New("unable to remove old device ID file (locally): " + err.Error()) + return "", "", false, errors.New("unable to remove old device ID file (locally): " + err.Error()) } - return registerResp.EntryRoot, registerResp.AgeDir, strconv.FormatBool(registerResp.IsWindows), nil + return registerResp.EntryRoot, registerResp.AgeDir, registerResp.IsWindows, nil }