Do not rely on importing projects for libmutton initialization

This commit is contained in:
2025-05-29 23:35:50 +00:00
parent 50ab27c674
commit 74b8261bc8
11 changed files with 185 additions and 104 deletions
-100
View File
@@ -1,100 +0,0 @@
package core
import (
"fmt"
"github.com/rwinkhart/go-boilerplate/back"
"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 {
cfg, err := ini.Load(global.ConfigPath)
if err != nil {
back.PrintError("Failed to load libmutton.ini: "+err.Error(), back.ErrorRead, true)
}
return cfg
}
// 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),
// missingValueError (an error message to display if a key is missing a value, set to "" for auto-generated or "0" to exit/return silently with code 0).
// Returns: config (slice of values for the specified keys),
// error (nil if no error occurred, otherwise an error using the generated or provided message).
func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]string, error) {
var err error
cfg := loadConfig()
var config []string
for _, pair := range valuesRequested {
value := cfg.Section(pair[0]).Key(pair[1]).String()
// ensure specified key has a value
if value == "" {
switch missingValueError {
case "":
err = fmt.Errorf("Failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
case "0":
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
default:
err = fmt.Errorf("%s", missingValueError)
}
back.PrintError(err.Error(), back.ErrorRead, false)
// if interactive (soft exit), return nil and the error to be handled by the caller
return nil, err
}
config = append(config, value)
}
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) {
var cfg *ini.File
if append {
// load existing ini file
cfg = loadConfig()
} 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 aquire 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 {
back.PrintError("Failed to save libmutton.ini: "+err.Error(), back.ErrorWrite, true)
}
}
+66 -5
View File
@@ -1,15 +1,76 @@
package core
import (
"cmp"
"errors"
"strconv"
"strings"
"github.com/rwinkhart/go-boilerplate/back"
"github.com/rwinkhart/libmutton/cfg"
"github.com/rwinkhart/libmutton/global"
"github.com/rwinkhart/libmutton/synccycles"
"github.com/rwinkhart/rcw/wrappers"
)
// RCWSanityCheckGen generates the RCW sanity check file for libmutton.
func RCWSanityCheckGen(passphrase []byte) {
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", passphrase)
if err != nil {
back.PrintError("Failed to generate sanity check file: "+err.Error(), back.ErrorWrite, true)
// LibmuttonInit creates the libmutton config structure based on user input.
// rcwPassphrase and clientSpecificIniData are cab be left blank if not needed.
func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][3]string, rcwPassphrase []byte, preserveOldConfigDir bool) error {
r := strings.ToLower(inputCB("Configure SSH settings (for synchronization)? (y/N)"))
if len(r) > 0 && r[0] == 'y' {
// ensure ssh key file exists
fallbackSSHKey := back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "id_ed25519"
sshKeyPath := cmp.Or(back.ExpandPathWithHome(inputCB(back.AnsiBold+"Note:"+back.AnsiReset+" Only key-based authentication is supported (keys may optionally be passphrase-protected).\n The remote server must already be in your ~"+global.PathSeparator+".ssh"+global.PathSeparator+"known_hosts file.\n\nSSH private identity file path (falls back to \""+fallbackSSHKey+"\"):")), fallbackSSHKey)
sshKeyIsFile, _ := back.TargetIsFile(sshKeyPath, false, 0)
if !sshKeyIsFile {
return errors.New("ssh identity file not found: " + sshKeyPath)
}
// get other ssh info from user
var sshKeyProtected bool
r = strings.ToLower(inputCB("Is the identity file password-protected? (y/N)"))
if len(r) > 0 && r[0] == 'y' {
sshKeyProtected = true
}
sshUser := inputCB("Remote SSH username:")
sshIP := inputCB("Remote SSH IP/domain:")
sshPort := inputCB("Remote SSH port:")
// perform operations based on collected user input
//// initialize libmutton directories
oldDeviceID := global.DirInit(preserveOldConfigDir)
//// write config file
//// temporarily assign sshEntryRoot and sshIsWindows to null to pass initial device ID registration
cfg.WriteConfig(append(
clientSpecificIniData,
[][3]string{
{"LIBMUTTON", "sshUser", sshUser},
{"LIBMUTTON", "sshIP", sshIP},
{"LIBMUTTON", "sshPort", sshPort},
{"LIBMUTTON", "sshKey", sshKeyPath},
{"LIBMUTTON", "sshKeyProtected", strconv.FormatBool(sshKeyProtected)},
{"LIBMUTTON", "sshEntryRoot", "null"},
{"LIBMUTTON", "sshIsWindows", "false"}}...), nil, false)
// generate and register device ID
sshEntryRoot, sshIsWindows, err := synccycles.DeviceIDGen(oldDeviceID)
if err != nil {
return errors.New("failed to generate device ID: " + err.Error())
}
cfg.WriteConfig([][3]string{{"LIBMUTTON", "sshEntryRoot", sshEntryRoot}, {"LIBMUTTON", "sshIsWindows", sshIsWindows}}, nil, true)
} else {
// initialize libmutton directories
global.DirInit(preserveOldConfigDir)
// write config file
if len(clientSpecificIniData) > 0 { // TODO test passing empty clientSpecificIniData
cfg.WriteConfig(clientSpecificIniData, nil, false)
}
}
// generate rcw sanity check file (if requested)
if len(rcwPassphrase) > 0 {
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", rcwPassphrase)
if err != nil {
return errors.New("failed to generate sanity check file: " + err.Error())
}
}
return nil
}
-62
View File
@@ -1,10 +1,6 @@
package core
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"os"
"strings"
@@ -72,64 +68,6 @@ func EntryAddPrecheck(targetLocation string) uint8 {
return 0
}
// StringGen generates a random string of a specified length and complexity.
// Requires: complexity (minimum percentage of special characters to be returned in the generated string; set to 0 to generate a simple string),
// complexCharsetLevel (1 = safe for filenames, 2 = safe for most password entries, 3 = safe only for well-made password entries)
func StringGen(length int, complexity float64, complexCharsetLevel uint8) string {
var actualSpecialChars int // track the number of special characters in the generated string
var minSpecialChars int // track the minimum number of special characters to accept
var extendedCharset string // additions to character set used for complex strings
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" // default character set used for all strings
const extendedCharsetFiles = "!#$%&+,-.;=@_~^()[]{}`'" // additional special characters for complex strings (safe in file names)
const extendedCharsetMostPassword = "*:><?|" // additional special characters for complex strings (NOT safe in file names)
const extendedCharsetSpecialPassword = "\"/\\" // additional special characters for complex strings (NOT safe in file names)
if complexity > 0 {
minSpecialChars = int(math.Round(float64(length) * complexity)) // determine minimum number of special characters to accept
switch complexCharsetLevel {
case 1:
extendedCharset = extendedCharsetFiles
case 2:
extendedCharset = extendedCharsetMostPassword + extendedCharsetFiles[:len(extendedCharsetFiles)-9]
case 3:
extendedCharset = extendedCharsetFiles + extendedCharsetMostPassword + extendedCharsetSpecialPassword
}
charset += extendedCharset
}
// loop until a string of the desired complexity is generated
for {
// generate a random string
result := make([]byte, length)
for i := range result {
val, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
result[i] = charset[val.Int64()]
}
// return early if the string is not complex
if complexity <= 0 {
return string(result)
}
// count the number of special characters in the generated string
for _, char := range string(result) {
if strings.ContainsRune(extendedCharset, char) {
actualSpecialChars++
}
}
// return the generated string if it contains enough special characters
if actualSpecialChars >= minSpecialChars {
return string(result)
}
// reset special character counter
fmt.Println("Regenerating string until desired complexity is achieved...")
actualSpecialChars = 0
}
}
// EntryIsNotEmpty iterates through entryData and returns true if any line is not empty.
func EntryIsNotEmpty(entryData []string) bool {
for _, line := range entryData {
-12
View File
@@ -1,12 +0,0 @@
//go:build !windows
package core
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)
}
-8
View File
@@ -1,8 +0,0 @@
//go:build windows
package core
// setUmask is a dummy function on Windows.
func setUmask(umask int) {
return
}