mirror of
https://github.com/rwinkhart/libmutton.git
synced 2026-09-01 22:57:25 -04:00
Do not rely on importing projects for libmutton initialization
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
package core
|
package cfg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -36,7 +36,7 @@ func ParseConfig(valuesRequested [][2]string, missingValueError string) ([]strin
|
|||||||
if value == "" {
|
if value == "" {
|
||||||
switch missingValueError {
|
switch missingValueError {
|
||||||
case "":
|
case "":
|
||||||
err = fmt.Errorf("Failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
err = fmt.Errorf("failed to find value for key \"%s\" in section \"[%s]\" in libmutton.ini", pair[1], pair[0])
|
||||||
case "0":
|
case "0":
|
||||||
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
back.Exit(0) // hard (expected) exit for CLI; GUI/TUI continue silently
|
||||||
default:
|
default:
|
||||||
@@ -72,7 +72,7 @@ func WriteConfig(valuesToWrite [][3]string, keysToPrune [][2]string, append bool
|
|||||||
var section *ini.Section
|
var section *ini.Section
|
||||||
for _, trio := range valuesToWrite {
|
for _, trio := range valuesToWrite {
|
||||||
if cfg.Section(trio[0]) == nil {
|
if cfg.Section(trio[0]) == nil {
|
||||||
// create and aquire section if it doesn't exist
|
// create and acquire section if it doesn't exist
|
||||||
section, _ = cfg.NewSection(trio[0])
|
section, _ = cfg.NewSection(trio[0])
|
||||||
} else {
|
} else {
|
||||||
// acquire existing section
|
// acquire existing section
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package core
|
package cfg
|
||||||
|
|
||||||
import "syscall"
|
import "syscall"
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package core
|
package cfg
|
||||||
|
|
||||||
// setUmask is a dummy function on Windows.
|
// setUmask is a dummy function on Windows.
|
||||||
func setUmask(umask int) {
|
func setUmask(umask int) {
|
||||||
+66
-5
@@ -1,15 +1,76 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/rwinkhart/go-boilerplate/back"
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
|
"github.com/rwinkhart/libmutton/cfg"
|
||||||
"github.com/rwinkhart/libmutton/global"
|
"github.com/rwinkhart/libmutton/global"
|
||||||
|
"github.com/rwinkhart/libmutton/synccycles"
|
||||||
"github.com/rwinkhart/rcw/wrappers"
|
"github.com/rwinkhart/rcw/wrappers"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RCWSanityCheckGen generates the RCW sanity check file for libmutton.
|
// LibmuttonInit creates the libmutton config structure based on user input.
|
||||||
func RCWSanityCheckGen(passphrase []byte) {
|
// rcwPassphrase and clientSpecificIniData are cab be left blank if not needed.
|
||||||
err := wrappers.GenSanityCheck(global.ConfigDir+global.PathSeparator+"sanity.rcw", passphrase)
|
func LibmuttonInit(inputCB func(prompt string) string, clientSpecificIniData [][3]string, rcwPassphrase []byte, preserveOldConfigDir bool) error {
|
||||||
if err != nil {
|
r := strings.ToLower(inputCB("Configure SSH settings (for synchronization)? (y/N)"))
|
||||||
back.PrintError("Failed to generate sanity check file: "+err.Error(), back.ErrorWrite, true)
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -72,64 +68,6 @@ func EntryAddPrecheck(targetLocation string) uint8 {
|
|||||||
return 0
|
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.
|
// EntryIsNotEmpty iterates through entryData and returns true if any line is not empty.
|
||||||
func EntryIsNotEmpty(entryData []string) bool {
|
func EntryIsNotEmpty(entryData []string) bool {
|
||||||
for _, line := range entryData {
|
for _, line := range entryData {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ go 1.24.3
|
|||||||
require (
|
require (
|
||||||
github.com/pkg/sftp v1.13.9
|
github.com/pkg/sftp v1.13.9
|
||||||
github.com/pquerna/otp v1.5.0
|
github.com/pquerna/otp v1.5.0
|
||||||
github.com/rwinkhart/go-boilerplate v0.0.0-20250509173525-20670ec7bb9c
|
github.com/rwinkhart/go-boilerplate v0.0.0-20250529185306-e2e64d7fa43b
|
||||||
github.com/rwinkhart/rcw v0.2.0
|
github.com/rwinkhart/rcw v0.2.0
|
||||||
golang.design/x/clipboard v0.7.0 // only for Android builds
|
golang.design/x/clipboard v0.7.0 // only for Android builds
|
||||||
golang.org/x/crypto v0.38.0
|
golang.org/x/crypto v0.38.0
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||||
github.com/rwinkhart/go-boilerplate v0.0.0-20250509173525-20670ec7bb9c h1:RIMnYf1MwsvmAr9E0/cpn7rTh8BWdfsQ2r31ITKJp2A=
|
github.com/rwinkhart/go-boilerplate v0.0.0-20250529185306-e2e64d7fa43b h1:ENgsUlCmYktd1eauEkjW6Fu8rgZzyGOS/m/6jc968xI=
|
||||||
github.com/rwinkhart/go-boilerplate v0.0.0-20250509173525-20670ec7bb9c/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
|
github.com/rwinkhart/go-boilerplate v0.0.0-20250529185306-e2e64d7fa43b/go.mod h1:cnzIF45I0FCOvE4YIB+26pLCUx2kWyY2llKYZruNaRY=
|
||||||
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
|
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410 h1:NhHwFM3Pgm6zRUfFKvi0p5ndjfFbVWsRwmmhyFlG4PE=
|
||||||
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/rwinkhart/go-winio-easy-pipe-handles v0.0.0-20250407031321-96994a0e8410/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/rwinkhart/peercred-mini v0.1.0 h1:TiS6u8cEWzW55S9X4iVpU72Iuy/NG6rMUJMtUEVEFLw=
|
github.com/rwinkhart/peercred-mini v0.1.0 h1:TiS6u8cEWzW55S9X4iVpU72Iuy/NG6rMUJMtUEVEFLw=
|
||||||
|
|||||||
+15
-12
@@ -1,6 +1,7 @@
|
|||||||
package syncclient
|
package syncclient
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -9,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
"github.com/rwinkhart/go-boilerplate/back"
|
"github.com/rwinkhart/go-boilerplate/back"
|
||||||
"github.com/rwinkhart/libmutton/core"
|
"github.com/rwinkhart/libmutton/cfg"
|
||||||
"github.com/rwinkhart/libmutton/global"
|
"github.com/rwinkhart/libmutton/global"
|
||||||
"github.com/rwinkhart/libmutton/synccommon"
|
"github.com/rwinkhart/libmutton/synccommon"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
@@ -18,7 +19,7 @@ import (
|
|||||||
|
|
||||||
// GetSSHClient returns an SSH client connection to the server (also returns the remote EntryRoot and an indicator of the server's OS).
|
// GetSSHClient returns an SSH client connection to the server (also returns the remote EntryRoot and an indicator of the server's OS).
|
||||||
// Only supports key-based authentication (passphrases are supported for CLI-based implementations).
|
// Only supports key-based authentication (passphrases are supported for CLI-based implementations).
|
||||||
func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
func GetSSHClient(manualSync bool) (*ssh.Client, string, bool, error) {
|
||||||
// get SSH config info, exit if not configured (displaying an error if the sync job was called manually)
|
// get SSH config info, exit if not configured (displaying an error if the sync job was called manually)
|
||||||
var sshUserConfig []string
|
var sshUserConfig []string
|
||||||
var missingValueError string
|
var missingValueError string
|
||||||
@@ -27,7 +28,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
} else {
|
} else {
|
||||||
missingValueError = "0" // allow silent exit at this point in offline mode
|
missingValueError = "0" // allow silent exit at this point in offline mode
|
||||||
}
|
}
|
||||||
sshUserConfig, _ = core.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}}, missingValueError)
|
sshUserConfig, _ = cfg.ParseConfig([][2]string{{"LIBMUTTON", "sshUser"}, {"LIBMUTTON", "sshIP"}, {"LIBMUTTON", "sshPort"}, {"LIBMUTTON", "sshKey"}, {"LIBMUTTON", "sshKeyProtected"}, {"LIBMUTTON", "sshEntryRoot"}, {"LIBMUTTON", "sshIsWindows"}}, missingValueError)
|
||||||
|
|
||||||
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
var user, ip, port, keyFile, keyFileProtected, entryRoot string
|
||||||
var isWindows bool
|
var isWindows bool
|
||||||
@@ -49,7 +50,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
case 6:
|
case 6:
|
||||||
isWindows, err = strconv.ParseBool(key)
|
isWindows, err = strconv.ParseBool(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to parse server OS type: "+err.Error(), back.ErrorRead, true)
|
return nil, "", false, errors.New("unable to parse server OS type: " + err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
// read private key
|
// read private key
|
||||||
key, err := os.ReadFile(keyFile)
|
key, err := os.ReadFile(keyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to read private key: "+keyFile, back.ErrorRead, true)
|
return nil, "", false, errors.New("unable to read private key: " + keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse private key
|
// parse private key
|
||||||
@@ -68,14 +69,14 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
parsedKey, err = ssh.ParsePrivateKeyWithPassphrase(key, global.GetPassphrase("Enter passphrase for your SSH keyfile:"))
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to parse private key: "+keyFile, back.ErrorRead, true)
|
return nil, "", false, errors.New("unable to parse private key: " + keyFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// read known hosts file
|
// read known hosts file
|
||||||
var hostKeyCallback ssh.HostKeyCallback
|
var hostKeyCallback ssh.HostKeyCallback
|
||||||
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
|
hostKeyCallback, err = knownhosts.New(back.Home + global.PathSeparator + ".ssh" + global.PathSeparator + "known_hosts")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to read known hosts file: "+err.Error(), back.ErrorRead, true)
|
return nil, "", false, errors.New("unable to read known hosts file: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// configure SSH client
|
// configure SSH client
|
||||||
@@ -91,11 +92,10 @@ func GetSSHClient(manualSync bool) (*ssh.Client, string, bool) {
|
|||||||
// connect to SSH server
|
// connect to SSH server
|
||||||
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
sshClient, err := ssh.Dial("tcp", ip+":"+port, sshConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to connect to remote server: "+err.Error(), global.ErrorServerConnection, false) // do not crash/close interactive clients
|
return nil, "", false, errors.New("unable to connect to remote server: " + err.Error())
|
||||||
return nil, "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshClient, entryRoot, isWindows
|
return sshClient, entryRoot, isWindows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSSHOutput runs a command over SSH and returns the output as a string.
|
// GetSSHOutput runs a command over SSH and returns the output as a string.
|
||||||
@@ -418,14 +418,17 @@ func folderSync(folders []string) {
|
|||||||
// Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client.
|
// Setting returnLists to true will return the deletions, downloads, and uploads lists for use by the client.
|
||||||
func RunJob(manualSync, returnLists bool) [3][]string {
|
func RunJob(manualSync, returnLists bool) [3][]string {
|
||||||
// get SSH client to re-use throughout the sync process
|
// get SSH client to re-use throughout the sync process
|
||||||
sshClient, sshEntryRoot, sshIsWindows := GetSSHClient(manualSync)
|
sshClient, sshEntryRoot, sshIsWindows, err := GetSSHClient(manualSync)
|
||||||
|
if err != nil {
|
||||||
|
back.PrintError("sync failed - unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
|
}
|
||||||
if sshClient == nil { // indicate SSH dialing failure for interactive clients
|
if sshClient == nil { // indicate SSH dialing failure for interactive clients
|
||||||
return [3][]string{nil, nil, nil}
|
return [3][]string{nil, nil, nil}
|
||||||
}
|
}
|
||||||
defer func(sshClient *ssh.Client) {
|
defer func(sshClient *ssh.Client) {
|
||||||
err := sshClient.Close()
|
err := sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
back.PrintError("sync failed - unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
}
|
}
|
||||||
}(sshClient)
|
}(sshClient)
|
||||||
|
|
||||||
|
|||||||
+15
-6
@@ -15,7 +15,10 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
|||||||
|
|
||||||
if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode)
|
if !forceOffline && deviceID != "" { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _, err := GetSSHClient(false)
|
||||||
|
if err != nil {
|
||||||
|
back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
|
}
|
||||||
|
|
||||||
// ensure targetLocationIncomplete ends with a slash if it is a directory (for clarity in shear message)
|
// ensure targetLocationIncomplete ends with a slash if it is a directory (for clarity in shear message)
|
||||||
if isDir && !strings.HasSuffix(targetLocationIncomplete, "/") {
|
if isDir && !strings.HasSuffix(targetLocationIncomplete, "/") {
|
||||||
@@ -26,7 +29,7 @@ func ShearRemoteFromClient(targetLocationIncomplete string, forceOffline bool) {
|
|||||||
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath))
|
GetSSHOutput(sshClient, "libmuttonserver shear", deviceID+"\n"+strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||||
|
|
||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err = sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
}
|
}
|
||||||
@@ -43,7 +46,10 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
|
|||||||
deviceIDList := global.GenDeviceIDList(true)
|
deviceIDList := global.GenDeviceIDList(true)
|
||||||
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _, err := GetSSHClient(false)
|
||||||
|
if err != nil {
|
||||||
|
back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
|
}
|
||||||
|
|
||||||
// call the server to move the target on the remote system and add the old target to the deletions list
|
// call the server to move the target on the remote system and add the old target to the deletions list
|
||||||
GetSSHOutput(sshClient, "libmuttonserver rename",
|
GetSSHOutput(sshClient, "libmuttonserver rename",
|
||||||
@@ -52,7 +58,7 @@ func RenameRemoteFromClient(oldLocationIncomplete, newLocationIncomplete string,
|
|||||||
strings.ReplaceAll(newLocationIncomplete, global.PathSeparator, global.FSPath))
|
strings.ReplaceAll(newLocationIncomplete, global.PathSeparator, global.FSPath))
|
||||||
|
|
||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err = sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
}
|
}
|
||||||
@@ -69,13 +75,16 @@ func AddFolderRemoteFromClient(targetLocationIncomplete string, forceOffline boo
|
|||||||
deviceIDList := global.GenDeviceIDList(true)
|
deviceIDList := global.GenDeviceIDList(true)
|
||||||
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
if !forceOffline && len(deviceIDList) > 0 { // ensure a device ID exists (online mode)
|
||||||
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
// create an SSH client; manualSync is false in case a device ID exists but SSH is not configured
|
||||||
sshClient, _, _ := GetSSHClient(false)
|
sshClient, _, _, err := GetSSHClient(false)
|
||||||
|
if err != nil {
|
||||||
|
back.PrintError("Sync failed - Unable to connect to SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
|
}
|
||||||
|
|
||||||
// call the server to create the folder remotely
|
// call the server to create the folder remotely
|
||||||
GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath)) // call the server to create the folder remotely
|
GetSSHOutput(sshClient, "libmuttonserver addfolder", strings.ReplaceAll(targetLocationIncomplete, global.PathSeparator, global.FSPath)) // call the server to create the folder remotely
|
||||||
|
|
||||||
// close the SSH client
|
// close the SSH client
|
||||||
err := sshClient.Close()
|
err = sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
back.PrintError("Sync failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,52 @@
|
|||||||
package syncclient
|
package synccycles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rwinkhart/go-boilerplate/back"
|
|
||||||
"github.com/rwinkhart/libmutton/core"
|
|
||||||
"github.com/rwinkhart/libmutton/global"
|
"github.com/rwinkhart/libmutton/global"
|
||||||
|
"github.com/rwinkhart/libmutton/syncclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DeviceIDGen generates a new client device ID and registers it with the server (will replace existing one).
|
// DeviceIDGen generates a new client device ID and registers it with the server (will replace existing one).
|
||||||
// Device IDs are only needed for online synchronization.
|
// Device IDs are only needed for online synchronization.
|
||||||
// Device IDs are guaranteed unique as the current UNIX time is appended to them.
|
// Device IDs are guaranteed unique as the current UNIX time is appended to them.
|
||||||
// Returns: the remote EntryRoot and OS type indicator.
|
// Returns: the remote EntryRoot and OS type indicator.
|
||||||
func DeviceIDGen(oldDeviceID string) (string, string) {
|
func DeviceIDGen(oldDeviceID string) (string, string, error) {
|
||||||
// generate new device ID
|
// generate new device ID
|
||||||
deviceIDPrefix, _ := os.Hostname()
|
deviceIDPrefix, _ := os.Hostname()
|
||||||
deviceIDSuffix := core.StringGen(rand.Intn(32)+48, 0.2, 1) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
|
deviceIDSuffix := StringGen(rand.Intn(32)+48, 0.2, 1) + "-" + strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
newDeviceID := deviceIDPrefix + "-" + deviceIDSuffix
|
newDeviceID := deviceIDPrefix + "-" + deviceIDSuffix
|
||||||
|
|
||||||
// create new device ID file (locally)
|
// create new device ID file (locally)
|
||||||
fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
|
fileToClose, err := os.OpenFile(global.ConfigDir+global.PathSeparator+"devices"+global.PathSeparator+newDeviceID, os.O_CREATE|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Failed to create local device ID file: "+err.Error(), back.ErrorWrite, true)
|
return "", "", errors.New("failed to create local device ID file: " + err.Error())
|
||||||
}
|
}
|
||||||
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
_ = fileToClose.Close() // error ignored; if the file could be created, it can probably be closed
|
||||||
|
|
||||||
// remove old device ID file (locally; may not exist)
|
// remove old device ID file (locally; may not exist)
|
||||||
err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID)
|
err = os.RemoveAll(global.ConfigDir + global.PathSeparator + "devices" + global.PathSeparator + oldDeviceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Failed to remove old device ID file (locally): "+err.Error(), back.ErrorWrite, true)
|
return "", "", errors.New("failed to remove old device ID file (locally): " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// register new device ID with server and fetch remote EntryRoot and OS type
|
// register new device ID with server and fetch remote EntryRoot and OS type
|
||||||
// also removes the old device ID file (remotely)
|
// also removes the old device ID file (remotely)
|
||||||
// manualSync is true so the user is alerted if device ID registration fails
|
// manualSync is true so the user is alerted if device ID registration fails
|
||||||
sshClient, _, _ := GetSSHClient(true)
|
sshClient, _, _, err := syncclient.GetSSHClient(true)
|
||||||
sshEntryRootSSHIsWindows := strings.Split(GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), global.FSSpace)
|
if err != nil {
|
||||||
|
return "", "", errors.New("device ID gen failed - unable to connect to SSH client: " + err.Error())
|
||||||
|
}
|
||||||
|
sshEntryRootSSHIsWindows := strings.Split(syncclient.GetSSHOutput(sshClient, "libmuttonserver register", newDeviceID+"\n"+oldDeviceID), global.FSSpace)
|
||||||
err = sshClient.Close()
|
err = sshClient.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
back.PrintError("Init failed - Unable to close SSH client: "+err.Error(), global.ErrorServerConnection, true)
|
return "", "", errors.New("device ID gen failed - unable to close SSH client: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1]
|
return sshEntryRootSSHIsWindows[0], sshEntryRootSSHIsWindows[1], nil
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package synccycles
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user